Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義View詳解

自定義View詳解

編輯:關於Android編程

雖然之前也分析過View回執過程,但是如果讓我自己集成ViewGroup然後自己重新onMeasure,onLayout,onDraw方法自定義View我還是會頭疼。今天索性來系統的學習下。

onMeasure

/**
     * <p>
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass's responsibility to make
     * sure the measured height and width are at least the view's minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

注釋說的非常清楚。但是我還是要強調一下這兩個參數:widthMeasureSpecheightMeasureSpec這兩個int類型的參數,看名字應該知道是跟寬和高有關系,但它們其實不是寬和高,而是由寬、高和各自方向上對應的模式來合成的一個值:其中,在int類型的32位二進制位中,31-30這兩位表示模式,0~29這三十位表示寬和高的實際值.其中模式一共有三種,被定義在Android中的View類的一個內部類中:View.MeasureSpec:

android.view
public static class View.MeasureSpec
extends Object
A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode. There are three possible modes:
UNSPECIFIED
The parent has not imposed any constraint on the child. It can be whatever size it wants.
EXACTLY
The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.
AT_MOST
The child can be as large as it wants up to the specified size.
MeasureSpecs are implemented as ints to reduce object allocation. This class is provided to pack and unpack the  tuple into the int.
MeasureSpec.UNSPECIFIED The parent has not imposed any constraint on the child. It can be whatever size it wants. 這種情況比較少,一般用不到。標示父控件沒有給子View任何顯示- - - -對應的二進制表示: 00 MeasureSpec.EXACTLY The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.
理解成MATCH_PARENT或者在布局中指定了寬高值,如layout:width=’50dp’. - - - - 對應的二進制表示:01 MeasureSpec.AT_MOST The child can be as large as it wants up to the specified size.理解成WRAP_CONTENT,這是的值是父View可以允許的最大的值,只要不超過這個值都可以。- - - - 對應的二進制表示:10

那具體MeasureSpec是怎麼把寬和高的實際值以及模式組合起來變成一個int類型的值呢? 這部分是在MeasureSpce.makeMeasureSpec()方法中處理的:

public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

那我們如何從MeasureSpec值中提取模式和大小呢?該方法內部是采用位移計算.

/**
 * Extracts the mode from the supplied measure specification.
 *
 * @param measureSpec the measure specification to extract the mode from
 * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
 *         {@link android.view.View.MeasureSpec#AT_MOST} or
 *         {@link android.view.View.MeasureSpec#EXACTLY}
 */
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

/**
 * Extracts the size from the supplied measure specification.
 *
 * @param measureSpec the measure specification to extract the size from
 * @return the size in pixels defined in the supplied measure specification
 */
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

onLayout

為了能合理的去繪制定義View,你需要制定它的大小。復雜的自定義View通常需要根據屏幕的樣式和大小來進行復雜的布局計算。你不應該假設你的屏幕上的View的大小。即使只有一個應用使用你的自定義View,也需要處理不同的屏幕尺寸、屏幕密度和橫屏以及豎屏下的多種比率等。

雖然View有很多處理測量的方法,但他們中的大部分都不需要被重寫。如果你的View不需要特別的控制它的大小,你只需要重寫一個方法:onSizeChanged()

onSizeChanged()方法會在你的View第一次指定大小後調用,在因某些原因改變大小後會再次調用。在上面PieChart的例子中,onSizeChanged()方法就是它需要重新計算表格樣式和大小以及其他元素的地方。
下面就是PieChart.onSizeChanged()方法的內容:

// Account for padding
float xpad = (float)(getPaddingLeft() + getPaddingRight());
float ypad = (float)(getPaddingTop() + getPaddingBottom());

// Account for the label
if (mShowText) xpad += mTextWidth;

float ww = (float)w - xpad;
float hh = (float)h - ypad;

// Figure out how big we can make the pie.
float diameter = Math.min(ww, hh);

onDraw

自定義View最重要的就是展現樣式。

重寫onDraw()方法

繪制自定義View最重要的步驟就是重寫onDraw()方法。onDraw()方法的參數是Canvas對象。可以用它來繪制自身。Canvas類定義了繪制文字、線、位圖和很多其他圖形的方法。你可以在onDraw()方法中使用這些方法來指定UI.

在使用任何繪制方法之前,你都必須要創建一個Paint對象。

創建繪制的對象

android.graphics框架將繪制分為兩步:

繪制什麼,由Canvas處理。 怎麼去繪制,由Paint處理。
Canvas

The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels,
a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap),
and a paint (to describe the colors and styles for the drawing).

Canvas():創建一個空的畫布,可以使用setBitmap()方法來設置繪制的具體畫布; Canvas(Bitmap bitmap):以bitmap對象創建一個畫布,則將內容都繪制在bitmap上,bitmap不得為null; canvas.drawRect(RectF,Paint)方法用於畫矩形,第一個參數為圖形顯示區域,第二個參數為畫筆,設置好圖形顯示區域Rect和畫筆Paint後,即可畫圖; canvas.drawRoundRect(RectF, float, float, Paint)方法用於畫圓角矩形,第一個參數為圖形顯示區域,第二個參數和第三個參數分別是水平圓角半徑和垂直圓角半徑。 canvas.drawLine(startX, startY, stopX, stopY, paint):前四個參數的類型均為float,最後一個參數類型為Paint。表示用畫筆paint從點(startX,startY)到點(stopX,stopY)畫一條直線; canvas.drawLines (float[] pts, Paint paint)``pts:是點的集合,大家下面可以看到,這裡不是形成連接線,而是每兩個點形成一條直線,pts的組織方式為{x1,y1,x2,y2,x3,y3,……},例如float []pts={10,10,100,100,200,200,400,400};就是有四個點:(10,10)、(100,100),(200,200),(400,400)),兩兩連成一條直線; canvas.drawArc(oval, startAngle, sweepAngle, useCenter, paint):第一個參數ovalRectF類型,即圓弧顯示區域,startAnglesweepAngle均為float類型,分別表示圓弧起始角度和圓弧度數,3點鐘方向為0度,useCenter設置是否顯示圓心,boolean類型,paint為畫筆; canvas.drawCircle(float,float, float, Paint)方法用於畫圓,前兩個參數代表圓心坐標,第三個參數為圓半徑,第四個參數是畫筆; canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) 位圖,參數一就是我們常規的Bitmap對象,參數二是源區域(這裡是bitmap),參數三是目標區域(應該在canvas的位置和大小),參數四是Paint畫刷對象,因為用到了縮放和拉伸的可能,當原始Rect不等於目標Rect時性能將會有大幅損失。 canvas.drawText(String text, float x, floaty, Paint paint)渲染文本,Canvas類除了上
面的還可以描繪文字,參數一是String類型的文本,參數二x軸,參數三y軸,參數四是Paint對象。 canvas.drawPath (Path path, Paint paint),根據Path去畫.
java
Path path = new Path();
path.moveTo(10, 10); //設定起始點
path.lineTo(10, 100);//第一條直線的終點,也是第二條直線的起點
path.lineTo(300, 100);//畫第二條直線
path.lineTo(500, 100);//第三條直線
path.close();//閉環
canvas.drawPath(path, paint);

Paint
setARGB(int a, int r, int g, int b) 設置Paint對象顏色,參數一為alpha透明值 setAlpha(int a) 設置alpha不透明度,范圍為0~255 setAntiAlias(boolean aa)是否抗鋸齒 setColor(int color)設置顏色 setTextScaleX(float scaleX)設置文本縮放倍數,1.0f為原始 setTextSize(float textSize)設置字體大小 setUnderlineText(String underlineText)設置下劃線

例如,Canvas提供了一個畫一條線的方法,而Paint提供了指定這條線的顏色的方法。Canvas提供了繪制長方形的方法,而Paint提供了是用顏色填充整個長方形還是空著的方法。簡單的說,Canvas指定了你想在屏幕上繪制的形狀,而Paint指定了你要繪制的形狀的顏色、樣式、字體和樣式等等。

所以,在你draw任何東西之前,你都需要創建一個或者多個Paint對象。下面的PieChart例子就是在構造函數中調用的init方法:

private void init() {
   mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mTextPaint.setColor(mTextColor);
   if (mTextHeight == 0) {
       mTextHeight = mTextPaint.getTextSize();
   } else {
       mTextPaint.setTextSize(mTextHeight);
   }

   mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mPiePaint.setStyle(Paint.Style.FILL);
   mPiePaint.setTextSize(mTextHeight);

   mShadowPaint = new Paint(0);
   mShadowPaint.setColor(0xff101010);
   mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));

   ...

下面是PieChart完整的onDraw()方法:

protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);

   // Draw the shadow
   canvas.drawOval(
           mShadowBounds,
           mShadowPaint
   );

   // Draw the label text
   canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);

   // Draw the pie slices
   for (int i = 0; i < mData.size(); ++i) {
       Item it = mData.get(i);
       mPiePaint.setShader(it.mShader);
       canvas.drawArc(mBounds,
               360 - it.mEndAngle,
               it.mEndAngle - it.mStartAngle,
               true, mPiePaint);
   }

   // Draw the pointer
   canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
   canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}

下面是一張View繪制過程中框架調用的一些標准方法概要圖:
image

下面來幾個例子:

自定義開關:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> public class ToogleView extends View { private int mSlideMarginLeft = 0; private Bitmap backgroundBitmap; private Bitmap slideButton; public ToogleView(Context context) { super(context); init(context); } public ToogleView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public ToogleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_bg); slideButton = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_slide); this.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mSlideMarginLeft == 0) { mSlideMarginLeft = backgroundBitmap.getWidth() - slideButton.getWidth(); } else { mSlideMarginLeft = 0; } invalidate(); } }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setAntiAlias(true); // 先畫背景圖 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再畫滑塊,用mSlideMarginLeft來控制滑塊距離左邊的距離。 canvas.drawBitmap(slideButton, mSlideMarginLeft, 0, paint); }




    

image
很明顯顯示的不對,因為高設置為warp_content了,但是界面顯示的確實整個屏幕,而且paddingLeft也沒生效,那該怎麼做呢? 當然是重寫onMeasure() 方法:

public class ToogleView extends View {
    private int mSlideMarginLeft = 0;
    private Bitmap backgroundBitmap;
    private Bitmap slideButton;


    public ToogleView(Context context) {
        super(context);
        init(context);
    }

    public ToogleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ToogleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.toogle_bg);
        slideButton = BitmapFactory.decodeResource(getResources(),
                R.drawable.toogle_slide);
        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mSlideMarginLeft == 0) {
                    mSlideMarginLeft = backgroundBitmap.getWidth() - slideButton.getWidth();
                } else {
                    mSlideMarginLeft = 0;
                }
                invalidate();
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);

        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width;
        int height;
        if (MeasureSpec.EXACTLY == measureWidthMode) {
            width = measureWidth;
        } else {
            width = backgroundBitmap.getWidth();
        }

        if (MeasureSpec.EXACTLY == measureHeightMode) {
            height = measureHeight;
        } else {
            height = backgroundBitmap.getHeight();
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        canvas.drawBitmap(backgroundBitmap, getPaddingLeft(), 0, paint);
        canvas.drawBitmap(slideButton, mSlideMarginLeft + getPaddingLeft(), 0, paint);
    }

}

這樣就可以了。簡單的說明一下,就是如果當前的模式是EXACTLY那就把父View傳遞進來的寬高設置進來,如果是AT_MOST或者UNSPECIFIED的話就使用背景圖片的寬高。

最後再來一個自定義ViewGroup的例子:

之前的引導頁面都是通過類似ViewPager這種方法左右滑動,現在想讓他上下滑動,該怎麼弄呢?

public class VerticalLayout extends ViewGroup {
    public VerticalLayout(Context context) {
        super(context);
    }
    public VerticalLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public VerticalLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }
}

繼承ViewGroup必須要重寫onLayout方法。其實這也很好理解,因為每個ViewGroup的排列方式不一樣,所以讓子類來自己實現是最好的。
當然畜類重寫onLayout之外,也要重寫onMeasure
代碼如下,滑動手勢處理的部分就不貼了。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measureSpec = MeasureSpec.makeMeasureSpec(mScreenHeight
                * getChildCount(), MeasureSpec.getMode(heightMeasureSpec));
        super.onMeasure(widthMeasureSpec, measureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 就像猴子撈月一樣,讓他們一個個的從上往下排就好了
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != View.GONE) {
                    child.layout(l, i * mScreenHeight, r, (i + 1)
                            * mScreenHeight);
                }
            }
        }
    }

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved