美文网首页
View 工作原理(三)| 艺术探索笔记

View 工作原理(三)| 艺术探索笔记

作者: brickx | 来源:发表于2018-10-31 22:55 被阅读0次

    这里介绍 View 的 Layout 和 Draw 过程。

    Layout 过程

    Layout 过程的作用是 ViewGroup 来确定子元素的位置,来看 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);
            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;
    }
    

    首先通过 setFrame 方法来设定 View 四个顶点位置,分别是 mLeft、mRight、mTop、mBottom 的值,四个顶点一旦确定,View 在父容器的位置也就确定了。接着调用 onLayout 方法,它用来确定子元素的位置。与 onMeasure 方法类似,View 和 ViewGroup 都没有实现 onLayout 方法,于是再次选择 LinearLayout 的 onLayout 方法

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

    由于 layoutVertical 与 layoutHorizontal 方法实现类似,所以只看 layoutVertical 方法的主要代码

    void layoutVertical(int left, int top, int right, int bottom) {
    
        ...
    
        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();
            }
    
            ...
    
            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 会逐渐增大,这意味元素会从上到下排列。

    看下 setChildFrame 方法

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

    setChildFrame 中仅仅是调用子元素的 layout 方法。其中 width 和 height 是子元素的测量宽高

    final int childWidth = child.getMeasuredWidth();
    final int childHeight = child.getMeasuredHeight();
    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
        childWidth, childHeight);
    

    总结一下,父元素在 layout 方法中完成定位后,通过 onLayout 方法调用子元素的 layout 方法,而子元素会通过 layout 方法来确定自己的位置,这样一层层的传递就完成了 View 的 Layout 过程。

    View 工作原理(一)中有提到测量宽高大部分情况下等于最终宽高,之所以说大部分情况,是因为测量宽高的赋值早于最终宽高,如果重写 View 的 layout 方法

    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r + 10, b + 10);
    }
    

    那么这种情况下,最终宽高不等于测量宽高。另一种情况是,当 View 需要多次 measure 来测量宽高,那么前几次的测量值可能就不会与最终宽高相等。

    Draw 过程

    Draw 过程的作用是将 View 绘制到屏幕上,看 draw 方法

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) ==
            PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        /*
        * 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);
    
            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);
    
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // we're done...
            return;
        }
    
        ...
    
    }
    

    于是得到主要步骤

    1. 绘制背景 drawBackground
    2. 绘制内容 onDraw
    3. 绘制子元素 dispatchDraw
    4. 绘制装饰 onDrawScrollBars

    来看 drawBackground 方法

    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        setBackgroundBounds();
    
        ...
    
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    

    drawBackground 方法使用 scrollX 和 scrollY 记录偏移值,然后根据偏移值进行绘制。

    由于 View 没有实现 onDraw 方法,所以先跳过。

    在普通 View 中,因为它没有子元素,所以 dispatchDraw 方法为空。于是看 ViewGroup 的 dispatchDraw 方法

    protected void dispatchDraw(Canvas canvas) {
    
        ...
    
        for (int i = 0; i < childrenCount; i++) {
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    
        ...
    
    }
    
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    

    可以看到,dispatchDraw 方法会遍历所有子元素的 draw 方法,这样就将 Draw 过程传递了下去。

    最后通过 onDrawScrollBars 方法绘制完滚动条(滚动条可能不显示)。这样 View 的 Draw 过程就完成了。

    相关文章

      网友评论

          本文标题:View 工作原理(三)| 艺术探索笔记

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