Android中View的layout布局和draw绘制

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

在博客带着问题学习Android中View的measure测量中,我们学习了view的measure测量的原理,以及解决了我们实际项目中遇到的一些问题的解决方案。在这篇博客中我们同样根据问题去学习下layout和draw的源码。

引导问题

  • 在自定义view中为什么onMeasure()方法会调用多次?目的是什么?
  • 在自定义view中draw()的绘制步骤是什么?
layout方法源码分析

概述
在自定义view/viewGroup中,我们一般需要重写onLayout方法进行布局,在这个方法中我们可以根据我们的需求对view的位置或者viewgroup中的子view的位置进行设定。我们在view的源码中查看下:

	/**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
复制代码

我们看到这个protected方法是一个空方法,一行代码也没有,这就需要我们进行重写该方法,然后实现我们的布局效果。那么这个方法在哪里执行的?就需要我们查找看看。

layout源码分析
经过查找,我们发现在layout()方法中调用了我们重写的onLayout()方法,然后实现我们自定义的布局效果。我们看下源码:

	/**
     * 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) {
		//判断layout之前是否需要进行measure,这里使用位运算符,
		//PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT是一个常量,用于标志在layout()之前
		//是否需要进行measure(),所以有时onMeasure方法调用多次
        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;
		//判断view的父view的layoutMode属性是否为LAYOUT_MODE_OPTICAL_BOUNDS,
		//然后调用set***Frame()方法进行控件的位置大小信息,以此判断控件大小是否改变
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
		//如果view的大小发生改变或者我们主动要求布局的时候,开始进行重新布局,调用我们
		//重写的onLayout方法
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
			//改变标志位
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
			//判断view中的layoutChange改变的监听事件是否为null
            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方法的解释:“为一个view的大小和位置进行赋值或者它的子项”。layout是布局机制的第二阶段,第一阶段是measure。在这个阶段parent调用layout方法放置他们的子view。view的派生类不能重写layout方法,派生类需要重写onLayout方法,在onL方法中调用layout方法来设置子view的的位置。关于参数:l、t、r、b这四个参数是相对于parent view的位置。

上面代码的注释也还比较清晰,我们主要讲解下changed变量,该变量标识着我们的view大小是否改变。我们看下系统是怎么判断的。这就涉及到这段代码:

	 boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
复制代码

代码中涉及到两个方法setOpticalFrame和setFrame,我们先看下setFrame方法。

	/**
     * Assign a size and position to this view.
     *
     * This is called from layout.
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the
     *         previous ones
     * {@hide}
     */
    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 + ")");
        }
		//判断view的位置大小是否改变,通过比较现在的值和以前存储的值进行比较
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
			//view大小改变,改变标识的值为true
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
			//计算view的旧长宽和本次的长宽值
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
			//判断view的长宽大小 是否改变
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position,刷新视图
            invalidate(sizeChanged);
			//存储本次的left、top、right、bottom值为旧值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
			//设置l、t、b、r的值
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

			//如果view大小改变,调用sizeChange方法
            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;

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
复制代码

实现的原理就是根据本次的l、r、t、b值与上次的值进行比较,判断view的位置大小是否改变,如果view的size改变,调用sizeChange()方法。我们看看这个方法里的东东。

	private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        if (mOverlay != null) {
            mOverlay.getOverlayView().setRight(newWidth);
            mOverlay.getOverlayView().setBottom(newHeight);
        }
        rebuildOutline();
    }
复制代码

在这个方法里调用了onSizeChanged()方法,这个方法就是我们自定义view时候,肯能需要重写的onSizeChanged()方法,用于处理我们对view大小改变的处理。这里涉及到一个知识点ViewOverlay这个类,可能很多人不了解,可以看下这个教程,ViewOverlay它是view的最上面的一个透明的层,我们可以在这个层之上添加内容而不会影响到整个布局结构。这个层和我们的界面大小相同,可以理解成一个浮动在界面表面的二维空间。sizeChange方法中判断当前view的ViewOverlay对象是否为null,如果不为null,就进行设置view相对parentView的right值和bottom值。具体涉及可以去看下setRight和setBottom的源码,当然还有setTop和setLeft两个方法。我们就仅仅看下setRight的源码,其它几个类似。

	/**
     * Sets the right position of this view relative to its parent. This method is meant to be called
     * by the layout system and should not generally be called otherwise, because the property
     * may be changed at any time by the layout.
     *
     * @param right The right of this view, in pixels.
     */
    public final void setRight(int right) {
        if (right != mRight) {//判断相对父view右边位置是否变化
			//判断transform matrix是否是identity matrix.
            final boolean matrixIsIdentity = hasIdentityMatrix();
            if (matrixIsIdentity) {
				//判断依附的父window是否为null
                if (mAttachInfo != null) {
                    int maxRight;
					//记录下新旧最大的right值
                    if (right < mRight) {
                        maxRight = mRight;
                    } else {
                        maxRight = right;
                    }
					//刷新计算出的新区域
                    invalidate(0, 0, maxRight - mLeft, mBottom - mTop);
                }
            } else {
                // Double-invalidation is necessary to capture view's old and new areas
                invalidate(true);
            }
			//计算旧的宽度和高度值
            int oldWidth = mRight - mLeft;
            int height = mBottom - mTop;

            mRight = right;
            mRenderNode.setRight(mRight);

            sizeChange(mRight - mLeft, height, oldWidth, height);

            if (!matrixIsIdentity) {//如果不是identity matrix.强制进行刷新一次,根据sizeChange改变后的值
                mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
                invalidate(true);
            }
            mBackgroundSizeChanged = true;
            invalidateParentIfNeeded();
            if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) {
                // View was rejected last time it was drawn by its parent; this may have changed
                invalidateParentIfNeeded();
            }
        }
    }
复制代码
draw源码分析

概述
通过上面的分析,我们已经完成了自定义view中的measure和layout两大过程,下面就开始了draw的过程,用于我们绘制我们的事务。在自定义view中,我们需要重写onDraw方法进行绘制:

	/**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
复制代码

又是一个空方法,“实现此方法来实现我们的绘制”。在View中通过draw方法来调用我们实现的onD方法来实现绘制。接下来我们就要分析draw方法中的绘制步骤?以及何时调用我们的onD方法进行绘制。

 /**
     * 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.
     */
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
		//标识该view的背景是否不透明
        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;
		//判断view是否设置水平边界渐变和垂直方向边界渐变,fadingEdge属性用来设置拉滚动条时边框渐变的放向。
		//none(边框颜色不变),horizontal(水平方向颜色变淡),vertical(垂直方向颜色变淡)。
        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,分发draw函数,绘制child view
            dispatchDraw(canvas);

            // Step 6, draw decorations (scrollbars),绘制scrollbars
            onDrawScrollBars(canvas);
			//如果ViewOverlay不为空,进行分发绘制
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(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;

        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;
		//判断是否需要paddingOffset
        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }
		//计算left、right、top、bottom的位置
        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);

        // Step 5, draw the fade effect and restore layers
        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);

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);

        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
    }
复制代码

通过代码可以看到draw的绘制顺序:

  • 绘制backgroud
  • 如果有可能为了fading进行保存cavas的图层。
  • 绘制view的内容,此时调用我们重写的onDraw方法
  • 绘制child view
  • 如果有可能,绘制fading edges同时restore Layout图层。
  • Draw decorations

我们针对draw()的源码进行了简单的注释,在view的绘制过程中,重要的两个方法就是onDraw()和dispatchDraw()两个方法。onDraw()方法是我们自定义view中自己根据需求进行编写函数。dispatchDraw()方法是我们进行绘制子view的方法。View.java中dispatchDraw()默认为空实现,因为其不包含子视图,而ViewGroup重载了dispatchDraw()来对其子视图进行绘制,通常应用程序不应该对dispatchDraw()进行重载,其默认实现体现了View系统绘制的流程。那么,接下来我们继续分析下ViewGroup中dispatchDraw()的具体流程:

	@Override  
    protected void dispatchDraw(Canvas canvas) {  
       ...  
  
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {  
            for (int i = 0; i < count; i++) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
                    more |= drawChild(canvas, child, drawingTime);  
                }  
            }  
        } else {  
            for (int i = 0; i < count; i++) {  
                final View child = children[getChildDrawingOrder(count, i)];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
                    more |= drawChild(canvas, child, drawingTime);  
                }  
            }  
        }  
      ......  
    } 
复制代码

dispatchDraw()的核心代码就是通过for循环调用drawChild()对ViewGroup的每个子视图进行绘制,上述代码中如果FLAG_USE_CHILD_DRAWING_ORDER为true,则子视图的绘制顺序通过getChildDrawingOrder来决定,默认的绘制顺序即是子视图加入ViewGroup的顺序,而我们可以重载getChildDrawingOrder函数来更改默认的绘制顺序,这会影响到子视图之间的重叠关系。
drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制,如果子视图的包含SKIP_DRAW标识,那么仅调用dispatchDraw(),即跳过子视图本身的绘制,但要绘制视图可能包含的字视图。

至此,基本完成了核心代码的分析。继续学习,感觉分析的不透彻,功力还不行啊!

Guess you like

Origin juejin.im/post/7035516275337986078