View 的工作流程(二)

上篇文章我们已经讲解了 View 的 measure 过程,这篇我们来继续分下下边的内容 View 的 Layout 过程和 Draw 过程

1. Layout 过程

Layout 的作用是 ViewGrrou 用来确定子元素的位置,当 ViewGrrou 的位置被确定后,它在 onLayout 中会遍历所有子元素并调用其 layout 方法,在 layout 方法中,onLayout 方法又会被调用,Layout 过程和 measure 过程相比就简单多了,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,先看下 View 的 layout 方法,代码如下:

   /**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @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);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            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;
    }

layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个定点的位置,即初始化 mLeft,mTop,mRight,mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了,接着会调用 onLayout 方法,这个方法用处是父容器确定子元素的位置,和 onMeasure 方法相似,onLayout 的具体实现同样和具体的布局有关,

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
LinearLayout 中 onLayout 的实现逻辑和 onMeasure 的实现逻辑类似,这里选择 layoutVertical 继续理解,为了更好的理解逻辑,这里只给出主要的代码:
 final int count = getVirtualChildCount();
 for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }

这里分析一下 layoutVertical 的代码逻辑,可以看到,此方法会遍历所有子元素调用 setChildFrame 方法来为子元素指定位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合 LinearLayout 的特性,至于 setChildFrame,它仅仅调用子元素的 layout 方法而已,这样父元素在 layout 方法中完成自己的定位以后,就通过 onLayout 方法去掉用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样一层一层的传递下去就完成了整个 View 树的 layout 过程,setChildFrame 对的代码如下:

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }
实际上,setChildFrame 里边的 width 和 height 就是子元素的测量宽高,我们来看下 layoutVertical 方法中的一段代码,就会明白为什么这么说了:
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            setChildFrame(child, childLeft + getLocationOffset(child), childTop,
                        childWidth, childHeight);
而在 layout 方法中会通过 setFrame 去设置 子元素的四个顶点,在 setFrame 中有如下几句赋值语句,这样一来子元素的位置就确定了:
  mLeft = left;
  mTop = top;
  mRight = right;
  mBottom = bottom;
上面我们提到一个问题,那就是大部分情况下,测量宽高就是最终宽高。那到底为什么这么说呢?这个问题可以具体为,View 的 getMeasureWidth 和 getWidth 这两个方法有什么区别,至于 getMeasureHeight 和 getHeight 的区别前两者完全一样。为了回答这个问题,首先我们来看下 getWidth 和 getHeight 的源码:
    /**
     * Return the width of the your view.
     *
     * @return The width of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getWidth() {
        return mRight - mLeft;
    }

    /**
     * Return the height of your view.
     *
     * @return The height of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getHeight() {
        return mBottom - mTop;
    }
从 getWidth 和 getHeight 的源码再结合 mLeft,mTop,mRight,mBottom 这四个变量的赋值过程来看,getWidth 方法的返回值刚好就是 View 的测量宽度,而 getHeight 刚好就是 View 的测量高。经过上面的分析,我们可以回答这个问题了,在 View 的默认视线中,View 的测量宽高和最终宽高是相等的,只不过测量宽高形成于 View 的 measure 过程,而最终宽高形成于 View 的 layout 过程,即两者的赋值时机不同。因此,在日常开发中,我们可以认为 View 的测量宽高就等于 View 的最终宽高,但是的确存在特殊情况会导致两者不一致,如重写 layout 方法:
   @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r + 100, b + 100);
    }

上述代码会导致在任何情况下 View 的测量宽高总是比最终宽高小 100dp,虽然这样写会导致 View 显示不正常并且没有实际的意义,但是这也证明了测量宽高不等于最终宽高,另外一种情况实在某些情况下,View 需要多次 measure 才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相等。

2. Draw 过程

Draw 过程就比较简单了,它的作用是将 View 绘制在屏幕上,View 的绘制过程遵循如下几步:

(1) 绘制背景 backgrouond.draw(canvas);

(2) 绘制自己(onDraw);

(3) 绘制 children(dispatchDraw)

(4) 绘制装饰(onDrawScrollBars)

这一点通过 draw 方法可以看出来,代码如下:

    /**
     * Manually render this view (and all of its children) to the given Canvas.
     * The view must have already done a full layout before this function is
     * called.  When implementing a view, implement
     * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
     * If you do need to override this method, call the superclass version.
     *
     * @param canvas The Canvas to which the View is rendered.
     */
    @CallSuper
    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)
         */

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

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

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

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

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

            // we're done...
            return;
        }
View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层的传递了下去,View 有一个特殊的方法 setWillNotDraw,先看下他的源码:
    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
从 setWillNotDraw 方法的注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true以后,系统会进行相应的优化,默认情况下,View 没有用这个优化标记为,但是 ViewGroup 会启用这个标记位。这个标记为对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备会职功能时,就可以开启这个标记位从而便于系统进行后续的优化,当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们要显式的关闭 WILL_NOT_DRAW  这个标记位。

到此,View 的工作原理就介绍完了。

猜你喜欢

转载自blog.csdn.net/sinat_29874521/article/details/80095265