查看原文
其他

带你彻底搞懂-View的工作原理!

胡飞洋 胡飞洋 2022-07-18

目录

  • 1、ViewRoot 和 DecorView

  • 2、 MeasureSpec

    • 2.1MeasureSpec

    • 2.2MeasureSpec和LayoutParams的对应关系

  • 3、View的工作流程

    • 3.1 Measure过程

      • 3.1.1 view的测量过程

      • 3.1.2 ViewGroup的测量过程

      • 3.1.3 获取View宽高的时机

        • 1、Activity/View#onWindowFocusChanged

        • 2、view.post(runnable)

        • 3、ViewTreeObserver

    • 3.2Layout过程

    • 3.3Draw过程

  • 4、自定义View

    • 4.1自定义view的分类

    • 4.2 自定义view 注意点

    • 4.3 例子

    • 4.4 自定义view的思想



1、ViewRoot 和 DecorView


1.ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带。View的三大流程是通过ViewRoot完成的。 在ActivityThread中,当Activity对象被创建完毕时,会将DecorView添加到Window中,同时会创建ViewRootImpl,且ViewRootImpl和DecorView会建立关联。如下代码,WindowManagerGlobal的addView()方法:

1public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
2            ...
3            root = new ViewRootImpl(view.getContext(), display);
4            root.setView(view, wparams, panelParentView);
5            ...
6            }


2.View绘制流程从 performTraversals开始,经过Measure、layout、draw。流程图如下


DecorVie是顶级View,是一个FrameLayout,上面是标题栏、下面是内容栏。内容栏就是setContengView的内容view,id是content。事件 经过DecorView 然后传给我们自己的View。


2、 MeasureSpec

MeasureSpec封装了从父级传递到子级的布局要求。系统把view的LayoutParams 根据 父容器施加的规则(父容器的SpecMode) 转换成 view的MeasureSpec,然后使用这个MeasureSpec确定view的测量宽高(不一定是最终宽高)。


2.1MeasureSpec

  1. MeasureSpec---view的测量规格:高2位的SpecMode,低30位的SpecSize。

  2. SpecMode的分类
    UNPECIFIED父容器对view不限制,要多大给多大,一般系统内部使用。
    EXACTLY,父容器检测出view所需大小,view最终大小就是SpecSize的值。对应 LayoutParams中的matchParent、具体数值 两种模式。
    AT_MOST,父容器制定了可用大小即SpecSize,view的大小不能大于这个值,具体要看view的具体实现。对应LayoutParams中的wrap_content。


2.2MeasureSpec和LayoutParams的对应关系

前面说了View的MeasureSpec是由LayoutParams和父容器的MeasureSpec共同决定。顶级view,即DecorView,是由窗口尺寸和自身LayoutParams决定

1、DecorView,ViewRootImpl中measureHierarchy()方法(performTraversals中执行),代码如下,desiredWindowWidth、desiredWindowHeight是屏幕的尺寸。

1private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
2            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
3            ...
4                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
5                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
6                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
7            ...
8            }


performMeasure()内部是调用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),mView就是DecorVIew。继续看getRootMeasureSpec()方法如下:


1/**
2     * Figures out the measure spec for the root view in a window based on it's
3     * layout params.
4     *
5     * @param windowSize
6     *            The available width or height of the window
7     *
8     * @param rootDimension
9     *            The layout params for one dimension (width or height) of the
10     *            window.
11     *
12     * @return The measure spec to use to measure the root view.
13     */
14    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
15        int measureSpec;
16        switch (rootDimension) {
17
18        case ViewGroup.LayoutParams.MATCH_PARENT:
19            // Window can't resize. Force root view to be windowSize.
20            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
21            break;
22        case ViewGroup.LayoutParams.WRAP_CONTENT:
23            // Window can resize. Set max size for root view.
24            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
25            break;
26        default:
27            // Window wants to be an exact size. Force root view to be that size.
28            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
29            break;
30        }
31        return measureSpec;
32    }

DecorView的MeasureSpec就明确了,根据其LayoutParams:

  • MATCH_PARENT:精确模式,就是窗口大小;

  • WRAP_CONTENT:最大值模式,最大值不能超过窗口大小;

  • 固定值(如100dp):精确模式,就是LayoutParams的指定值。


2、普通View,测量过程从ViewGroup传递下来,看ViewGroup的measureChildWithMargins()方法:


1/**
2     * Ask one of the children of this view to measure itself, taking into
3     * account both the MeasureSpec requirements for this view and its padding
4     * and margins. The child must have MarginLayoutParams The heavy lifting is
5     * done in getChildMeasureSpec.
6     *
7     * @param child The child to measure
8     * @param parentWidthMeasureSpec The width requirements for this view
9     * @param widthUsed Extra space that has been used up by the parent
10     *        horizontally (possibly by other children of the parent)
11     * @param parentHeightMeasureSpec The height requirements for this view
12     * @param heightUsed Extra space that has been used up by the parent
13     *        vertically (possibly by other children of the parent)
14     */
15    protected void measureChildWithMargins(View child,
16            int parentWidthMeasureSpec, int widthUsed,
17            int parentHeightMeasureSpec, int heightUsed) {
18        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
19
20        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
21                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
22                        + widthUsed, lp.width);
23        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
24                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
25                        + heightUsed, lp.height);
26
27        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
28    }


即先获取child的MeasureSpec,再调child.measure()。可以看到,child的MeasureSpec是由父容器的MeasureSpec、父容器的padding、child的LayoutParams、child的marging 共同决定。继续看getChildMeasureSpec()方法:


1/**
2     * Does the hard part of measureChildren: figuring out the MeasureSpec to
3     * pass to a particular child. This method figures out the right MeasureSpec
4     * for one dimension (height or width) of one child view.
5     *
6     * The goal is to combine information from our MeasureSpec with the
7     * LayoutParams of the child to get the best possible results. For example,
8     * if the this view knows its size (because its MeasureSpec has a mode of
9     * EXACTLY), and the child has indicated in its LayoutParams that it wants
10     * to be the same size as the parent, the parent should ask the child to
11     * layout given an exact size.
12     *
13     * @param spec The requirements for this view
14     * @param padding The padding of this view for the current dimension and
15     *        margins, if applicable
16     * @param childDimension How big the child wants to be in the current
17     *        dimension
18     * @return a MeasureSpec integer for the child
19     */
20    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
21        int specMode = MeasureSpec.getMode(spec);
22        int specSize = MeasureSpec.getSize(spec);
23
24        //padding,就是已被占用的空间,就是 父容器的padding+child的marging
25        //size,是ViewGroup本身size减去已使用的空间,是ViewGroup能提供给child的最大值。
26        int size = Math.max(0, specSize - padding);
27
28        int resultSize = 0;
29        int resultMode = 0;
30
31        switch (specMode) {
32        // Parent has imposed an exact size on us
33        case MeasureSpec.EXACTLY:
34            if (childDimension >= 0) {
35                resultSize = childDimension;
36                resultMode = MeasureSpec.EXACTLY;
37            } else if (childDimension == LayoutParams.MATCH_PARENT) {
38                // Child wants to be our size. So be it.
39                resultSize = size;
40                resultMode = MeasureSpec.EXACTLY;
41            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
42                // Child wants to determine its own size. It can't be
43                // bigger than us.
44                resultSize = size;
45                resultMode = MeasureSpec.AT_MOST;
46            }
47            break;
48
49        // Parent has imposed a maximum size on us
50        case MeasureSpec.AT_MOST:
51            if (childDimension >= 0) {
52                // Child wants a specific size... so be it
53                resultSize = childDimension;
54                resultMode = MeasureSpec.EXACTLY;
55            } else if (childDimension == LayoutParams.MATCH_PARENT) {
56                // Child wants to be our size, but our size is not fixed.
57                // Constrain child to not be bigger than us.
58                resultSize = size;
59                resultMode = MeasureSpec.AT_MOST;
60            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
61                // Child wants to determine its own size. It can't be
62                // bigger than us.
63                resultSize = size;
64                resultMode = MeasureSpec.AT_MOST;
65            }
66            break;
67
68        // Parent asked to see how big we want to be
69        case MeasureSpec.UNSPECIFIED:
70            if (childDimension >= 0) {
71                // Child wants a specific size... let him have it
72                resultSize = childDimension;
73                resultMode = MeasureSpec.EXACTLY;
74            } else if (childDimension == LayoutParams.MATCH_PARENT) {
75                // Child wants to be our size... find out how big it should
76                // be
77                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
78                resultMode = MeasureSpec.UNSPECIFIED;
79            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
80                // Child wants to determine its own size.... find out how
81                // big it should be
82                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
83                resultMode = MeasureSpec.UNSPECIFIED;
84            }
85            break;
86        }
87        //noinspection ResourceType
88        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
89    }


可见,view的MeasureSpec由viewParent的MeasureSpec和自身layoutParams确定。另外,child的可利用的尺寸是parent尺寸减去padding,上面代码已有注释,这很好理解。


梳理如下:

parentSpecMode
/childLayoutParams
EXACTLYAT_MOSTUNSPECIFIED
dp/pxEXACTLY
childSize
EXACTLY
childsize
EXACTLY
childsize
match_parentEXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_contentAT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

注意,parentSize是父容器可使用的大小。



3、View的工作流程

View的三大流程,measure、layout、draw。measure确定view的测量宽高,layout确定view的最终宽高和四个顶点位置,draw绘制到屏幕。


3.1 Measure过程

view的测量过程,由measure()方法完成。viewGroup测量自身后,还需调用child.measure()遍历测量子view。


3.1.1 view的测量过程

1/**
2     * <p>
3     * This is called to find out how big a view should be. The parent
4     * supplies constraint information in the width and height parameters.
5     * </p>
6     *
7     * <p>
8     * The actual measurement work of a view is performed in
9     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
10     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
11     * </p>
12     *
13     *
14     * @param widthMeasureSpec Horizontal space requirements as imposed by the
15     *        parent
16     * @param heightMeasureSpec Vertical space requirements as imposed by the
17     *        parent
18     *
19     * @see #onMeasure(int, int)
20     */
21public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
22    ...
23    // measure ourselves, this should set the measured dimension flag back
24     onMeasure(widthMeasureSpec, heightMeasureSpec);
25    ...
26}


可见view的measure()方法是final,不可被子类重写。里面调用onMeasure(),实际真正的测量过程在onMeasure()中。所以只有onMeasure()可以且必须被子类重写。另外,参数widthMeasureSpec、heightMeasureSpec就是上一节最后的表格中的值。继续看onMeasure():

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


从名字就可以看出,setMeasuredDimension()就是设置测量的尺寸,且在onMeasure()中必须被调用,否则在测量时会发送异常。getDefaultSize()获取默认的宽/高。所以View类中的onMeasure() 是设置默认的宽高。== 继续看getDefaultSize()具体实现:

1public static int getDefaultSize(int size, int measureSpec) {
2        int result = size;
3        int specMode = MeasureSpec.getMode(measureSpec);
4        int specSize = MeasureSpec.getSize(measureSpec);
5
6        switch (specMode) {
7        case MeasureSpec.UNSPECIFIED:
8            result = size;
9            break;
10        case MeasureSpec.AT_MOST:
11        case MeasureSpec.EXACTLY:
12            result = specSize;
13            break;
14        }
15        return result;
16    }


UNSPECIFIED,一般是系统使用,不需要关心。这里view大小直接取size,就是getSuggestedMinimumWidth()/getSuggestedMinimumHeight(),意思是 建议的 最小宽高。看下实现:

1protected int getSuggestedMinimumWidth() {
2        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
3    }


没有背景,就取mMinWidth,就是xml中设置的minWidth属性值;有背景,取 mMinWidth 、背景的MinimumWidth 的较大值。drawable的getMinimumWidth()如下,有固有宽度就取固有宽度(如BitmapDrawable),没有就是0(如ShadeDrawable)。

1    public int getMinimumWidth() {
2        final int intrinsicWidth = getIntrinsicWidth();
3        return intrinsicWidth > 0 ? intrinsicWidth : 0;
4    }


AT_MOST、EXACTLY,直接取specSize,就是上一节最后的表格中的值,作为测量宽高。那这样取specSize是否合适呢?再来看一遍specSize的来源。


parentSpecMode
/childLayoutParams
EXACTLYAT_MOSTUNSPECIFIED
dp/px1EXACTLY
childSize
2EXACTLY
childsize
EXACTLY
childsize
match_parent3EXACTLY
parentSize
4AT_MOST
parentSize
UNSPECIFIED
0
wrap_content5AT_MOST
parentSize
6AT_MOST
parentSize
UNSPECIFIED
0


1、2的情况,具体dp值,取SpecSize没问题,因为是EXACTLY,就是给定的的尺寸。
3的情况,match_parent,取SpecSize,即parentSize,也没问题,因为是EXACTLY,也是确定的尺寸。
4的情况,match_parent,但父容器又是wrap_content,系统就给了AT_MOST+parentSize,限制最大尺寸为parentSize。而这里直接取specSize即parentSize,似乎也没问题。这个看一个例子一,如下,view是match_parent,可见view取得确实是parentSize。


在这里插入图片描述

5、6的情况,wrapContent即AT_MOST+parentSize,取specSize也就是parentSize,所以和3、4一样都是parentSize,即 View类 中 默认wrapContent等同于match_parent。

再看一个情况例子二,如下,View换成TextView(继承View),尺寸就不是parentSize了,而是内容尺寸,说明TextView在onMeasure中做了处理。


继续看,例子三如下,同时有TextView、View,此时textView又是取parentSize(可用空间):

所以得出结论:
通常直接继承View的自定义View,在onMeasure()需要处理 :
a、wrap_content的情况,否则wrap_content就等同于match_parent;
b、match_parent+父容器wrap_content的情况,否则就像例子一,父容器wrap_content是无效的,处理方式就是例子二中的textView。

总结就是,直接继承View的自定义View,需要处理AT_MOST时的宽高


处理方式如下:

1@Override
2    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
4        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
5        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
6        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
7        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
8
9        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
10            setMeasuredDimension(mWidth, mHeight);
11        } else if (widthMode == MeasureSpec.AT_MOST) {
12            setMeasuredDimension(mWidth, heightSize);
13        } else if (heightMode == MeasureSpec.AT_MOST) {
14            setMeasuredDimension(widthSize, mHeight);
15        }
16    }

实际就是在 AT_MOST时 设置一个指定的尺寸mWidth、mHeight,其他情况沿用系统。至于mWidth、mHeight是多少,则要具体看你的view的逻辑了。例如TextView,可以参考其源码的实现。


3.1.2 ViewGroup的测量过程

ViewGroup需要完成自身的测量,还要遍历子view调用measure()方法进行测量。

ViewGroup是抽象类,没有重写onMeasure,因为无法做到统一,是让具体继承ViewGroup的子类重写自己的逻辑。但是提供一些方便的方法给子类调用。如measureChildren()、measureChild()、measureChildWithMargins(),上面第二节分析过measureChildWithMargins(),这里我们看下measureChildren():

1/**
2     * Ask all of the children of this view to measure themselves, taking into
3     * account both the MeasureSpec requirements for this view and its padding.
4     * We skip children that are in the GONE state The heavy lifting is done in
5     * getChildMeasureSpec.
6     *
7     * @param widthMeasureSpec The width requirements for this view
8     * @param heightMeasureSpec The height requirements for this view
9     */
10    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
11        final int size = mChildrenCount;
12        final View[] children = mChildren;
13        for (int i = 0; i < size; ++i) {
14            final View child = children[i];
15            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
16                measureChild(child, widthMeasureSpec, heightMeasureSpec);
17            }
18        }
19    }


就是遍历子view,调用measureChild(),继续看:


1protected void measureChild(View child, int parentWidthMeasureSpec,
2            int parentHeightMeasureSpec) {
3        final LayoutParams lp = child.getLayoutParams();
4
5        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
6                mPaddingLeft + mPaddingRight, lp.width);
7        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
8                mPaddingTop + mPaddingBottom, lp.height);
9
10        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
11    }

通过getChildMeasureSpec()获取child的MeasureSpec,然后调用child.measure(),测量就传到child内部了,很好理解。measureChild()相比measureChildWithMargins() 没有考虑child的margin值。


上面说了,ViewGroup没有重写onMeasure,因为无法做到统一,让具体继承ViewGroup的子类重写自己的逻辑。具体看下LinearLayout的测量过程

1@Override
2    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3        if (mOrientation == VERTICAL) {
4            measureVertical(widthMeasureSpec, heightMeasureSpec);
5        } else {
6            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
7        }
8    }


继续看measureVertical():


1void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
2    ...
3    //下面这句官方注释:看每个人多高,也记住最大宽度。想想这不就是计算竖向LinearLayout宽高的思路嘛!
4    // See how tall everyone is. Also remember max width.
5        for (int i = 0; i < count; ++i) {
6            ...
7            final View child = getVirtualChildAt(i);
8            ...
9            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
10            ...
11                // Determine how big this child would like to be. If this or
12                // previous children have given a weight, then we allow it to
13                // use all available space (and we will shrink things later
14                // if needed).
15                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
16                //这里测量child(里面就是measureChildWithMargins())
17                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
18                        heightMeasureSpec, usedHeight);
19
20                final int childHeight = child.getMeasuredHeight();
21                ...
22                final int totalLength = mTotalLength;
23                //这里mTotalLength加上child的高度、margin,就是child高度累积。
24                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
25                       lp.bottomMargin + getNextLocationOffset(child));
26                ...
27                //这里记录最大宽度(包含margin)
28                final int margin = lp.leftMargin + lp.rightMargin;
29                final int measuredWidth = child.getMeasuredWidth() + margin;
30                maxWidth = Math.max(maxWidth, measuredWidth);
31            ...
32        }
33        //遍历完了:高度加上自身的上下padding
34        // Add in our padding
35        mTotalLength += mPaddingTop + mPaddingBottom;
36        int heightSize = mTotalLength;
37        // Check against our minimum height
38        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
39
40        //这里很重要:调用resolveSizeAndState--决定 计算的高度(高度累加)和 LinearLayout的父容器约束的高度,取哪一个。
41        // Reconcile our calculated size with the heightMeasureSpec
42        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
43        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
44    ...
45        //最大宽度加上左右margin
46        maxWidth += mPaddingLeft + mPaddingRight;
47
48        // Check against our minimum width
49        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
50
51        //设置最终的测量尺寸(宽也也同样调用resolveSizeAndState决定取哪个)
52        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
53                heightSizeAndState);
54}


所以,简单概括就是:
1.先测量所有child;
2.根据child的情况获取自身宽高(累加高度、最大宽度)。


那么,是否就取  累加高度、最大宽度?再看下resolveSizeAndState():


1/**
2     * Utility to reconcile a desired size and state, with constraints imposed
3     * by a MeasureSpec. Will take the desired size, unless a different size
4     * is imposed by the constraints. The returned value is a compound integer,
5     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
6     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
7     * resulting size is smaller than the size the view wants to be.
8     *
9     * @param size How big the view wants to be. --想要的尺寸
10     * @param measureSpec Constraints imposed by the parent. --父布局给的measureSpec
11     * @param childMeasuredState Size information bit mask for the view's
12     *                           children.
13     * @return Size information bit mask as defined by
14     *         {@link #MEASURED_SIZE_MASK} and
15     *         {@link #MEASURED_STATE_TOO_SMALL}.
16     */
17    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
18        final int specMode = MeasureSpec.getMode(measureSpec);
19        final int specSize = MeasureSpec.getSize(measureSpec);
20        final int result;
21        switch (specMode) {
22            case MeasureSpec.AT_MOST:
23                //AT_MOST时,想要的尺寸大于约束的尺寸,就只能取 约束的尺寸。
24                if (specSize < size) {
25                    result = specSize | MEASURED_STATE_TOO_SMALL;
26                } else {
27                    result = size;
28                }
29                break;
30            case MeasureSpec.EXACTLY:
31                //dp值、match_parent且父EXACTLY,就是SpecSize
32                result = specSize;
33                break;
34            case MeasureSpec.UNSPECIFIED:
35            default:
36                result = size;
37        }
38        return result | (childMeasuredState & MEASURED_STATE_MASK);
39    }


这个过程就是 限制AT_MOST时,即wrap_content(或match_parent且父wrap_content)时高度不能大于parent的剩余空间。


3.1.3 获取View宽高的时机

Measure过程完成,就可通过getMeasuredWidth()、getMeasuredHeight()获取测量宽高。但某些极端情况
需要多次Measure才能确定最终宽高。所以在onLayout方法中获取测量宽高是真正ok的。
我们知道,activity的onCreate中无法获取到view的宽高。实际onCreate、onStart、onResume都不能保证view已完成测量,所以可能获取的都是0。因为view的measure和activity生命周期不是同步的。

以下是保证可以获取view测量宽高的方法


1、Activity/View # onWindowFocusChanged

onWindowFocusChanged:View已初始化完毕,宽高已准备ok。但会多次调用,获取焦点、失去焦点都回调用。(这个回调是ViewRootIml中分发到DecorView,接着到Activity、到各级View。)

1@Override
2    public void onWindowFocusChanged(boolean hasFocus) {
3        super.onWindowFocusChanged(hasFocus);
4        if (hasFocus) {
5            int measuredWidth = scoreView.getMeasuredWidth();
6            int measuredHeight = scoreView.getMeasuredHeight();
7        }
8    }


2、view.post(runnable)

view.post可以把runnable放入消息队列,等待looper到此runnable是view已经初始化完成。v详细原理参考【Android源码解析】View.post()到底干了啥

1@Override
2    protected void onStart() {
3        super.onStart();
4        scoreView.post(new Runnable() {
5            @Override
6            public void run() {
7                int measuredWidth = scoreView.getMeasuredWidth();
8                int measuredHeight = scoreView.getMeasuredHeight();
9            }
10        });
11    }


3、ViewTreeObserver

ViewTreeObserver有很多回调,其中有个OnGlobalLayoutListener,当View树的状态发生改变或者View树内部view的可见性发生改变时 方法 onGlobalLayout()都会被调用。所以是会回调多次。此时也可以获取view的宽高:

1ViewTreeObserver observer = view.getViewTreeObserver();
2        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
3            @Override
4            public void onGlobalLayout() {
5                mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
6                if (mIsGroupListAnimating) {
7                    mIsGroupListAnimationPending = true;
8                } else {
9                    updateLayoutHeightInternal(animate);
10                }
11            }
12        });



3.2Layout过程


layout()的作用是View用来确定view本身位置,内部调用onLayout()来确定子view的位置。layout过程比measure过程简单很多。看View的layout方法:

1    public void layout(int l, int t, int r, int b) {
2        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
3            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
4            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
5        }
6
7        int oldL = mLeft;
8        int oldT = mTop;
9        int oldB = mBottom;
10        int oldR = mRight;
11        //使用setFrame方法设置4个顶点,就确定位置了~
12        boolean changed = isLayoutModeOptical(mParent) ?
13                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
14
15        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
16            //这里调用onLayout,是个空实现。ViewGroup中重写了,还是空实现,但加了abstract,即ViewGroup的子类必须重写onLayout确定子View的位置。
17            onLayout(changed, l, t, r, b);
18
19            if (shouldDrawRoundScrollbar()) {
20                if(mRoundScrollbarRenderer == null) {
21                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
22                }
23            } else {
24                mRoundScrollbarRenderer = null;
25            }
26
27            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
28
29            ListenerInfo li = mListenerInfo;
30            if (li != null && li.mOnLayoutChangeListeners != null) {
31                ArrayList<OnLayoutChangeListener> listenersCopy =
32                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
33                int numListeners = listenersCopy.size();
34                for (int i = 0; i < numListeners; ++i) {
35                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
36                }
37            }
38        }
39
40        ...
41    }

先是用setFrame方法设置4个顶点,就确定位置了,即mLeft、mTop、mBottom、mRight确定了。 然后调用onLayout,是个空实现。ViewGroup中重写了onLayout,还是空实现,但加了abstract,即ViewGroup的子类必须重写onLayout确定子View的位置


那就看看LinearLayout的onLayout


1@Override
2    protected void onLayout(boolean changed, int l, int t, int r, int b) {
3        if (mOrientation == VERTICAL) {
4            layoutVertical(l, t, r, b);
5        } else {
6            layoutHorizontal(l, t, r, b);
7        }
8    }

继续看layoutVertical():

1void layoutVertical(int left, int top, int right, int bottom) {
2        final int paddingLeft = mPaddingLeft;
3
4        int childTop;
5        int childLeft;
6
7        // Where right end of child should go
8        final int width = right - left;
9        int childRight = width - mPaddingRight;
10
11        // Space available for child
12        int childSpace = width - paddingLeft - mPaddingRight;
13
14        final int count = getVirtualChildCount();
15
16        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
17        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
18
19        switch (majorGravity) {
20           case Gravity.BOTTOM:
21               // mTotalLength contains the padding already
22               childTop = mPaddingTop + bottom - top - mTotalLength;
23               break;
24
25               // mTotalLength contains the padding already
26           case Gravity.CENTER_VERTICAL:
27               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
28               break;
29
30           case Gravity.TOP:
31           default:
32               childTop = mPaddingTop;
33               break;
34        }
35        //遍历子view
36        for (int i = 0; i < count; i++) {
37            final View child = getVirtualChildAt(i);
38            if (child == null) {
39                childTop += measureNullChild(i);
40            } else if (child.getVisibility() != GONE) {
41                //获取child的测量宽高
42                final int childWidth = child.getMeasuredWidth();
43                final int childHeight = child.getMeasuredHeight();
44
45                final LinearLayout.LayoutParams lp =
46                        (LinearLayout.LayoutParams) child.getLayoutParams();
47
48                int gravity = lp.gravity;
49                if (gravity < 0) {
50                    gravity = minorGravity;
51                }
52                final int layoutDirection = getLayoutDirection();
53                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
54                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
55                    case Gravity.CENTER_HORIZONTAL:
56                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
57                                + lp.leftMargin - lp.rightMargin;
58                        break;
59
60                    case Gravity.RIGHT:
61                        childLeft = childRight - childWidth - lp.rightMargin;
62                        break;
63
64                    case Gravity.LEFT:
65                    default:
66                        childLeft = paddingLeft + lp.leftMargin;
67                        break;
68                }
69
70                if (hasDividerBeforeChildAt(i)) {
71                    childTop += mDividerHeight;
72                }
73
74                childTop += lp.topMargin;
75                //以上就是获取子view的左、上的位置,即宽高,然后调用setChildFrame
76                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
77                        childWidth, childHeight);
78                 //top位置加上高度和margin,就是下一个view的top
79                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
80
81                i += getChildrenSkipCount(child, i);
82            }
83        }
84    }


就是遍历子view,确认childLeft、childTop,调用setChildFrame确认子view的位置:

1private void setChildFrame(View child, int left, int top, int width, int height) {
2        //这里width、height就是 上面获取的 测量宽高
3        child.layout(left, top, left + width, top + height);
4    }


也就是调用child的layout方法,这样就走child的layout过程了。

一个问题:getMeasuredWidth() 与 getWidth()有何区别?
答曰:==一般情况,getMeasuredWidth() 与 getWidth()两者无区别==。
先看,getWidth():

1public final int getWidth() {
2        return mRight - mLeft;
3    }


在上面分析LinearLayout时,child.layout的参数中 mRight就是mLeft + measuredWidth,所以getWidth()就是measuredWidth。只不过是measuredWidth在测量过程产生,getWidth()在layout过程产生。只要不重写view的layout()方法(也不需要重写)改变顶点位置就不会出现不同的情况,例如下面这个最终宽高比测量宽高大100。

1    public void layout(int l, int t, int r, int b) {
2        super.layout(l,t,r+100,b+100);
3    }



3.3Draw过程

draw过程:

  1. 画背景

  2. 画自己-- onDraw,自己实现

  3. 画子view-- dispatchDraw

  4. 画装饰

1public void draw(Canvas canvas) {
2        final int privateFlags = mPrivateFlags;
3        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
4                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
5        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
6
7        /*
8         * Draw traversal performs several drawing steps which must be executed
9         * in the appropriate order:
10         *
11         *      1. Draw the background
12         *      2. If necessary, save the canvas' layers to prepare for fading
13         *      3. Draw view's content
14         *      4. Draw children
15         *      5. If necessary, draw the fading edges and restore layers
16         *      6. Draw decorations (scrollbars for instance)
17         */
18
19        // Step 1, draw the background, if needed
20        int saveCount;
21
22        if (!dirtyOpaque) {
23            drawBackground(canvas);
24        }
25
26        // skip step 2 & 5 if possible (common case)
27        final int viewFlags = mViewFlags;
28        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
29        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
30        if (!verticalEdges && !horizontalEdges) {
31            // Step 3, draw the content
32            if (!dirtyOpaque) onDraw(canvas);
33
34            // Step 4, draw the children
35            dispatchDraw(canvas);
36
37            drawAutofilledHighlight(canvas);
38
39            // Overlay is part of the content and draws beneath Foreground
40            if (mOverlay != null && !mOverlay.isEmpty()) {
41                mOverlay.getOverlayView().dispatchDraw(canvas);
42            }
43
44            // Step 6, draw decorations (foreground, scrollbars)
45            onDrawForeground(canvas);
46
47            // Step 7, draw the default focus highlight
48            drawDefaultFocusHighlight(canvas);
49
50            if (debugDraw()) {
51                debugDrawFocus(canvas);
52            }
53
54            // we're done...
55            return;
56        }

ViewGroup一般不用onDraw画自己,只需要画子View就可以了。但明确需要画自己的话,需要调用setViewNotDraw(false);

以上View的三大流程就分析完了。


4、自定义View

自定义view涉及view层次结构、事件分发、工作原理,有一定复杂度,但也是有章可循的。

4.1自定义view的分类

  1. 继承View:重写onDraw,要处理wrap_content、padding。

  2. 继承ViewGroup:重写onMeasure测量自己、子View,重写onLayout布局子View。

  3. 继承特定View(如TextView):扩展自己的功能。

  4. 继承特定ViewGroup(如LinearLayout):扩展自己的功能。


4.2 自定义view 注意点

  1. 支持wrap_content:直接继承View或ViewGroup的,要在onMeasure中处理wrap_content的情况。

  2. 支持padding:直接继承View在onDraw中处理;直接继承ViewGroup,在onMeasure、onLayout中处理padding和子view的margin。

  3. 不要在View中使用handler,因为本身提供了post方法。

  4. 在View#onDetachedFromWindow中停止动画或线程。

  5. 处理好嵌套滑动。


4.3 例子

自定义ViewGroup实例:

横向滑动HorizontalView

https://blog.csdn.net/hfy8971613/article/details/81589829


4.4 自定义view的思想

先掌握基本功,弹性滑动、滑动冲突、绘制原理等,然后选择自定义的类别,按照注意事项多做就可以了。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存