Android自定义ViewAndroid技术知识Android知识

View—View绘制的三大流程

2018-01-18  本文已影响40人  SharryChoo

View 三大流程的发起点

从 Window 机制探索中, 我们看到在 WindowManagerGlobal 调用 addView 方法时, 会走到 ViewRootImpl.setView 方法中去, 从而触发 performTraversals 方法, 下面就来看看这个方法做了哪些处理

    /**
     * ViewRootImpl.performTraversals 方法
     */
    private void performTraversals() {
        final View host = mView;
        if (host == null || !mAdded)
            return;
        // 更新标记位, 正在 Traversal
        mIsInTraversal = true;

        // ... 这里省略了数百行代码
        WindowManager.LayoutParams lp = mWindowAttributes;
        if (!mStopped || mReportNextDraw) {
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                    updatedConfiguration) {
                // 这个 host 为 Window 下的第一个 View, 它的宽高一般就是制定的 Window 的宽高
                // ViewRootImpl, 其实就是用于处理 Window 下最直接的 View, 用 ViewRootImpl 来命名就显而易见了
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                // 1. 调用 performMeasure 开启 View 树的测量
                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;
                // 判断是否需要重新 performMeasure
                // 保证宽度权重大于0
                if (lp.horizontalWeight > 0.0f) {
                    width += (int) ((mWidth - width) * lp.horizontalWeight);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }
                // 保证高度权重大于0
                if (lp.verticalWeight > 0.0f) {
                    height += (int) ((mHeight - height) * lp.verticalWeight);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }
                if (measureAgain) {
                    // 从新测量一波
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
                layoutRequested = true;
            }
        }

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        if (didLayout) {
            // 2. 调用 performLayout 确定 View 的位置
            performLayout(lp, mWidth, mHeight);
        }
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
        if (!cancelDraw && !newSurface) {
            // 3. 调用 performDraw 开启 View 的绘制
            performDraw();
        } else {
            if (isViewVisible) {
                // Try again
                scheduleTraversals();
            }
        }
        mIsInTraversal = false;
    }

ViewRootImpl.performTraversals 方法一共有 700 多行代码, 这里只分析与 View 相关的核心部分, 这个方法做了如下的事情

  1. ViewRootImpl.performMeasure, 测量 View 的大小
    • 若宽高的权重不大于0, 则需要重新测量 View 大小
  2. ViewRootImpl.performLayout 确定 View 摆放的位置
  3. ViewRootImpl.performDraw 开启 View 的绘制

下面逐个分析 View 的三大流程

View 的测量

    /**
     * ViewRootImpl.performMeasure
     */
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        try {
            // 很简单直接调用了 mView 的 measure 方法
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
    
    /**
     * View.measure
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        // 1 判断是否开启了强制 Layout
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        // 2 与缓存的 mOldMeasureSpec 进行一系列的比较判断得到最终的Flag needsLayout
        // 2.1 判断 MeasureSpec 是否与缓存的一致
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        // 2.2.1 判断 MeasureSpec 的测量模式是否为 MeasureSpec.EXACTLY 
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        // 2.2.2 判断 MeasureSpec 的测量值是否与当前View测量值一致
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        // 2.3 得出最终 Flag 的值
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        // 若添加了强制测量的 Flag 或者 needsLayout = true 才会走下面的代码
        if (forceLayout || needsLayout) {
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // 3.1 无缓存: 这里回调用我们最最熟悉的 onMeasure 
                onMeasure(widthMeasureSpec, heightMeasureSpec);
            } else {
                // 3.2 有缓存: 直接调用 setMeasuredDimensionRaw
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            }
        }
        // 更新缓存的 MesureSpec
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL);
    }

可以看到 measure 方法做了如下的事情:

  1. 获取 forceLayout 和 needLayout 这两个 Flag 的值
  2. 可以看到 needLayout 这里做了很多的判断, 可以根据其判断流程做一些优化, 防止过度 Measure
    • 判断 XXXMeasureSpec 是否与缓存的 mOldXXXMeasureSpec 一致
    • 判断 MeasureSpec 的测量模式是否为 MeasureSpec.EXACTLY
    • 判断 MeasureSpec 的测量值是否与当前View测量值一致
  3. 根据这个两个 Flag 来决定是否需要启动 View 的测量
    • 无缓存: onMeasure
    • 有缓存: setMeasuredDimensionRaw

注意:

ViewGroup extends View, ViewGroup 中默认是没有重写 onMeasure 的

所以当我们继承 ViewGroup 写自定义 View 的时候必须要重写 onMeasure 方法, 一般步骤如下

    /**
     * 重写 View 的 onMeasure 方法
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 1. 获取从上层传递下来的 Mode 和 Size
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        // 2. 遍历测量子 View 大小
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            // 调用了这个 measureChildWithMargins 
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        }
        // 3. 根据子 View 大小来设置自己的大小
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }
    
    /**
     * ViewGroup.measureChildWithMargins
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        // 测量 View 的大小, 连同其设置的 margin 参数
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        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);
        // 可以看到最终又回调了 View 的 measure 方法, 这样就一层一层的将 View 的  measure 传递下去了
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

可以看到 ViewGroup 中的 Measure 主要做了两件事情:

  1. 遍历子 View, 将 measure 操作向下分发

  2. 子 View 全部测量好之后, 根据自身布局的特性, 设置自身大小

接下来看看 View.onMeasure 方法

    /**
     * View.onMeasure
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
            getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
        );
    }
    
    /**
     * View.getSuggestedMinimumWidth
     */
    protected int getSuggestedMinimumWidth() {
        // 获取建议的最小宽度, 即背景的宽度
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    
    /**
     * View.getDefaultSize
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:// 未指定的
            // 当未指定的时候, View 的大小默认为 size, 通过上面得知为其 Background 的宽高
            result = size;
            break;
        // 可见如果不重写 View 的 onMeasure 方法的话, 其 wrap_content 的作用于 match_parent 是一样的
        case MeasureSpec.AT_MOST:// 最大的: wrap_content
        case MeasureSpec.EXACTLY:// 精确的: match_parent 和 10dp
            result = specSize;
            break;
        }
        return result;
    }
    
    /**
     * View.setMeasureDimension
     */
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        // 调用者这个方法
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    
    /**
     * View.setMeasuredDimensionRaw
     */
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        // 给成员变量赋值
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

总结:
Measure 操作的过程

  1. 将 MeasureSpec 经过自身处理后, 分发到下层子 View
  2. 子 View 确定了大小之后, 再回溯到父容器中
  3. 父容器结合子 View 大小自身布局特性 来确定自己的大小, 一直回溯到顶层

可以看到 View 的 onMeasure 中还是有一些细节的, 在我们自定义 View 中起到至关重要的作用:

  1. 当测量模式为 MeasureSpec.UNSPECIFIED 时, View 的大小默认为 getSuggestedMinimumWidth() 即背景的宽高
  2. View 默认是不支持 wrap_content 属性的, 源码中它直接走到了下面的 MeasureSpec.EXACTLY 才得以 break, 我们需要重写 onMeasure, 让其支持 wrap_content
  3. 当 View 的 setMeasureDimension() 执行后, 我们可以通过 getMeasureWidth/Height() 获取其初步的宽高值

View 位置的确定

    /**
     * ViewRootImpl.performLayout
     */
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mInLayout = true;
        final View host = mView;
        if (host == null) {
            return;
        }
        try {
            // 调用了 View.layout 方法, 将他的边界传入
            // 因为 ViewRootImpl 关联的是 Window 下的直接 View, 所以他的起始位置就是 0, 0
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            mInLayout = false;
            // ... 省略了部分代码
        }
    }
    
    /**
     * ViewGroup.layout
     */
    @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 方法, 可见 ViewGroup 的 layout 方法并没有什么实际作用 
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }
    
    /**
     * View.layout
     */
    public void layout(int l, int t, int r, int b) {
        // ... 
        // 1. 很重要的方法 View.setFrame()
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 2. 调用了 onLayout 方法
            onLayout(changed, l, t, r, b);
            // ...
        }
    }
    
    /**
     * View.setFrame
     */
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            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);
            // 1. 更新当前 View 四个顶点的位置, 更新了顶点位置之后, 便可以通过 getWidth/Height 来获取宽高了
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
        }
    }
    
    /**
     * View.onLayout
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

    /**
     * ViewGroup.onLayout
     */
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

看到这里, 我们就可以解开心中的疑惑了

  1. performLayout 调用了顶级 View 的 layout 方法
  2. View.layout 方法会先调用 View.setFrame() 方法, 更新当前 View 四个顶点的坐标,
    • 注意这个时候, 就已经可以通过 getWidth/getHeight 获取自身的宽高了, 但是还不能获取子 View 的宽高
  3. Vew.layout 再调用 View.onLayout() 方法,
    • 多肽性————若当前为 ViewGroup 实例, 则会调用 ViewGroup 的 layout 方法
    • 这也就揭示了为什么 ViewGroup 必须重写 onLayout 的原因
  4. 调用了 ViewGroup 中的 onLayout 又会遍历子 View, 调用其 layout 方法, 这样每个 View 就确定了自己的位置, 是一个从上往下分发的过程

View 的绘制

    /**
     * ViewRootImpl.performDraw
     */
    private void performDraw() {
        // 只关注核心部分
        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;
        mIsDrawing = true;
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
    
    /**
     * ViewRootImpl.draw 
     */
    private void draw(boolean fullRedrawNeeded) {
        // 调用了drawSoftware
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
            return;
        }
    }
    
    /**
     * ViewRootImpl.drawSoftware 
     */
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        mView.draw(cavans)
    }
    
    
    /**
     * View.draw
     */
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        // 这个 dirtyOpaque 非常重要, 它是回调 onDraw 方法的必要条件 
        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;
        // 1. 绘制背景, 如果需要的话
        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
            // 2. 若满足条件则调用自身的 View.onDraw 方法
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            // 3. ViewGroup 中实现了该方法, 它会调用 drawChild
            // 从而回调 View.draw, 又回到该方法, 不断的往下分发
            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;
        }

好了, 代码里注释很清晰, 不过还是有一个值得关注的点:

    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

至此对 View 绘制的三大流程就有一个系统的认识了

上一篇下一篇

猜你喜欢

热点阅读