Android学习笔记(六)| View的工作原理(下)

参考书籍:《Android开发艺术探索》 任玉刚
如有错漏,请批评指出!

View的工作流程

前面说过,View的工作流程主要是指 measure、layout、draw 这三大流程,下面来一一进行分析。

measure过程

View和ViewGroup的 measure 过程是不同的,如果只是一个普通View,那么通过 measure 方法就能完成其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历所有子元素,并调用其 measure 方法,各个子元素再递归去执行这个流程。

  • View 的 measure 过程
    View的 measure 过程由其 measure() 方法来完成,measure()方法是一个 final 类型的方法,也就是说子类不能重写这个方法,在 measure() 方法中会调用 onMeasure() 方法,因此我们只需要看 onMeasure() 方法的源码实现:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    onMeasure() 方法通过调用 setMeasuredDimension() 方法来设置 View 的测量宽高(不等同于实际宽高),而这个测量宽高是通过 getDefaultSize() 方法来处理的,我们再来看 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:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    

    这个方法的逻辑很简单,我们需要注意的是,传入的 measureSpec 参数就是 View 宽 / 高的测量值,也就是说,当View的测量模式为 AT_MOST 或 EXACTLY 模式时,这个方法返回的就是View测量后的 SpecSize。
    当View测量模式为 UNSPECIFIED 模式时,一般用于系统内部的测量过程,这种情况下,会返回第一个参数,也就是getSuggestedMinimumWidth()getSuggestedMinimumHeight() 方法的返回值。我们来看一下它们的源码:

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
    

    这两个方法的实现原理是相同的,我们只看 getSuggestedMinimumWidth(),这个方法里面涉及到三个值:
    mBackground —— 即当前View的背景
    mMinWidth —— android:mMinWidth 这个属性指定的值
    mBackground.getMinimumWidth() —— 当前View背景Drawable的原始宽度
    这个逻辑很简单,我们直接来看这三个值,前两个很好理解,第三个是什么意思呢?来看一下 Drawable 的 getMinimumWidth() 方法的实现:

    public int getMinimumWidth() {
        // Drawable 的原始宽高
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    

    可以看到,当Drawable有原始宽高时,这个方法返回的是 Drawable 的原始宽高。至于Drawable什么时候有原始宽高(暂且略过,或者可以先看看《Android开发艺术探索》第六章),这里先举个例子,ShapeDrawable 无原始宽高,而BitmapDrawable有原始宽高(图片尺寸)。

    从getDefaultSize() 方法的实现可以得出一个结论,当我们自定义View时,如果直接继承View,就需要重写 onMeasure() 方法并设置 wrap_content(即AT_MOST模式) 的默认大小,否则在布局中使用 wrap_content 属性的效果
    和 match_parent 属性的效果是一样的。原因如下:当我们给View的宽高指定 wrap_content 属性时,它的 specMode
    是 AT_MOST模式, 在这种模式下,它的宽高等于 specSize。在 Android学习笔记(五)| View的工作原理(上) 的结尾,我们总结了一张表,在这种模式下,View的 specSize不会超过 parentSize,而 parentSize 是父容器的 可用空间,但是View 类并没有对这个specSize作任何处理,所以 specSize 就是 parentSize ,也就是说,这种模式下View会填充满父容器的可用空间,即和 match_parent 的效果相同。

    这个问题很好解决。只需要重写 onMeasure() 方法,给 AT_MOST 模式指定一个默认宽高即可:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
        // mWidth 即指定的默认宽度
        if (widthMeasureMode == MeasureSpec.AT_MOST){
            widthMeasureSize = mWidth;
        }
        // mHeight 即指定的默认高度
        if (heightMeasureMode == MeasureSpec.AT_MOST){
            heightMeasureSize = mHeight;
        }
        setMeasuredDimension(widthMeasureSize, heightMeasureSize);
    }
    
  • ViewGroup 的 measure 过程
    ViewGroup 除了完成自己的 measure 过程,还会遍历所有的子元素并调用其 measure 方法,各个子元素再递归去执行这个过程。不过ViewGroup是一个抽象类,没有重写View的 onMeasure() 方法,但是它提供了一个 measureChildren() 的方法:

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

    这个方法会依次遍历所有子元素,若当前子元素的 visibility 不为 GONE 时,就会调用 measureChild() 方法:

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

    这个方法的逻辑也很简单,就是取出当前子元素的 LayoutParams,通过 getChildMeasureSpec() 方法(在前面关于 MeasureSpec 的内容中分析过这个方法的源码)来创建子元素的 MeasureSpec,然后将MeasureSpec直接传递给子元素的 measure() 方法进行测量。
    通过上面的分析,我们会发现 ViewGroup 并没有定义其具体测量过程,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等。之所以这么设计,是因为不同的ViewGroup子类有不同的布局特性,因此他们的测量细节存在差异,无法统一实现。

    接下类会从一个具体的ViewGroup(LinearLayout)的源码层面来分析其 measure 过程。

  • LinearLayout 的 measure 过程
    首先来看 LinearLayout 的 onMeasure() 方法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
    

    这里就是单纯的判断 LinearLayout 的布局方式,我们选择 VERTICAL 方式来进行分析,下面是 measureVertical() 方法的源码主干部分(因为这个方法很长,我们只看大概逻辑):

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        
        ...  (变量定义)
    
        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            
            ...    (判断View是否需要测量以及是否有分割线)
    
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
            totalWeight += lp.weight;
            // 是否使用了额外的空间
            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
    
                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
    
                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }
    
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
    
                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }
    
            ...            
        // 子元素测量完成
        }
        
        ...
    
    }
    

    从这段代码可以看出,LinearLayout 内部会遍历子元素并对需要测量的子元素执行 measureChildBeforeLayout() 方法,这个方法里面调用了 measureChildWithMargins() 方法(前面介绍 MeasureSpec 时分析过),这样每个子元素就依次进入 measure 流程了。并且 所有子元素的测量高度都会被累计起来,用 mTotalLength 这个变量来存储,每测量一个子元素,mTotalLength 就会增加(包括子View的高度和竖直方向的 mgrgin)。当子元素测量完毕后,LinearLayout会测量自己的大小,这部分代码刚才省略了:

    
        ...
    
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
    
        int heightSize = mTotalLength;
    
        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        ...
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    
        ...
    
    

    这段代码的逻辑也很清晰,就是确定 LinearLayout 的高度,然后调用 setMeasuredDimension() 方法来设置其宽高。对于竖直方向的 LinearLayout 而言,它在水平方向的测量过程遵循 View 的测量过程,这里调用了 resolveSizeAndState() 方法来确定测量宽度,具体逻辑与View的测量过程相似,这里不作分析。在竖直方向则有所不同,如果它的布局中高度采用的是 match_parent 或具体数值,则测量过程和View一致,即高度为specSize;如果它的布局中高度采用 wrap_content,那么它的高度是所有子View所占用高度的和(最大为它的父容器剩余空间);当然,它的最终高度还要减掉竖直方向上的 padding 值。

    View的 measure 过程是三大流程中最复杂的一个, measure 完成后,通过 getMeasuredWidth() / getMeasuredHeight()方法可以获取到测量宽高。不过在某些极端情况,系统可能需要多次测量才能确定最终的测量宽高,这个时候 onMeasure() 方法中拿到的测量宽高可能不准确。比较好的做法就是在 onLayout() 方法中去获取View的测量宽高或最终宽高。

layout 过程

参考博客:自定义View Layout过程
Layout 过程的作用是确定View的位置,对于ViewGroup而言,layout() 方法确定自身位置,然后在layout() 方法中会调用 onLayout() 方法,这个方法中会遍历所有子元素,并调用其layout方法,依次递归;而对于普通View,则只需要确定自身的位置。

  • 先来看普通 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;
    
        // 确定自身位置参数
        // 并判断当前View大小和位置是否发生变化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 普通View 没有子View,因此onLayout() 方法是一个空实现
            onLayout(changed, l, t, r, b);
    
            ···
        }
        ...
    }
    

    可以看到,这里其实就是确定自身的位置,不过涉及到两个方法 setOpticalFrame()setFrame() ,这两个方法的区别在于是否有 视觉边界,直接看源码:

    /**
      * setFrame()
      * 设置View自身位置(包含视觉边界)
      */
    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        // 实际还是调用 setFrame() 方法
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    
     /**
      * setFrame()
      * 设置View自身位置(不考虑视觉边界)
      * 返回值用来标记View大小和位置是否改变
      */
    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;
    
            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
    
            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);
    
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
            ···
        }
        return changed;
    }
    

    从上面的代码可以看到,setOpticalFrame() 方法实际上还是调用了 setFrame() 方法,setFrame() 方法实际就是根据传入的位置参数确定View实际的位置,如果View的位置大小发生变化,就会调用invalidate() 方法,从而让系统重绘这个View。

  • 对于ViewGroup,因为它是继承自View类的,因此它的 layout() 方法与 View 相似,而 onLayout() 的具体实现和具体布局有关,因此View和ViewGroup均没有具体实现onLayout() 方法,所以我们需要结合一个具体的ViewGroup来分析其 onLayout() 过程。这里以LinearLayout 为例,来看 LinearLayout的 onLayout() 方法:

    /**
     * onLayout() 方法分两种情况(就是 orientation 属性指定的 vertical 或 horizontal)
     * 分为水平布局和垂直布局(下面只看垂直布局)
     */
    @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);
        }
    }
    
    /**
     * onLayout() 在垂直布局情况下的实现
     */
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
    
        int childTop;
        int childLeft;
    
        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;
    
        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;
    
        // 子View数量
        final int count = getVirtualChildCount();
    
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    
        // 根据指定的停靠位置(gravity属性)来决定 子View 在竖直方向上的布局起始位置
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
    
               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;
    
           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
    
        // 依次遍历子Viwe
        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;
                // 确定子View的位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
                 // 将当前View的高度(包括divider、padding、margin)累加到chileTop中,下一个View从这个位置开始
                i += getChildrenSkipCount(child, i);
            }
        }
    }
    
    /**
     * 将 layout 过程传递到子View
     */
    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }
    

    逻辑其实很简单,就是依次从上往下排布子View,并且将layout过程传递到子View。这样自上而下就完成了整个View树的 layout 过程。

draw 过程

参考博客:自定义View Draw过程

  • draw 也分 普通 View 的 draw过程 和 ViewGroup 的 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;
    
        // 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 (foreground, scrollbars)
            onDrawForeground(canvas);
    
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
    
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
    
            // we're done...
            return;
        }
        ···
    }
    

    根据代码逻辑可以看到,View 的 draw 过程主要分为4步:

    1. step1 drawBackground() —— 绘制背景;
    2. step3 onDraw() —— 绘制View自身;
    3. step4 dispatchDraw() —— 绘制子View
    4. step6、step7 —— 绘制装饰
      (step2和step5 是涉及到优化的内容,这里先不管)

    对于普通View而言,显然是没有子View需要绘制的,因此对于普通View,dispatchDraw()方法是一个空实现;对于ViewGroup,则必须实现dispatchDraw() 方法。
    我们先来看 drawBackground()onDraw()onDrawForeground()这三个方法的实现:

    /**
     *  如果给View指定了背景,就会 调用了个方法来绘制背景
     */
    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        // 根据 layout 过程获得的位置参数,来确定绘制背景的边界
        setBackgroundBounds();
    
        ···
    
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            // 若 scrollX 和 scrollY有值,则对画布(canvas)的坐标进行偏移(有没有联想到scrollTo和ScrollBy方法)
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    
    /**
     * 自定义View时 必须 且 只需 实现这个方法,来绘制View
     */
    protected void onDraw(Canvas canvas) {
    // View 类没有实现,需要根据View自身的情况重写这个方法
    }
    
    /**
     * 绘制装饰,如滚动条、点击效果(最常见的如水波纹点击效果)
     */
    public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
    
        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
    
                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }
    
                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }
    
            foreground.draw(canvas);
        }
    }
    

    上面这三个方法在ViewGroup类中均没有重写,说明无论是普通View亦或是ViewGroup,都是一样实现这三个过程的,所以,我们只需要看ViewGroup类的 dispatchDraw() 方法:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        ···
        final int childrenCount = mChildrenCount;
        ···
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                ···
            }
    
            ···
        }
        ···
    }
    

    这里只给出了遍历子View的部分,其他的内容在这里不需要关心。其实就是遍历每一个子View,然后调用 drawChild() 方法:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    

    这个方法就是调用 子View 的draw方法,将draw过程传递给子View,依次递归。

    这里还有一个细节问题需要注意:View类有一个 setWillNotDraw() 方法

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

    这个方法涉及到一个标记位 WILL_NOT_DRAW,设置这个标记位为 true 以后,表示当前View不需要绘制任何内容,系统会进行相应优化。

    1. 默认情况下,View没有启用这个标记位(false),而ViewGroup启用了(true);
    2. 当我们自定义控件继承 ViewGroup 类时,如果需要通过 onDraw() 方法绘制内容,就需要显式关闭这个标记位。

上一篇:Android学习笔记(五)| View的工作原理(上)
下一篇:Android学习笔记(七)| Android动画(上)—— View动画

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容