前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >View的工作原理

View的工作原理

作者头像
俞其荣
发布于 2022-07-28 00:25:18
发布于 2022-07-28 00:25:18
1.2K00
代码可运行
举报
文章被收录于专栏:俞其荣的博客俞其荣的博客
运行总次数:0
代码可运行

注:本文分析的源码基于 Android API 25

View绘制的起点

WindowManagerGlobal

addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow)

WindowManagerGlobaladdView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) 方法中,创建了 ViewRootImpl 对象,将 ViewRootImplDecorView 相关联:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    root = new ViewRootImpl(view.getContext(), display);
    ...
    // view 是 PhoneWindow 的 DecorView
    root.setView(view, wparams, panelParentView);

创建好了 root 之后,调用了 ViewRootImplsetView(View view, WindowManager.LayoutParams attrs, View panelParentView) 方法。

将 DecorView 和 ViewRootImpl 相关联。

ViewRootImpl

setView(View view, WindowManager.LayoutParams attrs, View panelParentView)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                // 将 decorView 设置给全局的 mView
                mView = view;
                ...
                // 标记已经添加了 decorView
                mAdded = true;
                ...
                // 第一次发起布局,在添加到 WindowManager 之前
                // 确保在接收其他系统事件之前完成重新布局
                requestLayout();
                ...
                // 利用 mWindowSession 以跨进程的方式向 WMS 发起一个调用,从而将DecorView 最终添加到 Window 上
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel);
                } 
                ...
            }
        }
    }

setView(View view, WindowManager.LayoutParams attrs, View panelParentView) 方法中,主要做的事情有:

  1. 保存 DecorView
  2. 第一次调用 requestLayout() ,发起整个 View 的绘制流程
  3. 将 View 添加到 Window 上去

而在这,我们重点关注 requestLayout() 方法,因为恰恰这句代码引发了整个 View 的绘制。

requestLayout()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            // 检查当前线程
            checkThread();
            mLayoutRequested = true;
            // 调用绘制
            scheduleTraversals();
        }
    }

requestLayout() 中先检查了线程,若 OK 后调用 scheduleTraversals()

scheduleTraversals()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 发送消息,调用 mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            // 内部调用了 performTraversals()
            doTraversal();
        }
    }

scheduleTraversals() 中,其实是这样的:

scheduleTraversals() -> 调用 mTraversalRunnable -> doTraversal() -> performTraversals()

所以最后还是要看 performTraversals()

performTraversals()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private void performTraversals() {
    
        // 计算 Activity 中 window 的宽高等等
        ...

        if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    // 得到 view 宽高的规格
                    // mWidth 和 mHeight 即用来描述 Activity 窗口宽度和高度
                    // lp.width 和 lp.height 就是 DecorView 的宽高
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
                            + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                            + " mHeight=" + mHeight
                            + " measuredHeight=" + host.getMeasuredHeight()
                            + " coveredInsetsChanged=" + contentInsetsChanged);

                     // Ask host how big it wants to be
                     // 开始执行测量工作,测量是从这里发起的
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    // 检查是否需要重新测量
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    // 需要再次测量的话,就再执行一遍 performMeasure
                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(mTag,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }

        ...

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            // 执行布局工作,布局是从这里发起的
            performLayout(lp, mWidth, mHeight);

        ...

        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            // 执行绘制工作,绘制是从这里发起的
            performDraw();
        } 

        ...

    }

performTraversals() 方法的代码很长很长,但是我们关注点就可以放在三大流程上。其他的代码因为自己能力欠缺,并不能一一说出这些代码的作用。所以我们接下来就把重点放在:

  1. getRootMeasureSpec
  2. performMeasure
  3. performLayout
  4. performDraw

三大流程

ViewRootImpl

measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight)

其实在 performTraversals() 中有一句代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
        desiredWindowWidth, desiredWindowHeight);

measureHierarchy 方法中已经调用了 performMeasure 来进行测量。不过作用不同,只是为了确定 window 的大小而做的测量辅助。所以可以说,并不算上在三大流程中。

measureHierarchy 中,确定了 DecorView 的 MeasureSpec 。其中 childWidthMeasureSpecchildHeightMeasureSpec 即为 DecorView 对应的 MeasureSpec

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// desiredWindowWidth 和 desiredWindowHeight 是屏幕的宽高
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

getRootMeasureSpec(int windowSize, int rootDimension)

那么就来看看 getRootMeasureSpec 咯。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

代码很简洁,也很易懂。

  1. 如果是 MATCH_PARENT ,那么对应的就是窗口大小;
  2. 如果是 WRAP_CONTENT ,那么不能超过窗口大小;
  3. 固定大小,那么就是大小就是传入的 lp.width/lp.height 了。

ViewGroup

getChildMeasureSpec(int spec, int padding, int childDimension)

顺便,我们把平时自定义 ViewGroup 计算子 View 测量规格的 getChildMeasureSpec 方法也一起来看看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 父容器的 mode
        int specMode = MeasureSpec.getMode(spec);
        // 父容器的 size
        int specSize = MeasureSpec.getSize(spec);
        // 子 view 可以使用空间,即父容器的 size - padding
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面的 switch/case 代码比较简单,而且容易理解。我们可以整理为一张表格(该表格来自于《Android开发艺术探索》):

measurespec

在这里,我们小结一下。对于 DecorView 来说,其 MeasureSpec 是由窗口的大小和自身的 LayoutParams 来共同决定的;而对于普通的 View 来说,其 MeasureSpec 是由父容器MeasureSpec 和自身的 LayoutParams 共同决定的。

measure过程

ViewRootImpl

performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)

分析 measure 过程,我们的起点就是在 ViewRootImplperformMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) 方法中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            // 进行测量
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

performMeasure 中调用了 measure 方法。说到底,DecorView 只是一个所以我们又要进入 View 类中去看下。

View

measure(int widthMeasureSpec, int heightMeasureSpec)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...

        if (forceLayout || needsLayout) {
            ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // 调用 onMeasure
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            ...
    }

Viewmeasure 方法内部是调用了 onMeasure 。所以我们还要接着跟进到 onMeasure 中才行。另外, measure 方法是用 final 修饰的,所以子类是无法进行重写的。

FrameLayout

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

这里小提一下,我们都知道 DecorView 其实是一个 FrameLayout ,所以 onMeasure 应该在 FrameLayout 中去看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        // 判断当前 framelayout 布局的宽高是否至少一个是 match_parent 或者精确值 ,如果是则置 measureMatchParent 为 false .
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        // 遍历不为 GONE 的子 view
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 对每一个子 View 进行测量
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 寻找子 View 中宽高的最大者,因为如果 FrameLayout 是 wrap_content 属性
                // 那么它的宽高取决于子 View 中的宽高最大者
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                // 如果 FrameLayout 为 wrap_content 且 子 view 的宽或高为 match_parent ,那么就添加到 mMatchParentChildren 中
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
        //设置测量结果
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        // 子View中设置为match_parent的个数
        count = mMatchParentChildren.size();
        // 若 FrameLayout 为 wrap_content 且 count > 1
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                // 如果子 View 的宽度是 match_parent 属性,那么对 childWidthMeasureSpec 修改:
                // 把 widthMeasureSpec 的宽度修改为:framelayout总宽度 - padding - margin,模式设置为 EXACTLY
                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    // 否则就按照正常的来就行了
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                // 高度同理
                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }
                //对于这部分的子 View 需要重新进行 measure 过程
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

如果上面 FrameLayoutonMeasure 流程没看懂的话也没关系。其实总的来说重要的就只有遍历 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 这个方法,这是将父容器的 measure 过程传递到子 View 中。

ViewGroup

measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)

可能有些人也有疑问,在上面 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0) 后也没看到有 child.measure 的方法啊,这是因为在 measureChildWithMargins 中内部调用了 child.measure

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        // getChildMeasureSpec 我们上面分析过了
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        // measure 传递给子 View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这下明白了吧?父容器就是遍历调用了 child.measure 这个方法将 measure 过程传递给每一个子 View 的。虽然不同的父容器 onMeasure 方法都不一样,但是相同的是,他们都会遍历调用 child.measure

View

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

上面我们也讲过,measure 方法内部其实是调用了 onMeasure ,所以子 View 被父容器调用了 measure 后,也会调用属于自己的 onMeasure 方法。那么我们就直接看向 ViewonMeasure 方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

onMeasure 方法只有一句代码,所以重点就是 getDefaultSize(int size, int measureSpec) 咯。

getSuggestedMinimumWidth() 内部逻辑:

  1. 若没有设置背景,就是 android:minWidth 的值;
  2. 若有设置背景,就是 max(android:minWidth, 背景 Drawable 的原始宽度)

getSuggestedMinimumHeight() 也是同理。

getDefaultSize(int size, int measureSpec)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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;
        // 直接返回 specSize
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

从上面我们可以看到:

  • 若是 UNSPECIFIED ,则直接返回的就是 getSuggestedMinimumWidth/getSuggestedMinimumHeight 的值;
  • 若是 AT_MOST/EXACTLY ,直接用的就是 specSize 。

而根据我们之前总结出来的表可知,只要 view 不指定固定大小,那么无论是 AT_MOST 还是 EXACTLY ,都是按照 parentSize 来的。

这也是为什么我们在自定义 View 时,如果不重写 onMeasure(int widthMeasureSpec, int heightMeasureSpec) ,wrap_content 和 match_parent 效果一样的原因。

小结

我们把 measure 过程的代码流程理一下:

ViewRootImpl.performTraversals -> ViewRootImpl.performMeasure -> DecorView.measure -> DecorView.onMeasure -> DecorView.measureChildWithMargins -> ViewGroup.measure -> ViewGroup.onMeasure -> ViewGroup.measureChildWithMargins -> ... -> View.measure -> View.onMeasure

注:DecorView 其实就是 FrameLayout

layout过程

ViewRootImpl

performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight)

在上面分析过,layout 过程是从 ViewRootImpl 中的 performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) 开始的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {

            // host 就是 DecorView,调用了 layout 方法开始布局
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            mInLayout = false;
            // mLayoutRequesters 为需要重新请求布局的 view 集合数
            int numViewsRequestingLayout = mLayoutRequesters.size();

            // 下面的代码主要用于若有请求重新布局的 view ,那么再进行重新布局
            if (numViewsRequestingLayout > 0) {
                // requestLayout() was called during layout.
                // If no layout-request flags are set on the requesting views, there is no problem.
                // If some requests are still pending, then we need to clear those flags and do
                // a full request/measure/layout pass to handle this situation.
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    // Set this flag to indicate that any further requests are happening during
                    // the second pass, which may result in posting those requests to the next
                    // frame instead
                    mHandlingLayoutInLayoutRequest = true;

                    // view 请求布局,进行重新测量和布局
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }

                    // 对整个View树进行重新测量
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;

                    // 进行第二次布局
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;
                        // Post second-pass requests to the next frame
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    Log.w("View", "requestLayout() improperly called by " + view +
                                            " during second layout pass: posting in next frame");
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

基本可知,performLayout 是通过调用 DecorView 的 layout 方法来向下传递布局的。所以我们应该继续追踪 FrameLayoutlayout 方法,其实就是 ViewGrouplayout 方法。

ViewGroup

layout(int l, int t, int r, int b)

FrameLayoutlayout 是父类 ViewGroup 实现的,添加了 final 修饰符,无法被重写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        // 调用 view 的 layout 方法
        super.layout(l, t, r, b);
    } else {  
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

ViewGrouplayout 方法中又调用了父类的方法 super.layout(l, t, r, b) 。所以我们又要到 View 类中去看。

View

layout(int l, int t, int r, int b)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @SuppressWarnings({"unchecked"})
    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);

        // 如果视图的大小和位置发生变化,会调用onLayout()
        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;
    }

上面的代码中做了这几件事:

  1. 设置当前布局中的四个顶点;
  2. 调用 setFrame 来设置新的顶点位置;
  3. 调用 onLayout 方法;
  4. 回调布局位置改变监听器;

setOpticalFrame(int left, int top, int right, int bottom)

我们先来看 setOpticalFrame 方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
    Insets parentInsets = mParent instanceof View ?
            ((View) mParent).getOpticalInsets() : Insets.NONE;
    Insets childInsets = getOpticalInsets();
    // 调用 setFrame 方法
    return setFrame(
            left   + parentInsets.left - childInsets.left,
            top    + parentInsets.top  - childInsets.top,
            right  + parentInsets.left + childInsets.right,
            bottom + parentInsets.top  + childInsets.bottom);
}

其实在 setOpticalFrame 的内部也是调用 setFrame 方法的。

setFrame(int left, int top, int right, int bottom)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        // 如果新值和旧值不相等,那就是布局位置改变了
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            // 计算新的宽高和旧的宽高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            // 判断大小是否改变
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            // 设置 view 的上下左右,赋予最新的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            // 回调大小改变的方法
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                // If we are visible, force the DRAWN bit to on so that
                // this invalidate will go through (at least to our parent).
                // This is because someone may have invalidated this view
                // before this call to setFrame came in, thereby clearing
                // the DRAWN bit.
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                // parent display list may need to be recreated based on a change in the bounds
                // of any child
                invalidateParentCaches();
            }

            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }

            // Android无障碍辅助功能通知
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }

先回根据新旧的宽高进行比较,来确定是不是大小被改变了。如果是,会回调 sizeChange(newWidth, newHeight, oldWidth, oldHeight) 方法,这个方法是不是很眼熟呢?

之后还会把这消息通知给 AccessibilityService 无障碍服务。

最后返回布局是否改变的 boolean 值。

FrameLayout

onLayout(boolean changed, int left, int top, int right, int bottom)

接着,根据布局改变值 changed 会调用 onLayout 方法。

onLayout 方法在 View/ViewGroup 都是空的,是需要子类来实现的。所以我们还是要看 FrameLayout 中的 onLayout

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

onLayout 中调用了 layoutChildren 方法。

layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        // 遍历子 view
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                // 子 view 的宽高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                // 得到子 view 的 gravity
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                // 根据不同的 gravity 来计算 childLeft
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }


                // 根据不同的 gravity 来计算 childTop
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                // 调用子 view 的 layout 方法
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

简单来说,在 layoutChildren 中,遍历所有可见的子 View ,然后得到它们的宽高。

再根据不同的 gravity 来计算 childLeft 和 childTop ,最后调用 child.layout 来向子 View 传递下去。

小结

我们把 layout 过程的代码流程理一下:

ViewRootImpl.performTraversals -> ViewRootImpl.performLayout -> DecorView(ViewGroup).layout -> View.layout -> DecorView(FrameLayout).onLayout -> DecorView(FrameLayout).layoutChildren -> ViewGroup.layout -> View.layout -> ViewGroup.onLayout -> ... -> View.layout -> View.onLayout

注:

  • ViewGroup.onLayout 是抽象方法,根据不同的 ViewGroup 都有不同的实现方式。但是相同的是,都会遍历调用 child.layout 方法;
  • View.onLayout 是空方法;

draw过程

最后一个,draw 过程。 draw 过程应该来说是比较简单的。

ViewRootImpl

performDraw()

首先起点是 performDraw() 方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private void performDraw() {
        if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
            return;
        }

        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
        try {
            // 调用 draw 方法,fullRedrawNeeded 为是否重新绘制全部视图
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        ...

    }

如果是第一次绘制视图,那么显然应该绘制所有的视图,fullRedrawNeeded 参数就为 true ;反之如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图,即为 false 。

draw(boolean fullRedrawNeeded)

performDraw() 内部又调用了私有方法 draw(boolean fullRedrawNeeded) :

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private void draw(boolean fullRedrawNeeded) {
        
        ...

        // dirty 表示需要绘制的区域
        final Rect dirty = mDirty;
        if (mSurfaceHolder != null) {
            // The app owns the surface, we won't draw.
            dirty.setEmpty();
            if (animating && mScroller != null) {
                mScroller.abortAnimation();
            }
            return;
        }

        // 如果需要全部绘制,那么 dirty 就是整个屏幕了
        if (fullRedrawNeeded) {
            mAttachInfo.mIgnoreDirtyState = true;
            dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        }

        ...

        // 调用 drawSoftware ,把绘制区域 dirty 传入
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
            return;
        }
            
        ...

    }

在确定了绘制的区域 dirty 之后,调用了 drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty)

drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        final Canvas canvas;
        try {
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;

            //锁定画布,由 dirty 区域决定
            canvas = mSurface.lockCanvas(dirty);

            // The dirty rectangle can be modified by Surface.lockCanvas()
            //noinspection ConstantConditions
            if (left != dirty.left || top != dirty.top || right != dirty.right
                    || bottom != dirty.bottom) {
                attachInfo.mIgnoreDirtyState = true;
            }

            // TODO: Do this in native
            canvas.setDensity(mDensity);
        } catch (Surface.OutOfResourcesException e) {
            handleOutOfResourcesException(e);
            return false;
        } catch (IllegalArgumentException e) {
            Log.e(mTag, "Could not lock surface", e);
            // Don't assume this is due to out of memory, it could be
            // something else, and if it is something else then we could
            // kill stuff (or ourself) for no reason.
            mLayoutRequested = true;    // ask wm for a new surface next time.
            return false;
        }

        try {
            if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
                        + canvas.getWidth() + ", h=" + canvas.getHeight());
                //canvas.drawARGB(255, 255, 0, 0);
            }

            // If this bitmap's format includes an alpha channel, we
            // need to clear it before drawing so that the child will
            // properly re-composite its drawing on a transparent
            // background. This automatically respects the clip/dirty region
            // or
            // If we are applying an offset, we need to clear the area
            // where the offset doesn't appear to avoid having garbage
            // left in the blank areas.
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

            dirty.setEmpty();
            mIsAnimating = false;
            mView.mPrivateFlags |= View.PFLAG_DRAWN;

            if (DEBUG_DRAW) {
                Context cxt = mView.getContext();
                Log.i(mTag, "Drawing: package:" + cxt.getPackageName() +
                        ", metrics=" + cxt.getResources().getDisplayMetrics() +
                        ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
            }
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;

                // 调用 View 的 draw 方法
                mView.draw(canvas);

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    // Only clear the flag if it was not set during the mView.draw() call
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
            try {
                surface.unlockCanvasAndPost(canvas);
            } catch (IllegalArgumentException e) {
                Log.e(mTag, "Could not unlock surface", e);
                mLayoutRequested = true;    // ask wm for a new surface next time.
                //noinspection ReturnInsideFinallyBlock
                return false;
            }

            if (LOCAL_LOGV) {
                Log.v(mTag, "Surface " + surface + " unlockCanvasAndPost");
            }
        }
        return true;
    }

View

draw(Canvas canvas)

之后调用了 Viewdraw(Canvas canvas)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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)
         */

        // 第一步,画背景
        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) {
            // 第三步,画自己的内容
            if (!dirtyOpaque) onDraw(canvas);

            // 第四步,画自己子 view 的内容
            dispatchDraw(canvas);

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

            // 第六步,绘制View的装饰,比如 scrollbar 等 (foreground, scrollbars)
            onDrawForeground(canvas);

            // 做完了,直接返回 we're done...
            return;
        }

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */

        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        // 第二步,保存 canvas 图层
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

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

        // 第五步,绘制边缘效果和恢复图层
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

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

draw 过程大概有下面几步:

  1. 绘制背景:background.draw(canvas)
  2. 保存当前的图层信息(一般来说跳过);
  3. 绘制自己:onDraw(canvas)
  4. 绘制children:dispatchDraw(canvas)
  5. 绘制边缘效果,恢复图层(一般来说跳过);
  6. 绘制前景装饰:onDrawForeground(canvas)

在这里,我们继续看一下 dispatchDraw(Canvas canvas) 方法,这个方法是向子 View 分发绘制流程的。

因为 View 没有子 View ,所以 dispatchDraw(Canvas canvas) 方法是空的,所以我们要到 ViewGroup 中去看看。

ViewGroup

dispatchDraw(Canvas canvas)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();

            // 遍历子 view 
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }

            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }

            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;

            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }

        int clipSaveCount = 0;
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            clipSaveCount = canvas.save();
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }

        // We will draw our child's animation, let's reset the flag
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        boolean more = false;
        final long drawingTime = getDrawingTime();

        if (usingRenderNodeProperties) canvas.insertReorderBarrier();
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        int transientIndex = transientCount != 0 ? 0 : -1;
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                // 调用 drawChild 来绘制子 view
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ...
    }

dispatchDraw(Canvas canvas) 中,遍历子 View ,然后调用 drawChild(Canvas canvas, View child, long drawingTime) 方法来执行子 View 的绘制流程。

drawChild(Canvas canvas, View child, long drawingTime)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

发现在 drawChild(Canvas canvas, View child, long drawingTime) 中还是调用了 draw(Canvas canvas, ViewGroup parent, long drawingTime) 方法。但是这个 draw(Canvas canvas, ViewGroup parent, long drawingTime) 和上面的 draw(Canvas canvas) 参数不同,所以不是同一个方法。

View

draw(Canvas canvas, ViewGroup parent, long drawingTime)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

        ...

        // 如果没有绘制缓存
        if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                // 如果设置了 willNotDraw 为 true ,那么不会绘制自己,直接跳过,优化绘制性能
                // View 默认是 false ,ViewGroup 默认是 true ,直接让自己的子 View 进入绘制
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    // 调用 draw 方法
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            // 有缓存就用缓存绘制
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
                // no layer paint, use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == null) {
                    cachePaint = new Paint();
                    cachePaint.setDither(false);
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                // use layer paint to draw the bitmap, merging the two alphas, but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                if (alpha < 1) {
                    mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                }
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                if (alpha < 1) {
                    mLayerPaint.setAlpha(layerPaintAlpha);
                }
            }
        }

        ...

    }

draw(Canvas canvas, ViewGroup parent, long drawingTime) 中,若没有缓存的话:

  • willNotDraw 设置为 false 的话,那么调用 draw(canvas)
  • 否则直接调用 dispatchDraw(canvas) 分发给子 View ,一般适用于 ViewGroup ;

willNotDraw 代表一个 View 不需要绘制任何内容的话,那么系统会跳过,进行性能上的优化。

到这里,就调用了子 View 的 draw(Canvas canvas) 方法,从而实现了绘制过程的向下传递。

小结

我们把 draw 过程的代码流程理一下:

ViewRootImpl.performTraversals -> ViewRootImpl.performDraw -> ViewRootImpl.draw(boolean fullRedrawNeeded) -> ViewRootImpl.drawSoftware -> DecorView(View).draw(Canvas canvas) -> DecorView(ViewGroup).dispatchDraw -> DecorView(ViewGroup).drawChild -> ViewGroup(View).draw(Canvas canvas, ViewGroup parent, long drawingTime) -> ViewGroup.dispatchDraw -> ViewGroup.drawChild -> ViewGroup.draw(Canvas canvas, ViewGroup parent, long drawingTime) -> ... -> View.draw(Canvas canvas) -> View.onDraw -> View.dispatchDraw

注:

  • 其中 View.dispatchDraw 为空实现;
  • DecorView 在 draw(Canvas canvas) 的方法内不会调用 onDraw 方法;
  • ViewGroup 不会调用 draw(Canvas canvas) 方法;

最后

总体来说,三个流程中主要还是 measure 过程较复杂。其他的两个流程整体上来说还是比较清晰简单的。

可以说 View 工作的三大流程是每一位 Android 开发者都必须掌握的。之前虽然也了解,但是没有写成博客好好捋一下,现在终于完成了,篇幅真的太长了。 _

另外,除了需要了解这三大流程外,还需要知道 requestLayoutinvalidate 等方法的原理。这些东西等有空了我理一理再写出来给大家吧。

今天就这样了,如果有不懂的地方可以在下面留言。

References

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-12-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Java-JDK动态代理(AOP)
使用JDK的反射机制,创建对象的能力,创建的时代理类的对象.而不用我们创建类的文件.
Java架构师必看
2021/05/14
2760
Java-JDK动态代理(AOP)
Feign原理 (图解)_feign原理
从上图可以看到,Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的 Request 请求。通过Feign以及JAVA的动态代理机制,使得Java 开发人员,可以不用通过HTTP框架去封装HTTP请求报文的方式,完成远程服务的HTTP调用。
全栈程序员站长
2022/11/10
4.3K0
Feign原理 (图解)_feign原理
SpringCloud原理之feign
前面一节我们学习了一下eureka,我们来回顾一下,首先它是一个cs架构,分为客户端和服务端,
用户9927510
2022/07/29
6500
SpringCloud原理之feign
Spring Cloud Feign工作原理、负载均衡及使用示例
Feign 是一个开源的Java HTTP客户端框架,主要用于简化服务间的HTTP调用,特别是针对微服务架构中的服务间通信。它允许开发者采用声明式的方式来定义HTTP请求,即将HTTP请求抽象成Java接口的方法调用,从而让服务间的调用看起来像是在调用本地方法一样简洁易懂。
用户7353950
2024/04/24
1.1K0
Spring Cloud Feign工作原理、负载均衡及使用示例
Feign 体系架构解析
正所谓麻雀虽小五脏俱全,HTTP 调用看着简单,实则下面隐藏的是一套非常复杂的流程。
程序员波特
2024/01/29
1940
Feign 体系架构解析
10000字 | 深入理解 OpenFeign 的架构原理
上次我们深入讲解了 Ribbon 的架构原理,这次我们再来看下 Feign 远程调用的架构原理。
悟空聊架构
2022/05/13
2.4K0
10000字 | 深入理解 OpenFeign 的架构原理
简单理解Feign的原理与使用
Spring Cloud 常见的集成方式是使用Feign+Ribbon技术来完成服务间远程调用及负载均衡的,如下图
ha_lydms
2023/08/09
8560
简单理解Feign的原理与使用
程序员不可不学的REST服务集成,Feign实现REST调用?
Feign 是 一 个 声 明 式 的 Web Service 客 户 端 , 它 使 得 编 写 WebService客户端更为容易。Feign受到Retrofit、JAXRS2.0、WebSocket的影响,采用声明式的API调用模式。
愿天堂没有BUG
2022/10/28
7920
程序员不可不学的REST服务集成,Feign实现REST调用?
spring cloud服务间调用之feign
在微服务架构盛行的年代,我们将一个大型的系统,拆解成各个服务,要完成一个业务逻辑,就可能需要,调用不同服务。比如订单服务调用会员服务。当然我们可以使用JDK自带的URLConnection,或者Apache的Http Client来调用,但是最为优雅的使用feign。
丁D
2022/08/12
9820
spring cloud服务间调用之feign
花一个周末,掌握 SpringCloud OpenFeign 核心原理
现在的微服务在互联网圈子里应用已经相关广泛了,SpringCloud 是微服务领域当之无愧的 "头牌"
Java识堂
2021/08/05
8.1K0
花一个周末,掌握 SpringCloud OpenFeign 核心原理
Feign的核心功能(三)
Feign支持通过集成Ribbon和Eureka来实现负载均衡和服务发现的功能。Ribbon是一个客户端负载均衡器,它可以根据不同的负载均衡算法来选择要调用的服务实例。Eureka是一个服务发现组件,它可以帮助我们动态地发现和注册服务实例,以实现服务的动态扩缩容和高可用性。
堕落飞鸟
2023/04/07
3220
SpringCloudRPC远程调用核心原理:Feign远程调用的执行流程
由于Feign中生成RPC接口JDK动态代理实例涉及的InvocationHandler调用处理器有多种,导致Feign远程调用的执行流程稍微有所区别,但是远程调用执行流程的主要步骤是一致的。这里主要介绍与两类InvocationHandler调用处理器相关的RPC执行流程:
愿天堂没有BUG
2022/10/31
1.5K0
SpringCloudRPC远程调用核心原理:Feign远程调用的执行流程
Feign的工作原理
Feign是一个伪Java Http 客户端,Feign 不做任何的请求处理。Feign 通过处理注解生成Request模板,从而简化了Http API 的开发。开发人员可以使用注解的方式定制Request API模板。
全栈程序员站长
2022/11/09
6580
Feign的工作原理
万字+33张图探秘OpenFeign核心架构原理
在很久之前,我写过两篇关于OpenFeign和Ribbon这两个SpringCloud核心组件架构原理的文章
三友的java日记
2024/02/29
1.4K0
万字+33张图探秘OpenFeign核心架构原理
你都用过SpringCloud的哪些组件,它们的原理是什么?
看到文章的题目了吗?就是这么抽象和笼统的一个问题,确实是我面试中真实被问到的,某共享货车平台的真实面试问题。 SpringCloud确实是用过,但是那是三四年前了,那个时候SpringCloud刚开始流行没多久,我们技术总监让我们调研一下,然后算上我在内的三个同事就一人买了一本SpringCloud的书籍,开始看,开始研究,正好那个时候DDD也比较火,然后我们就一边研究的SpringCloud一边按照DDD的模型搭建自己的项目。 但是这个项目最后做了三个月,才完成了一期。后面二期还没开始,我就撤了。所以SpringCloud总共的使用时间就两三个月,所以对这部分知识掌握的并不扎实,而且入职了新公司之后,都是使用公司自己封装的框架,也已经三年没有用过SpringCloud了,这次是要面试换工作了,所以决定将这方面的知识,总结一下。
纪莫
2021/01/21
7420
配置Spring Cloud Feign(一)
Spring Cloud Feign是一个声明式的HTTP客户端,它简化了使用HTTP客户端调用RESTful API的过程。使用Spring Cloud Feign,我们可以将RESTful API的调用看做是一个普通的方法调用,而不需要关心具体的HTTP请求和响应的细节。
堕落飞鸟
2023/04/07
3820
Feign:简化微服务通信的利器
Feign 是一个声明式、模板化的 HTTP 客户端,它简化了编写 Web 服务客户端的过程。它的主要目的是使 HTTP API 客户端的开发变得更加简单和直观。Feign 的设计理念是将 HTTP 客户端的细节隐藏在背后,使开发者可以专注于定义与服务端通信的接口而无需关注底层的实现细节。
繁依Fanyi
2024/05/13
6510
面试系列之-Spring Cloud Feign
根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象;
用户4283147
2023/11/20
3600
面试系列之-Spring Cloud Feign
Feign源码解析4:调用过程
前面几篇分析了Feign的初始化过程,历经艰难,可算是把@FeignClient注解的接口对应的代理对象给创建出来了。今天看下在实际Feign调用过程中的一些源码细节。
低级知识传播者
2024/01/17
4260
Feign源码解析4:调用过程
SpringCloudRPC远程调用核心原理:FeignRPC动态代理实例创建流程
首先回顾一下Feign的整体运作流程。Feign英文直译为假装/装作,也就是说Feign是一个伪客户端,即它不做任何的HTTP请求处理。
愿天堂没有BUG
2022/10/31
8600
SpringCloudRPC远程调用核心原理:FeignRPC动态代理实例创建流程
相关推荐
Java-JDK动态代理(AOP)
更多 >
目录
  • View绘制的起点
    • WindowManagerGlobal
      • addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow)
    • ViewRootImpl
      • setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
      • requestLayout()
      • scheduleTraversals()
      • performTraversals()
  • 三大流程
    • ViewRootImpl
      • measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight)
      • getRootMeasureSpec(int windowSize, int rootDimension)
    • ViewGroup
      • getChildMeasureSpec(int spec, int padding, int childDimension)
  • measure过程
    • ViewRootImpl
      • performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)
    • View
      • measure(int widthMeasureSpec, int heightMeasureSpec)
    • FrameLayout
      • onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    • ViewGroup
      • measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
    • View
      • onMeasure(int widthMeasureSpec, int heightMeasureSpec)
      • getDefaultSize(int size, int measureSpec)
    • 小结
  • layout过程
    • ViewRootImpl
      • performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight)
    • ViewGroup
      • layout(int l, int t, int r, int b)
    • View
      • layout(int l, int t, int r, int b)
      • setOpticalFrame(int left, int top, int right, int bottom)
      • setFrame(int left, int top, int right, int bottom)
    • FrameLayout
      • onLayout(boolean changed, int left, int top, int right, int bottom)
      • layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity)
    • 小结
  • draw过程
    • ViewRootImpl
      • performDraw()
      • draw(boolean fullRedrawNeeded)
      • drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty)
    • View
      • draw(Canvas canvas)
    • ViewGroup
      • dispatchDraw(Canvas canvas)
      • drawChild(Canvas canvas, View child, long drawingTime)
    • View
      • draw(Canvas canvas, ViewGroup parent, long drawingTime)
    • 小结
  • 最后
  • References
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档