Android進階之自定義控件二

瞭解自定義控件的三大流程(measure、layout、draw)

在上一篇博客中我們大致介紹了一下View和ViewGroup,接下來我們就學習一下自定義控件的三大流程,爲我們打下夯實的基礎。(本博客主要參考《Android羣英傳》和《Android開發藝術探索》,大家也可以去閱讀這兩本書籍)

自定義控件三大流程簡介

什麼是自定義控件的三大流程,相信正在閱讀這篇博客的你肯定接觸過自定義控件,也見過onMeasure()、onLayout()和onDraw()這三個方法,自定義控件的三大流程就是這三個方法了,下面就讓我們循序漸進的瞭解一下這三個方法。

measure

onMeasure方法的作用通俗點講就是確認View位置,而對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷去調用所有子元素的measure方法,各子元素再遞歸去執行這個過程。下面就依次來講解:

1、View的測量過程

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

我們在onMeasure上按住ctrl後鼠標左擊,進入 super.onMeasure的源碼,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

這裏我們大致的可以明白onMeasure方法是通過setMeasuredDimension來控制控件大小的,我們不需要深入的去了解,我們在看一下getDefaultSize方法返回的是什麼?如何來控制控件大小的,代碼如下:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

這段代碼相信大家很容易理解,當然我們首先需要了解一下MeasureSpec這個類,MeasureSpec其實是一個32位的int值,高2位爲測量模式,低30位爲測量的大小,內部封裝了一些獲取測量模式和測量大小的位運算。測量模式分一下三種:

UNSPECIFIED

中文翻譯爲未特別指定(規定)的,既父容器不對View有任何限制,View想要多大就多大

EXACTLY

中文翻譯爲精確地,即精確值模式,但我們將控件的layout_width或者layout_height指定爲固定值,如“10dp”,或者爲match_parent時使用該模式

AT_MOST

最大值模式,當控件寬高指定爲wrap_content時,使用改模式

通過上面的分析我們可以很輕鬆的寫出我們自己想要的測量方法,下面給出一個示例:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
    }

    public static int getMeasureSize(int measureSpec) {
        int size = 200;
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

2、ViewGroup的measure過程

ViewGroup是一個抽象類,因此它沒有重寫View的onMeasure方法,但它提供了一個measureChildren的方法,同樣的我們依次地大致閱讀以下源碼:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }


相信大家也能很容易的讀懂,原理就是遍歷ViewGroup內所有的View去調用View的measure方法。

layout

Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它在onLayout中會遍歷所有的子元素並調用其layout方法,在layout方法中子view的onLayout方法有會被調用,這段話大家可能看的雲裏霧裏,下面給出LinearLayout的onLayout的源碼,相信大家就一目瞭然了:

1、寫一個繼承LinearLayout的類,重寫onLayout方法

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

2、進入super.onLayout方法

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

3、從源碼很明顯可以看出線性佈局分垂直和水平方向,我們以垂直方向爲例,進入layoutVertical方法:

這裏代碼量很大,大家沒必要看的非常仔細,首先找到for (int i = 0; i < count; i++)這個for循環,大家肯定明白這是遍歷LinearLayout中的子View,之後調用setChildFrame這個方法確定子View的位置

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

4、接下來進入setChildFrame方法看如何實現確定子View的位置

private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

到這裏大家就稍微明白了,原來是調用子View的layout方法來確定子View的位置

5、進入layout方法一探究竟

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

這裏代碼量也有些多,相信仔細看了的肯定也能讀懂,沒懂也沒關係,不用看得那麼仔細,首先找到boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
setOpticalFrame和setFrame從參數我們就大致能猜到是確定子View的位置的,之後再找到onLayout(changed, l, t, r, b);這個方法,這樣就到了子View的onLayout方法中,若子View是一個ViewGroup的話又可以確定子View的位置了,這樣就可以確定一個View樹上所有View的位置。

相信看到這裏大家都恍然大悟了,希望大家在看博客的時候也打開eclipse或者as簡單的閱讀以下源碼,參考源碼我相信大家聰慧的大腦,肯定能玩轉自定義View的layout。

draw

當View的位置確定好之後我們就要開始繪製View了,這裏我們就需要了解一下Paint和Canvas這兩個對象了,相信大家已經非常熟悉了,這裏稍微做一下總結:

1.Paint(畫筆)類
要繪製圖形,首先得調整畫筆,按照自己的開發需要設置畫筆的相關屬性。Pain類的常用屬性設置方法如下:

  setAntiAlias(); //設置畫筆的鋸齒效果

  setColor(); //設置畫筆的顏色

  setARGB(); //設置畫筆的A、R、G、B值

  setAlpha(); //設置畫筆的Alpha值

  setTextSize(); //設置字體的尺寸

  setStyle(); //設置畫筆的風格(空心或實心)

  setStrokeWidth(); //設置空心邊框的寬度

  getColor(); //獲取畫筆的顏色
  
2.Canvas(畫布)類

  畫筆屬性設置好之後,還需要將圖像繪製到畫布上。Canvas類可以用來實現各種圖形的繪製工作,如繪製直線、矩形、圓等等。Canvas繪製常用圖形的方法如下:

  繪製直線:canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);

  繪製矩形:canvas.drawRect(float left, float top, float right, float bottom, Paint paint);

  繪製圓形:canvas.drawCircle(float cx, float cy, float radius, Paint paint);

  繪製字符:canvas.drawText(String text, float x, float y, Paint paint);

  繪製圖形:canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);

示例:簡單的實現一下音頻條效果

public class CustomView extends View {

    private Paint mPaint = null;
    private int count = 80;
    private int mRectWidth = 10;
    private int offset = 2;
    private float mRectHight = 400;
    private float mCurrentHight = 400;

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

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

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

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
    }

    public static int getMeasureSize(int measureSpec) {
        int size = 200;
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        LinearGradient mLinearGradient = new LinearGradient(
                0,
                0,
                mRectWidth,
                mRectHight,
                Color.YELLOW,
                Color.BLUE,
                Shader.TileMode.CLAMP
        );

        mPaint.setShader(mLinearGradient);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < count; i++){
            double mRandom = Math.random();
            mCurrentHight = (float) (mRectHight * mRandom);
            canvas.drawRect((float)(mRectWidth * i + offset),
                    mRectHight-mCurrentHight,
                    (float)(mRectWidth * (i+1)),
                    mRectHight,
                    mPaint);

        }
        postInvalidateDelayed(300);
    }
}

這裏寫圖片描述

這個示例博主這裏就不做解釋了,實現方法有很多種,我只是提供一種思路,更多的還是希望讀者親自去敲一遍試試效果,遇到不懂的baidu或者google,這樣收穫會更多,感謝您的閱讀,下一篇將繼續討論自定義View的滑動和事件分發機制,歡迎大家進一步學習!自定義控件的時間分發、攔截、處理http://blog.csdn.net/u010083327/article/details/60874681

發佈了27 篇原創文章 · 獲贊 10 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章