View 体系详解:View 的工作流程

View 树体系 简单来说,View 树体系是一个由View和ViewGroup对象组成的、具有层次关系的树形结构。它定义了整个用户界面的结构和组件关系。View:是所有 UI 组件的基类,是叶子节点。例如一个按钮 (Button)、一段文本 (TextView)、一张图片 (ImageView它们负责绘制自己并响应点击等事件。ViewGroup:是View的子类,是容器,是分支节点。它可以包含多个View或其他ViewGroup。例如线性布局 ()、相对布局 ()、帧布局 () 等。它们负责测量和定位其子视图。把。 阅读详情

1、View 树的加载流程

当我们调用 startActivity() 方法的时候,会调用到 ActivityThread 中的 performLaunchActivity() 获取一个 Activity 实例, 并在 Instrumentation 的 callActivityOnCreate() 方法中调用 Activity 的 onCreate() 完成 DecorView 的创建。这样我们就获取了一个 Activity 的实例,然后我们调用 handleResumeActivity() 来回调 Activity 的 onResume():

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        // ....
        WindowManagerGlobal.initialize();
        // 创建 Activity 的实例,在这里完成对 Activity 的 onCreate() 方法的回调
        Activity a = performLaunchActivity(r, customIntent);
        if (a != null) {
            // ...
            // 在这里回调 Activity 的 onResume() 方法
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
            if (!r.activity.mFinished && r.startsNotResumed) {
                // 在这里完成对 Activity 的 onPause() 方法的回调
                performPauseActivityIfNeeded(r, reason);
                // ...
            }
        }
        // ...
    }

然后,在 handleResumeActivity() 方法中的 performResumeActivity() 会回调 Activity 的 onResume() 方法。在该方法中,我们会从 Window 中获取之前添加进去的 DecorView,然后将其添加到 WindowManager 中:

    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        // 在这里会回调 Activity 的 onResume()
        r = performResumeActivity(token, clearHide, reason);
        if (r != null) {
            final Activity a = r.activity;
            // ...
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                // 在这里获取 DecorView
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                // 获取 WindowManager 实例,实际是 WindowManagerImpl 
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Activity 被重建,复用 DecorView,通知子元素
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        // 将 DecorView 添加到 WindowManager 中
                        wm.addView(decor, l);
                    } else {
                        a.onWindowAttributesChanged(l);
                    }
                }
            }
        }
    }

这里的 WindowManager 是 WindowManagerImpl 的实例,而调用它的 addView() 方法的时候会使用 WindowManagerGlobal 的 addView() 方法。在该方法中会 new 出来一个 ViewRootImpl,然后调用它的 setView() 把传进来的 DecorView 添加到 Window 里。同时,会调用 requestLayout() 方法进行布局,然后,并最终调用 performTraversals() 完成对整个 View 树进行遍历:

    private void performTraversals() {
        // ...
        if (!mStopped || mReportNextDraw) {
            // ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
        // ...
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
            // ...
        }
        // ...
        if (!cancelDraw && !newSurface) {
            // ...
            performDraw();
        }
    }

在该方法中会调用 performMeasure()、performLayout() 和 performDraw() 三个方法,它们分别会调用 DecorView 的 measure()、layout() 和 draw() 完成对整个 View 树的测量、布局和绘制,一个界面也就呈现给用户了。如果您做过自定义 View 的话,那么您对 onMeasure()、onLayout() 和 onDraw()三个方法一定不会陌生,前面的三个方法与后面的三个方法之间的关系就是:后面的三个方法会被前面的三个方法调用,本质上就是提供给用户用来自定义的方法。下面我们就看下这三个方法究竟各自做了什么操作,当然,我们尽可能从自定义控件的角度来分析,因为这对一个开发者可能帮助更大。

2、measure()

View 的大小不仅由自身所决定,同时也会受到父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。在上面我们提到了 measure() 方法,它是用来测量 View 的大小的,但实际上测量的主要工作是交给 onMeasure() 方法的。在 View 中,onMeasure() 是一个 protected 的方法,显然它设计的目的就是:提供给子 View 按照父容器提供的限制条件,控制自身的大小,实现自己大小的测量逻辑。所以,当我们自定义一个控件的时候,只会去覆写 onMeasure() 而不去覆写 measure() 方法。

在 Android 中,我们的控件分成 View 和 ViewGroup 两种类型。根据上面的分析,对 View 的测量,我们可以得出如下结论:在 Android 中,ViewGroup 会根据其自身的布局特点,把限制条件封装成 widthMeasureSpec 和 heightMeasureSpec 两个参数传递给子元素;然后,在子元素中根据这两个参数来调整自身的大小。所以,ViewGroup 的 measure() 方法会根据其布局特性的不同而不同;而 View 的 measure(),不论其父容器是哪种类型,只根据 widthMeasureSpec 和 heightMeasureSpec 决定。

下面我们来看一下 onMeasure() 在 View 和 ViewGroup 中的不同表现形式。

2.1 View 的 onMeasure()

下面是 View 类中的 onMeasure() 方法。这是一个默认的实现,调用了 setMeasuredDimension() 方法来存储测量之后的宽度和高度。当我们自定义 View 的时候,也需要调用 setMeasuredDimension() 方法把最终的测量结果存储起来:

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

显然,我们的测量依据就是 widthMeasureSpec 和 heightMeasureSpec 两个参数。它们是整型的、32位变量,包含了测量模式和测量数值的信息(按位存储到整型变量上,包装成整型的目的是为了节约存储空间)。一般我们会像下面这样来分别获取高度和宽度的测量模式和测量数值(实际就是按位截取):

    int widthsize = MeasureSpec.getSize(widthMeasureSpec);      // 测量数值
    int widthmode = MeasureSpec.getMode(widthMeasureSpec);      // 测量模式    
    int heightsize = MeasureSpec.getSize(heightMeasureSpec);    // 测量数值
    int heightmode = MeasureSpec.getMode(heightMeasureSpec);    // 测量模式

测量模式共有 MeasureSpec.UNSPECIFIED、MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 三种,分别对应二进制数值 00、01 和 10,它们各自的含义如下:

  1. UNSPECIFIED:默认值,父控件没有给子 View 任何限制,子 View 可以设置为任意大小;
  2. EXACTLY:表示父控件已经确切的指定了子 View 的大小;
  3. AT_MOST:表示子 View 具体大小没有尺寸限制,但是存在上限,上限一般为父 View 大小。

这里,我不打算详细介绍 View 中默认测量逻辑的具体实现。它的大致逻辑是这样的:首先我们会用 getDefaultSize() 获取默认的宽度或者高度,这个方法接收两个参数,一个是默认的尺寸,一个测量模式。如果父控件没有给它任何限制,它就使用默认的尺寸,否则使用测量数值。这里的默认的尺寸通过 getSuggestedMinimumHeight()/getSuggestedMinimumWidth() 方法得到,它会根据背景图片高度/宽度和 mMinHeight/mMinWidth 的值,取一个最大的值作为控件的高度/宽度。

所以,View 的默认的测量逻辑的实际效果是:首先 View 的大小受父容器的影响,如果父容器没有给它限制的话,它会取背景图片和最小的高度或者宽度中取一个最大的值作为自己的大小。

2.2 ViewGroup 的 onMeasure()

2.2.1 ViewGroup 中的方法

由于 ViewGroup 本身没有布局的特点,所以它没有覆写 onMeasure()。有自身布局特点的,比如 LinearLayout 和 RelativeLayout 等都覆写并实现了这个方法。尽管如此,ViewGroup 提供了一些方法帮助我们进行测量,首先是 measureChildren() 方法:

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

这里的逻辑比较简单,就是对子元素进行遍历并判断如果指定的 View 是否位 GONE 的状态,如果不是就调用 measureChild() 方法:

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

该方法也比较容易理解,就是将子元素的布局参数 LayoutParams 取出,获取它的宽度和高度之后,将所有信息传递给 getChildMeasureSpec()。这样就得到了用于子元素布局的 childWidthMeasureSpec 和 childHeightMeasureSpec 参数。然后,再调用子元素的 measure() 方法,从而依次完成对整个 View 树的遍历。下面我们看下 getChildMeasureSpec() 方法做了什么操作:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 首先从 spec 中取出父控件的测量模式和测量数值
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        // 这里需要保证 size 不能为负数,也就是预留给子元素的最大空间,由父元素的测量数值减去填充得到
        int size = Math.max(0, specSize - padding);
        // 用于返回的值
        int resultSize = 0;
        int resultMode = 0;
        // 根据父空间的测量模式
        switch (specMode) {
            // 父控件的大小是固定的
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    // 子 View 指定了大小
                    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) {
                    // 子 View 指定了大小
                    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) {
                    // 子控件希望和父控件一样大,但是父控件多大都不确定;系统23以下返回true,以上返回size
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
        }
        // 返回一个封装好的测量结果,就是把测量数值和测量模式封装成一个32位的整数
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面我们已经为这段代码作了非常详细的注释。只需要注意,这里在获取子元素的测量结果的时候是基于父控件的测量结果来的,需要根据父元素的测量模式和测量数值结合自身的布局特点分成上面九种情况。或者可以按照下面的写法将其划分成下面几种情况:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec), specSize = MeasureSpec.getSize(spec);
        int size = Math.max(0, specSize - padding);
        int resultSize = 0, resultMode = 0;
        if (childDimension >= 0) {
            // 子元素指定了具体的大小,就用子元素的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
            // 子元素希望和父控件一样大,需要设置其上限,然后测量模式与父控件一致即可
            if (specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST) {
                resultSize = size;
                resultMode = specMode;
            } else if (specMode == MeasureSpec.UNSPECIFIED) {
                // API23一下就是0,父控件没有指定大小的时候,子控件只能是0;以上是size
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
        } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
            // 子元素希望自己决定大小,设置其大小的上限是父控件的大小即可
            if (specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (specMode == MeasureSpec.UNSPECIFIED) {
                // API23一下就是0,父控件没有指定大小的时候,子控件只能是0;以上是size
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这两种方式只是划分的角度不一样,后面的这种方法是从子元素的布局参数上面来考虑的。另外,这里有个 sUseZeroUnspecifiedMeasureSpec 布尔参数需要提及一下,会根据系统的版本来进行赋值:

    sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;

也就是当系统是 API23 以下的时候的为 true. 加入这个参数的原因是,API23 之后,当父控件的测量模式是 UNSPECIFIED 的时候,子元素可以给父控件提供一个可能的大小。下面是注释的原话 ?

    // In M and newer, our widgets can pass a "hint" value in the size
    // for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
    // know what the expected parent size is going to be, so e.g. list items can size
    // themselves at 1/3 the size of their container. It breaks older apps though,
    // specifically apps that use some popular open source libraries.
2.2.2 LinearLayout 的 onMeasure()

上面我们分析的是 ViewGroup 中提供的一些方法,下面我们以 LinearLayout 为例,看一下一个标准的容器类型的控件是如何实现其测量的逻辑的。

下面是其 onMeasure() 方法,显然在进行测量的时候会根据其布局的方向分别实现测量的逻辑:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

然后,我们以 measureVertical() 为例,来看一下 LinearLayout 在垂直方向上面是如何进行测量的。这段代码比较长,我们只截取其中的一部分来进行分析:

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        // ...
        // 获取LinearLayout的测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // ...
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
        // ...
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final float childWeight = lp.weight;
                if (childWeight > 0) {
                    // ...
                    // 获取一个测量的数值和测量模式
                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width);
                    // 调用子元素进行测量
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }
                
                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                // 将宽度增加到 mTotalLength 上
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
            mTotalLength += mPaddingTop + mPaddingBottom;
        // ...
        maxWidth += mPaddingLeft + mPaddingRight;
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        // 最终确定测量的大小
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
        // ...
    }

上面是 LinearLayout 在垂直方向上面的测量的过程,在测量的时候会根据子元素的布局将子元素的测量高度添加到 mTotalLength 上,然后再加上填充的大小,作为最终的测量结果。

3、layout()

layout() 用于确定控件的位置,它提供了 onLayout() 来交给字类实现,同样我们在自定义控件的时候只要实现 onLayout() 方法即可。在我们自定义 View 的时候,如果定义的是非 ViewGroup 类型的控件,一般是不需要覆写 onLayout() 方法的。

下面我们先看一下 layout() 方法在 View 中的实现:

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

        // ...
    }

这里会调用 setFrame() 方法,它的主要作用是根据新的布局参数和老的布局参数做一个对比,以判断控件的大小是否发生了变化,如果变化了的话就调用 invalidate() 方法并传入参数 true,以表明绘图的缓存也发生了变化。这里就不给出这个方法的具体实现了。然后注意到,在 layout() 方法中会回调 onLayout() 方法来完成各个控件的位置的确定。

对于 ViewGroup,它重写了 layout() 并在其中调用了 View 中的 layout() 方法,不过整体并没有做太多的逻辑。与测量过程类似,ViewGroup 并没有实现 onLayout 方法。同样,对于 ViewGroup 类型的控件,我们还是以 LinearLayout 为例说明一下 onLayout() 的实现逻辑:

与测量过程类似,LinearLayout 在 layout 的时候也根据布局的方向分成两种情形:

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

这里我们仍以垂直方向的方法为例。与测量的过程相比,layout 的过程的显得简单、清晰得多:

    void layoutVertical(int left, int top, int right, int bottom) {
        // ...
        // 根据控件的 gravity 特点得到顶部的位置
        switch (majorGravity) {
           case Gravity.BOTTOM:
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
           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;
                // 本质上调用子控件的 layout() 方法
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
                i += getChildrenSkipCount(child, i);
            }
        }
    }

因为布局方向是垂直方向的,所以在对子元素进行遍历之前,先对自身的顶部的位置进行计算,然后再依次遍历子元素,并对顶部的高度不断叠加,最后调用 setChildFrame() 方法:

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

这样就完成了对整个 View 树的 layout() 方法的调用。

4、draw()

View 的 draw() 方法实现的逻辑也很清晰。在绘制的过程会按照如下的步骤进行:

  1. 绘制背景
  2. 保存 canvas
  3. 绘制自身的内容
  4. 绘制子控件
  5. 绘制 View 的褪色边缘,比如阴影效果之类的
  6. 绘制装饰,比如滚动条之类的

View 中提供了 onDraw() 方法用来完成对自身的内容的绘制,所以,我们自定义 View 的时候只要重写这个方法就可以了。当我们要自定义一个 ViewGroup 类型的控件的时候,一般是不需要重写 onDraw() 方法的,因为它只需要遍历子控件并依次调用它们的 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;

        // Step 1, draw the background, if needed
        int saveCount;

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

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }

        // ...
    }

注意到在上面的方法中会调用 dispatchDraw(canvas) 方法来分发绘制事件给子控件来完成整个 View 树的绘制。在 View 中,这是一个空的方法,ViewGroup 覆写了这个方法,并在其中调用 drawChild() 来完成对指定的 View 的 draw() 方法的调用:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

而对于 LinearLayout 这样本身没有绘制需求的控件,没有覆写 onDraw() 和 dispatchDraw(canvas) 等方法,因为 View 和 ViewGroup 中提供的功能已经足够使用。

总结:

上文中,我们介绍了在 Android 系统中整个 View 树的工作的流程,从 DecorView 被加载到窗口中,到测量、布局和绘制三个方法的实现。本质上整个工作的流程就是对 View 树的一个深度优先的遍历过程。

View|体系详解 View体系 一、学习脑图 二、View基础 2.1 什么是View? Q1:怎么理解View? View是界面层的控件的一种抽象,代表了一个控件。 是android在视觉上的呈现。 是所有控件是基类,可以是单个控件View可以是一组控件ViewGroup。 Q2:View的重要性? View在Android中是一个十分重要的概念,虽然说View不属于四大组件,但是它的作用堪比四大组件,在开发中,Activity承担了可视化的功能,Android提供了很多基础的控件,当我们不满足于这些基础控件 阅读详情

相关推荐

Android触摸滑动全解(三)——View坐标体系详解

Android触摸滑动全解(三)——View坐标体系详解 当我们触摸屏幕上的View时,有时候想要获取此时View的一些属性状态,比如说在屏幕的坐标,或者相对于父布局的坐标,或者View的宽高等,但是由于View有很多属性,我们很苦恼不知道应该去选择哪个方法去调用,今天,我们就梳理一下View的坐标体系。 一、屏幕区域划分 Android系统的屏幕区域划分如图: 获取上述区域宽...

自由奔跑的风 1370

View|工作流程

前言 一、基础知识 在学习View的工作原理之前,需要先学习一些基本的概念。 2.1 ViewRoot Q1: ViewRoot是什么? 对应ViewRootImpl类。 连接WindowManagerService和DecorView的纽带。 Q2:ViewRoot在View绘制中有什么作用? View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。 注意:ViewRoot并不属于View树的一份子。从源码实现上来看,它既非Vi

Yuki-r的博客 1261

View体系

View视图坐标与方法 简介 Android View体系是界面编程的核心。 View是Android所有控件的基类,同时ViewGroup也是继承自View。 坐标系 Android坐标系 在Android中,将屏幕的左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,原点向下是Y轴正方向。getRawX() ,getRawY() 视图坐标系 getHeight():获取V...

JxinY的博客 299

requestLayout()的执行流程

每次更新View都会想到requestLayout()方法,所以想看看它的流程是怎样的。我一直觉得,先理清楚步骤再去看源码会好很多,所以先给出requestLayout(...

Android技术之家 5692

View 的 requestLayout 发起的重绘流程源码分析(Android Q)

View 的 requestLayout 发起的重绘流程源码分析(Android Q) 我们平时经常会调用 View 类的 requestLayout 方法更新视图,那么它又是如何发起的重绘逻辑呢? View 的 requestLayout 方法: public void requestLayout() { …… if (mParent != null && !mParent.isLayoutRequested()) { //

卜大爷的博客 2万+

从噪声中提取真相:LabVIEW振动信号积分处理的降噪艺术

本文深入探讨了LabVIEW在振动信号积分处理中的降噪技术,揭示了如何通过选择性频率加权从噪声中提取关键故障特征。详细介绍了时域和频域积分方法的实现原理及优化策略,并结合工业案例展示了多轴信号联合降噪的实际应用效果,为设备状态监测提供了高效解决方案。

weixin_29164185的博客 304

View的工作流程1

当然,这个时候界面仍然不会显示出什么,因为View的工作流程还没有执行完,还需要经过measure,layout以及draw才会把view绘制出来。这里调用performLaunchActivity方法来创建Activity,在这个方法里会调用Activity的onCreate方法,从而完成DecorView的创建。在这个方法里,performeResumeActivity会有调用,而在performResumeActivity里,会调用Activity的onResume方法。View的工作流程入口。

Crazy Android 316

树莓派4B+Ubuntu22.04+思岚RPLIDAR C1激光雷达应用

树莓派+Ubuntu22.04+思岚RPLIDAR C1激光雷达简单应用

techfuture的博客 3705

调用View#requestLayout后,哪些View会被影响?

最小化效果: ①发起`requestLayout()`调用的View,及其`各级parent(直到ViewRootImpl)`,它们的`requestLayout方法`都会被调用,都会添加`PFLAG_FORCE_LAYOUT`和`PFLAG_INVALIDATED`标记。 ②下一个刷新时机中,`添加了标记的View`会`从上到下`依次执行`onMeasure`、`onLayout`方法。

tinyVampire的博客 1047

基于知识图谱的旅游推荐系统

这一系统整合了当今先进的数据技术:采集携程等大型平台的景点数据,通过爬虫技术源源不断地获取最新的旅游信息。此外,我们采用Neo4j图数据库来存储和处理这些数据,结合自然语言处理技术,为你提供精准的旅游建议。:通过直观的知识图谱展示,了解各个景点之间微妙的联系,无论是文化背景还是地理方位,你都能从中发现新的旅行灵感。准备毕业设计的同学,关注我们,这是一个集成了多种前沿技术的项目,为你的学术成果锦上添花。:不再浪费时间,系统根据你的偏好和历史评分,利用协同过滤技术推荐适合的旅游目的地。

qalangtao的博客-qalangtao.com 774

TP4056锂电池充电电路设计原理与工程实践

锂离子电池充电管理是嵌入式电源系统的核心基础技术,其本质是通过恒流恒压(CC/CV)控制策略,匹配电池电化学特性以实现安全、高效、长寿命充放电。TP4056作为典型线性充电IC,以精简外围实现预充、恒流、恒压及自动截止全周期管理,但受限于线性拓扑的热耗散特性,必须协同散热设计、电阻精度选型与PCB布局进行系统级优化。该方案广泛应用于STM32智能小车、便携传感器节点等3.7V单节锂电供电场景,兼顾成本、可靠性与工程可实施性,是入门嵌入式硬件电源设计的关键实践范例。

weixin_36064575的博客 224

Android View体系

View的工作流程,指的就是measure、layout和draw其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View。

xfb的博客 402

Robot Arm 机械臂源码解析

由于我们用的是步进电机,运动的时候是一步一步的,并不是平滑的,这里假设在一个平面上面,一个x步进电机,一个y步进电机。那么放大它们所走的轨迹就会是阶梯状的。而它们也只能沿着网格运动。相当于有了一个最小的分辨率,这个分辨率就是一步。如下图所示,如果x,y电机同时走就能走出45度的斜线。在三维空间里面运动也是如此,你应该可以想象的出来;首先,是updateActualPosition,用来时时更新此时的差补位置,得到的是一个路径与时间t的函数(其中路径的范围为0,1,时间范围为0,dist/v)...

爱吃炸鸡的小猪's blog 1万+

【安卓小叙】详论View体系(二)

这一章,详细简述了事件分发机制、view工作流程(measure、layout、draw)图文并茂,从宏观角度了解执行过程。

Frms的博客 453

总目录- AutoSAR从入门到精通实战系列课程【持续更新中...】

欢迎大家订阅《AutoSAR入门和实战系列》,开始前博主先列出AutoSAR学习的大纲,同时这也可以作为大家学习AutoSAR的参考。想学习和及以及的同学们,可以订阅本博客,本博客一直持续更新中。如果想深入或从本质上学习和理解AutoSAR,打开此链接后,,点击左下方【立即订阅】订阅专栏!!!】......

4万+

100 道大数据开发技术原理与实战面试宝典(必过答案详细解析)I

在数字化浪潮席卷全球的今天,大数据开发工程师已成为企业数字化转型的核心力量。无论是互联网巨头的用户行为分析,还是金融机构的风险控制,亦或是智慧城市的数据中枢,都离不开大数据技术的支撑。然而,面对BAT等头部企业严格的面试要求,候选人不仅需要掌握扎实的理论基础,更需具备将技术落地的实战能力。本文基于历年高频面试题与企业真实场景需求,梳理100道必考题目,涵盖基础概念、框架原理、算法设计、性能优化等核心领域,助你构建完整的知识体系,从容应对面试挑战。

AI天才研究院 1388

View体系和自定义View:

需要注意的是,凡事都要有度,自定义View毕竟不是规范的控件,如果设计不好、不考虑性能,则反而会适得其反;另外,其适配起来可能也会产生问题。如果能用系统控件的情况还是应尽量用系统控件。自定义View分为三大类,第一种是自定义View,第二种是自定义ViewGroup,第三种是自定义组合控件。其中,自定义View又分为继承系统控件(比如 TextView)和继承View两种情况。

weixin_74399377的博客 1020

Windows Server 2016远程桌面服务配置和授权激活

Server 2016默认远程桌面连接数是2个用户,如果多余两个用户进行远程桌面连接时,系统就会提示超过连接数,可以通过添加远程桌面授权解决:1、添加远程桌面授权服务第一步:服务器管理 - 添加角色和功能打开添加角色和功能向导窗口,选择基于角色或给予功能安装:第二步:添加远程桌面会话主机和远程桌面授权功能:以上配置完成后即可使用多于2用户同时登陆,但使用期限为120天,再次登陆会有如下提示:2、添...

mahailiang的专栏 6万+

View详解之二View的事件体系

这周接着上次的学习,继续来学习View相关知识,View的事件体系。首先,掌握几个知识点。View的位置参数先上图,左图中left 、top、right、bottom分别是四个顶点相对于父容器的值,右图中当view反生平移时,view的top值是不会改变的x、y、transitionX、transitionY会反生改变,并且相对于父容器而言。y = top + transitionY;x= left

抬头看世界 795

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

View绘制体系(二)——View的inflate详解

View绘制体系(二)——View的inflate详解 前言 上一篇博客讲到setContentView最后会调用mLayoutInflater.inflate来创建了自定义xml中的布局视图,添加到mContentParent中,这里我们就来学习下inflate的具体实现以及它的基本使用方法。 inflate的基本使用 首先我们需要明确的是,inflate方法是讲xml文件反射成一个View...

boyeleven的博客 4071

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

【Android】View的工作流程——measure

看到这里你对Activity的构成有一定的了解,每个Activity都有一个与之关联的Window对象,而DecorView是这个Window的根视图。当DecorView被创建以及加载资源的时候,此时它的内容还无法进行显示,因为还没有加载到Window当中。接下来就看看是如何加载的。DecorViewWindow当我们调用Activity的方法的时候,最终调用的是的方法来创建上面的方法是用来创建Activity,里面会调用Activity的onCreate我们看到会调用方法最终调用到了Activity的。

2301_79828182的博客 1605

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

android activity怎么调用非activity类_Android视图体系—View的工作流程

Android中View体系概览在Android应用中所有的用户界面元素都是由View和ViewGroup对象构建而成。View对象用于绘制屏幕中可以和用户交互的内容。ViewGroup用于存储其他View对象和ViewGroup对象,从而构成界面的布局。就如图中所示,用户界面的布局就是通过View和ViewGroup的结构层次定义的。View和ViewGroup组成了用户界面的View树,在Vi...

weixin_42538752的博客 203

【Android】View的工作流程

measure流程分为View的measure流程和ViewGroup的measure流程。其中,ViewGroup的measure流程除完成自身测量外,还需遍历调用子元素的measure()方法。

2303_79296612的博客 1073

View的工作流程

View的工作流程View的工作流程View的工作流程入口DecorView被加载到Window中ViewRootlmpl的PerformTraveals方法理解MeasureSpecView的measure流程View的measure流程ViewGroup的measure流程View的layout流程View的draw流程步骤1:绘制背景步骤3:绘制View的内容步骤4:绘制子View步骤6:绘...

leeyhCoding的博客 342

自定义View从实现到原理(四)- View的工作流程(一)

自定义View从实现到原理(四) 在之前几篇博客我i们已经探讨了View的事件分发机制,接下来就是这一步了,那么我们就来看一下View的工作流程: View的工作流程,指的就是measure,layout和draw。其中measure作用是测量View的宽和高,layout用来确定View的位置,都让我的作用就是用来绘制View了。 View的工作流程入口 在Activity组成那一部分,最后讲到了DecorView的创建以及它加载的资源,不过这个时候我们的DecorView并没有加载到Window上,因此

Loveyy 2447
上一篇: Andriod 网络框架 OkHttp 源码解析
下一篇: View 体系详解:坐标系、滑动、手势和事件分发机制
开发者如是说
博客等级 码龄10年 67粉丝 163原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值