自定义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事件一层层的传递。

猜你喜欢

转载自blog.csdn.net/qq_37321098/article/details/81745998
今日推荐