美文网首页
重温View绘制原理(二)

重温View绘制原理(二)

作者: 疯震震 | 来源:发表于2019-06-23 23:41 被阅读0次

    好记性不如烂笔头。生活中多做笔记,不仅可以方便自己,还可以方便他人。

    (下面的源码大部分是来自API 28)

    紧接着上一篇文章重温View绘制原理(一),继续看view绘制原理。

    1. View绘制流程

    view的绘制是从根视图 ViewRoot 的 performTraversals() 方法开始,从上到下遍历整个视图树,每个 View 控制负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。performTraversals 方法在viewRoot的实现类 ViewRootImpl 里面:

    • measure方法用于测量View的宽高
    • layout方法用于确定View在父容器中的位置
    • draw方法负责将View绘制在屏幕上

    view绘制流程图:

    流程图1.png 流程图2.png

    看看performTraversals方法源码:

      private void performTraversals() {
          ...
          int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
          int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
          ...
          // 测量
          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
          ...
          // 布局
          performLayout(lp, mWidth, mHeight);
          ...
          // 绘制
          performDraw();
          ...
      }
    

    依次调用performMeasure、performLayout、performDraw方法,分别完成顶级View的measure、layout、draw流程。performMeasure会调用measure方法,而measure又会调用onMeasure方法,在onMeasure方法中又会对子元素进行measure,这样重复下去就完成了整个View树的遍历。

    performLayout、performDraw传递过程也非常类似,不过performDraw是在draw方法中通过dispatchDraw方法实现的。

    measure过程决定了View的宽高,而Layout方法则确定了四个顶点的坐标和实际的宽高(往往等于measure中计算的宽高),draw方法则决定了View的显示。只有完成了draw方法才能正确显示在屏幕上。

    2. MeasureSpec

    MeasureSpec是measure的重要参数。
    MeasureSpec 表示的是一个 32 位的整数值,它的高 2 位表示测量模式 SpecMode,低 30 位表示某种测量模式下的规格大小 SpecSize(PS:这里用到了位运算进行状态压缩来节省内存)。MeasureSpec 是 View 类的一个静态内部类,用来说明应该如何测量这个View,有三种模式:

    • UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少使用到。

    • EXACTLY:精确测量模式,当该视图的 layout_width 或者 layout_height 指定为具体数值或者 match_parent 时生效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值。

    • AT_MOST:最大值模式,当前视图的 layout_width 或者 layout_height 指定为 wrap_content 时生效,此时子视图的尺寸可以是不超过父视图运行的最大尺寸的任何尺寸。

    下表是普通View的MeasureSpec的创建规则对应表:

    childLayoutParams/parentSpecParams EXACTLY AT_MOST UNSPECIFIED
    dp/px EXACTLY childSIze EXACTLY childSIze EXACTLY childSIze
    match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
    wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

    3. Measure

    View的绘制从测量开始,看看performMeasure()方法:

        private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    

    具体操作是分发给 ViewGroup 的,由 ViewGroup 在它的 measureChild 方法中传递给子 View。ViewGroup 通过遍历自身所有的子 View,并逐个调用子 View 的 measure 方法实现测量操作。

    3.1 View的measure过程

    View的measure过程由measure方法来完成,measure方法是一个final方法,不能重写,它会调用VIew的onMeasure方法。onMeasure方法中会调用getDefaultSize方法,而getDefault方法中又会调用getSuggestedWidth和getSuggestedHeight方法。

        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    
          /**
         * Utility to return a default size. Uses the supplied size if the
         * MeasureSpec imposed no constraints. Will get larger if allowed
         * by the MeasureSpec.
         *
         * @param size Default size for this view
         * @param measureSpec Constraints imposed by the parent
         * @return The size this view should be.
         */
        public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
    

    getDefaultSize方法所返回的就是测量后的View的大小。

    接着看getSuggestedWidth和getSuggestedHeight方法:

        /**
         * Returns the suggested minimum width that the view should use. This
         * returns the maximum of the view's minimum width
         * and the background's minimum width
         *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
         * <p>
         * When being used in {@link #onMeasure(int, int)}, the caller should still
         * ensure the returned width is within the requirements of the parent.
         *
         * @return The suggested minimum width of the view.
         */
        protected int getSuggestedMinimumWidth() {
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
    
        protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    
        }
    

    它在没有指定background的情况下,返回的是minSize这一属性对应的值,而在指定了背景的情况下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法对应的值

    这两个方法在Drawable有原始宽度的情况下返回原始宽度,否则返回0

    从getDefaultSize方法可以看出,View的宽高由specSize决定。

    3.2 ViewGroup的measure过程

    ViewGroup除了完成自己的measure过程,还会遍历调用子元素的measure方法,然后子元素再次递归执行,ViewGroup是一个抽象类,因此没有重写View的onMeasure方法。但它提供了一个measureChildren的方法,如下:

        /**
         * Ask all of the children of this view to measure themselves, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * We skip children that are in the GONE state The heavy lifting is done in
         * getChildMeasureSpec.
         *
         * @param widthMeasureSpec The width requirements for this view
         * @param heightMeasureSpec The height requirements for this view
         */
        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);
                }
            }
        }
    
        /**
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * The heavy lifting is done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param parentHeightMeasureSpec The height requirements for this view
         */
        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);
        }
    

    可以看到,ViewGroup执行measure时,会遍历子元素,调用measureChild方法对子元素进行measure。

    在measureChild方法中,取出子元素的LayoutParams,通过getChildMeasureSpec方法创建子元素MeasureSpec,然后传递给View的measure方法进行测量。

    ViewGroup没有定义测量具体过程,因为它是个抽象类。具体的测量过程的onMeasure方法需要子类来实现,由于它的子类的特性可能会很大不同,所以没法做统一处理(如LinearLayout和RelativeLayout)。

    4. Layout

    Layout流程的作用是ViewGroup确定子元素的位置。当ViewGroup被确定后,在onLayout中会遍历所有子元素并调用layout方法,在layout方法中会调用onLayout方法。layout方法确定View的位置,而onLayout方法则确定所有子元素的位置。

    ViewRootImpl 的 performLayout 如下:

        private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
            mLayoutRequested = false;
            mScrollMayChange = true;
            mInLayout = true;
    
            final View host = mView;
            if (host == null) {
                return;
            }
            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.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                ...
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            mInLayout = false;
        }
    

    先看View的layout方法:

        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);
    
                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);
                    }
                }
            }
    
            final boolean wasLayoutValid = isLayoutValid();
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
            if (!wasLayoutValid && isFocused()) {
                mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
                if (canTakeFocus()) {
                    // We have a robust focus, so parents should no longer be wanting focus.
                    clearParentsWantFocus();
                } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                    // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                    // layout. In this case, there's no guarantee that parent layouts will be evaluated
                    // and thus the safest action is to clear focus here.
                    clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                    clearParentsWantFocus();
                } else if (!hasParentWantsFocus()) {
                    // original requestFocus was likely on this view directly, so just clear focus
                    clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
                // otherwise, we let parents handle re-assigning focus during their layout passes.
            } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
                mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
                View focused = findFocus();
                if (focused != null) {
                    // Try to restore focus as close as possible to our starting focus.
                    if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                        // Give up and clear focus once we've reached the top-most parent which wants
                        // focus.
                        focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                    }
                }
            }
    
            if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
                mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
                notifyEnterOrExitForAutoFillIfNeeded(true);
            }
        }
    

    首先,通过setFrame方法设定View四个顶点的位置(初始化mLeft,mRight,mTop,mBottom)。四个顶点一旦确定,则在父容器中的位置也确定了,接着便会调用onLayout方法,来让父容器确定子容器的位置。onLayout同样和具体布局有关,因此View和ViewGroup均没有实现onLayout方法。

    5. Draw

    draw流程是将View绘制到屏幕上。先看看performDraw 方法:

        private void performDraw() {
            ....
            try {
                boolean canUseAsync = draw(fullRedrawNeeded);
                ....
            } finally {
                mIsDrawing = false;
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
    
            ...
        }
    
        private boolean draw(boolean fullRedrawNeeded) {
        ...
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                            scalingRequired, dirty, surfaceInsets)) {
          return false;
        }
      }
    
      private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
         ...
         mView.draw(canvas);
         ...
      }
    

    最终调用到每个 View 的 draw 方法绘制每个具体的 View,绘制基本上可以分为六个步骤:

       public void draw(Canvas canvas) {
        ...
        // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
          drawBackground(canvas);
        }
        ...
        // Step 2, save the canvas' layers
        saveCount = canvas.getSaveCount();
        ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
    
        // Step 4, draw the children
        dispatchDraw(canvas);
    
        // Step 5, draw the fade effect and restore layers
        canvas.drawRect(left, top, right, top + length, p);
        ...
        canvas.restoreToCount(saveCount);
        ...
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
      }
    

    View绘制过程的传递是通过dispatchDraw实现的。dispatchDraw会遍历调用所有子元素的draw方法。这样draw事件就一层层传递了下来。

    它有个比较特殊的setWillNotDraw方法。如果一个View不需要绘制任何内容,在我们设定这个标记为true后,系统就会对其进行相应优化。一般View没有启用这个标记位。但ViewGroup是默认启用的。

    它对实际开发的意义在于:我们的自定义控件继承于ViewGroup并且不具备绘制功能时,可以开启这个标记位方便系统进行后续优化。

    6. 结束语

    关于view绘制的原理在网上也特别多,时间久了也容易忘记,看一遍别人的,还不如顺便把他们的记录下来,方便自己以后温习。

    重要参考:

    相关文章

      网友评论

          本文标题:重温View绘制原理(二)

          本文链接:https://www.haomeiwen.com/subject/xvfexctx.html