1. 程式人生 > >自定義View的三大流程淺析

自定義View的三大流程淺析

目錄

 

1.自定義View簡介

2.MeasureSpec

1) SpecMode

3.View的工作流程

1) View的measure過程

2)ViewGroup的measure過程

3)layout流程

4)draw流程


1.自定義View簡介

理解View的基本流程(包括測量流程,佈局流程,繪製流程),可以實現各種效果的自定義View。

自定義View可以分成兩大類:

1.繼承檢視ViewGroup (ViewGroup、LinearLayout、FrameLayout、RelativeLayout等)

2.繼承控制元件View(View、TextView、ImageView、Button等)

View的繪製基本上由measure()、layout()、draw()這個三個函式完成,再講3大流程前,先理解幾個名詞。

2.MeasureSpec

MeasureSpec參與View的測量工作,自身為32位的int值,高二位代表SpecMode(測量模式),低30位代表SpecSize(在某種測量模式下的規格大小)。SpecMode和SpecSize都為int型別。一組SpecMode和SpecSize可以組合成一個MeasureSpec。也可通過MeasureSpec去拿到SpecMode或SpecSize。

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {


...
 public static class MeasureSpec {

  public static final int UNSPECIFIED = 0 << MODE_SHIFT;

  public static final int EXACTLY     = 1 << MODE_SHIFT;

  public static final int AT_MOST     = 2 << MODE_SHIFT;

   @MeasureSpecMode
   public static int getMode(int measureSpec) {
       return (measureSpec & MODE_MASK);
   }

   public static int getSize(int measureSpec) {
       return (measureSpec & ~MODE_MASK);
   }
}
}

       可以看出View的靜態內部類MeasureSpec,定義了3常量,UNSPECIFIED和EXACTLY和AT_MOST,這3個都是父容器對子容器的約束等級,同時也能看出getMode和getSize函式能獲取到父容器的大小值和約束型別。

       正常情況使用View指定的MeasureSpec進行View的測量,同時可給View設定LayoutParams。LayoutParams在View測量時,會在父容器的約束下轉換成對應MeasureSpec,從而確定View的寬高。

        因此,View的MeasureSpec確定(即View的寬高確定)需要父容器的MeasureSpec約束,加上View的LayoutParams才行。下面驗證一下。View的測量由measure函式實現,但是,measure函式呼叫前,都會先呼叫父容器的getChildMeasureSpec函式

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        //父容器施加了一個精確的尺寸
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父容器指定最大值
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父容器不限制大小
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

翻譯如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        獲取限制資訊中的尺寸和模式。
        switch (限制資訊中的模式) {
            case 當前容器的父容器,給當前容器設定了一個精確的尺寸:
                if (子View申請固定的尺寸) {
                    你就用你自己申請的尺寸值就行了;
                } else if (子View希望和父容器一樣大) {
                    你就用父容器的尺寸值就行了;
                } else if (子View希望包裹內容) {
                    你最大尺寸值為父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
                } 
                    break;
            case 當前容器的父容器,給當前容器設定了一個最大尺寸:
                if (子View申請固定的尺寸) {
                    你就用你自己申請的尺寸值就行了;
                } else if (子View希望和父容器一樣大) {
                    你最大尺寸值為父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
                } else if (子View希望包裹內容) {
                    你最大尺寸值為父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
                } 
                    break;
            case 當前容器的父容器,對當前容器的尺寸不限制:
                if (子View申請固定的尺寸) {
                    你就用你自己申請的尺寸值就行了;
                } else if (子View希望和父容器一樣大) {
                    父容器對子View尺寸不做限制。
                } else if (子View希望包裹內容) {
                    父容器對子View尺寸不做限制。
                }
                    break;
        } return 對子View尺寸的限制資訊;
    }

不難看出,父容器的MeasureSpec和View的LayoutParams確定子View的MeasureSpec,同時View的margin和padding也會有影響。

1) SpecMode

UNSPECIFIED:父容器不對內部view做大小限制

EXACTLY:父容器已檢測出view的精確大小,此時view大小即為SpecSize所指定的值。此情況對應為view設定了match_parent或者指定值大小的LayoutParams這兩模式

AT_MOST:父容器指定最大值,View的大小不能超出。此情況對應於View設定了wrap_content的LayoutParams。

3.View的工作流程

如果是view,measure函式就可以完成測量過程。如果是viewGroup,除了自身測量外,同時也會去遍歷子元素的measure函式。

1) View的measure過程

常量方法measure內部呼叫onMeasure方法,只需看onMeasure的實現。

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

測量完大小之後都必須呼叫setMeasuredDimension()方法來儲存view的測量值。下面看看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;
    }

可以看出AT_MOST和EXACTLY父佈局限制條件下,getDefaultSize()返回的就是MeasureSpec中測量大小過的SpecSize,注意的是這裡只是返回測量大小,View真正大小是在layout流程中決定的。但是幾乎所有情況下測量過後的大小和最終大小是相等的。而UNSPECIFIED父佈局限制條件下的測量大小為getSuggestedMinimumWidth()/getSuggestedMinimumHeight()的返回值。

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
 protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

返回值與View是否設定背景相關。

2)ViewGroup的measure過程

ViewGroup通過measureChild(int widthMeasureSpec, int 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);
            }
        }
    }

在看看measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)

 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);
    }

可以看出measureChild()方法,拿到子元素的LayoutParams,再根據父容器的MeasureSpec和padding值,然後去生成子元素的MeasureSpec,最終交由view的measure處理。

3)layout流程

當ViewGroup位置確定後,會在onLayout方法中遍歷子元素的layout()方法去決定子元素位置,看下layout()原始碼

public void layout(int l, int t, int r, int b) {
        ...
        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,決定子元素在viewgroup中的位置
            onLayout(changed, l, t, r, b);

            ...
        }
        ...
}

layout方法中先通過setFrame()方法確定子View的四個頂點位置,又重新呼叫onLayout,確定子元素在viewgroup中的位置。

4)draw流程

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, 畫背景
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 2和5步驟跳過
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, 畫內容
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, 畫子元素view。遍歷呼叫子元素view的draw方法
            dispatchDraw(canvas);

            // Step 6, 畫裝飾 (如scrollbars的滾動條)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
            return;
        }
        ...
    }

可以看出繪製分為6步,其中第二步和第五步通常都是跳過的。我們只看剩下的四步:

  1. 繪製背景
  2. 繪製自己的內容(onDraw())
  3. 繪製子view(dispatchDraw())
  4. 繪製裝飾

細節還是很多的,其中dispatchDraw()會遍歷所有子元素的draw方法,使得draw事件一層層的傳遞。