自定义View(九)-View的工作原理- View的layout()和draw()

前言

上一节我们将View的测量流程理的差不多了,这篇我们来看下View的剩下的2大流程layout(布局)和draw(绘制)。相对测量来说,布局与绘制就简单了许多,所以我们将这的两大流程放在一起讲解。


performLayout()布局 由上上篇我们知道,布局是从ViewRootImpl#performLayout()发起的,那我们进入这个方法看一下:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        
        ......
        //标记开始当前布局
        mInLayout = true;//2062
        //将全局变量mView(DecorView)赋值给host
        final View host = mView;//2064
        
        ......
        
        //DecorView开始布局自己
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//2072

        //标志布局结束
        mInLayout = false;
    }        

首先我们知道host是就DecorView(FrameLayout),那我们进入ViewGroup看看,发现ViewGroup也是调用了它的父类(View)的layout方法,所以这里host.layout()就是调用了View#layout()。layout()方法的四个参数分别是当前View的左(left),上(top),右(right),下(bottom)。由上一篇我们知道,host.getMeasuredWidth(),host.getMeasuredHeight()就是屏幕的大小,所以从参数我们清楚的知道DecorView的四个顶点是从屏幕左上角到屏幕右下角,即整个屏幕。 那我们进入View#layout():

小提示:这里我们需要区分下测量的宽高与最终的宽高: 我们知道测量宽高和最后的宽高在多数情况下都是相等的,因为从上面我们知道,在layout的时候是调用的getMeasuredWidth()与getMeasuredHeight(),即:测量完成后的宽高作为参数来布局的。不过这是指大多数的情况下,如果你自定义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;
        }
        
        //保存上一次View的四个点的位置
        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);
        //在第一次或是位置改变时changed=true 条件成立视图View重新布局
        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);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

这里会首先通过setFrame方法来设置当前View的四个顶点的位置,即初始化mLeft,mTop,mBottom,mRight这四个值,这四个值一旦确定,那么当前View在父容器中的位置也就确定了。也就是说当setFrame()方法完成后,就基本上完成了当前View的布局。那我们来看下这个方法:

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

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

            mPrivateFlags |= PFLAG_HAS_BOUNDS;
             
            判断当前位置是否有变化
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            ......
            
        return changed;
    }

这个方法完成后,当前View的布局也就基本完成了,并且将最新的4个位置赋值给mLeft,mTop,mBottom,mRight。这里我们在来分析上面的小提示。来区分下测量宽高和最终宽高,其实比较这两个的不同就是比较getWidth()/getHeight与getMeasuredWidth()/getMeasuredHeight()。那我们来看下getWidth()/getHeight()方法:

public final int getWidth() {
        return mRight - mLeft;
    }
 public final int getHeight() {
        return mBottom - mTop;
    }

上面这4个值正是我们在layout布局中得到的(具体我们可知是在setFrame()方法中),那我们总结下两者的关系:

  1. 最终宽高的生成需要一般需要测量宽高作为参数。
  2. 测量宽高的生成比最终宽高的生成要早。
  3. 最终宽高是由layout来决定的,也就是View在父布局中显示的位置,通常情况下2着相同 (这里用到通常情况,因为在我重写layout时如果改变layout的参数,那么最终在父布局中显示的位置也会改变)

通过setFrame()方法完成了对自己的布局,那么onLayout()他的作用是什么呢?其实onLayout方法的用途是父容器确定子元素的位置。我们来看下View#onLayout():

   /**
     * 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) {
    }

可以发现这是一个空的方法,既然上面提到了他的作用是父容器确定子元素的位置。那我们就是容器所有的父类,View的直接子类ViewGroup去看一下:

  @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

可以发现他是一个抽象方法,那么就说明所有直接继承ViewGroup的容器都要实现这个方法。其实也容易理解,想想平时我们用到的LinearLayout,RelativeLayout都是直接继承ViewGroup的。很明显在使用的时候,在布局子View的时候位置使不用的。在回到开始处,是由host.layout()发起的布局,并且host就是我们的顶级View(DecorView),同时知道DecorView是继承FrameLayout的那么我进入FrameLayout#onLayout(),如下:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

这里直接调用了layoutChildren()方法,那么我们继续往下走:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
        
        //遍历所有FrameLayout下的ziView
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //判断当前子View的可见度。不过是GONE那么就不进行布局
            if (child.getVisibility() != GONE) {
                //获取子子View的getLayoutParams
                final LayoutParams lp = (getLayoutParams) child.getLayoutParams();
                获取子View的宽高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;
                //根据子View的LayoutParams来获取gravity的设置
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //下面都是根据gravity属性的设置来决定如何设置子View四个点的值
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                //设置完成子View的四个点的值传入子View的layout方法开始布局
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

通过代码和注释,我们了解到FrameLayout调用了这里直接调用了layoutChildren()方法,在这个方法中完成每个子View的布局。这个方法中通过对对齐方式和Margin的计算,来获得子View四个点的位置,最后调用child.layout()方法,如果是View就会走上面View的布局如果是ViewGrouop那么就和上面FrameLayout的布局逻辑一致(这里说的逻辑一致是因为直接继承ViewGroup的容器都会根据自己的特点重写onLayout()方法。比如LinearLayout和FrameLayout的布局方式是不同。但是最后都是会调用child.layout()方法,也就是逻辑都是一样的)

总结:

上面我们就完成了整个布局的绘制流程。下面我就对我们将到的知识点进行一下总结 通过整个layout(布局)我们可以总结如下:

  1. 直接继承ViewGroup的容器要重写onLayout方法,根据自己的特点,完成对子View的布局。
  2. 直接继承ViewGroup的容器要自己处理子View的Margin属性,否则会到时失效。
  3. 通过上面我们知道,在View设置可见度为GONE是不会布局。这个是为什么设置View.GONE不会占用布局的原因。
  4. 必须要在布局完成后才能获取到调用getHeight()和getWidth()方法获取到的View的宽高否则为0。

关于其他容器是如何重写onLayout()的大家可以自己看下。相信在理解上面的内容,妈妈就再也不用担心我不敢看源码啦~~!我们将我们的流程用流程图来表示,如下:

View树layout绘制流程.png

到此View的绘制也就完成了。下面我们来看下draw(绘制)。


performDraw()绘制 现在我们来看看View三大流程最后一个流程-->draw(绘制)。它的作用就是讲View绘制在屏幕上。我们还是来看下绘制发起的方法ViewRootImpl#performDraw():

private void performDraw() {

        ......
        
        try {
            draw(fullRedrawNeeded);//2337
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
       
       ......
    }

调用了draw()方法,我们继续走:

    private void draw(boolean fullRedrawNeeded) {
      
      ...... 
       
      if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {//2519
                    return;
                }
      ......    
    }

在draw()方法的最后调用了drawSoftware(),这个方法比较中重要。我们进入这个方法来看下:

/**
     * @return true if drawing was successful, false if an error occurred
     */
  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        final Canvas canvas;
        try {
            //从surface对象中获得canvas变量
            canvas = mSurface.lockCanvas(dirty);

            // If this bitmap's format includes an alpha channel, we
            // need to clear it before drawing so that the child will
            // properly re-composite its drawing on a transparent
            // background. This automatically respects the clip/dirty region
            // or
            // If we are applying an offset, we need to clear the area
            // where the offset doesn't appear to avoid having garbage
            // left in the blank areas.
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

           ......

            try {
                //调整画布的位置
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;
                //调用View类中的成员方法draw开始绘制View视图
                mView.draw(canvas);
            } 

        ......

        return true;
    }

从这个方法名字我们可以看出绘制成功返回true,失败返回false,说明绘制是在这里进行的。在这个方法中我们获得了画布canvas并将这个参数传递给 mView.draw(canvas);方法,我们在点进去看看:

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

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

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

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

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

这个方法有点长,但是很好理解。已进入方法就提示了绘制的过程遵循以下6个步骤:

  1. 绘制当前视图的背景。
  2. 保存当前画布的堆栈状态,并且在在当前画布上创建额外的图层,以便接下来可以用来绘制当前视图在滑动时的边框渐变效果。
  3. 绘制当前视图的内容。
  4. 绘制当前视图的子视图的内容。
  5. 绘制当前视图在滑动时的边框渐变效果。
  6. 绘制当前视图的滚动条。

在一般情况下2和5我们在自定义View时是不会去修改的。但是为了记录,还是简单讲解下。

1.绘制视图View的背景 通过上面我们知道绘制背景首先通过dirtyOpaque表示位来判断是否需要绘制背景,如果需要就到用drawBackground(canvas)方法。如下:

  /**
     * Draws the background onto the specified canvas.
     *
     * @param canvas Canvas on which to draw the background
     */
    private void drawBackground(Canvas canvas) {
        //获取背景drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //在绘制背景之前先设置背景的矩形大小
        setBackgroundBounds();

        ......
        
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //利用 background.draw(canvas);来绘制背景
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

这里的流程就是先回去背景,然后在绘制背景之前先设置背景的矩形大小,最后利用background.draw(canvas);方法来完成绘制背景。

2.保存画布canvas的边框参数 首先通过下面的获得当前视图View水平或者垂直方向是否需要绘制边框渐变效果

boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

如果不需要绘制边框的渐变效果,就无需执行上面的2,5了。那么就直接执行上面的3,4,6步骤。这里描述的就是我们的ListView滑动到最底端时,底部会有一个淡蓝色的半圆形的边框渐变背景效果。既然说要记录下我们就把所有过程都走一遍。

在标记第二步开始和结束的位置之间的这段代码用来检查是否需要保存参数canvas所描述的一块画布的堆栈状态,并且创建额外的图层来绘制当前视图在滑动时的边框渐变效果。视图的边框是绘制在内容区域的边界位置上的,而视图的内容区域是需要排除成员变量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom所描述的视图内边距的。此外,视图的边框有四个,分别位于视图的左、右、上以及下内边界上。因此,这段代码首先需要计算出当前视图的左、右、上以及下内边距的大小,以便得到边框所要绘制的区域。

3.绘制视图View的内容onDraw

第三步是调用onDraw()方法绘制内容。发现是一个空的方法,也就是说所有View继承View的控件都要重写这个方法来实现对自己内容的绘制。也很好理解,TextView绘制文本,ImageView绘制图片,控件他是什么属性就绘制什么样的内容。所以我们在自定义View的时候要重写onDraw()方法来完成自己的绘制。因为你想怎么实现什么样的效果View也不知道就只好给你一个空方法你自己去实现。

4.绘制当前视图的子视图的内容dispatchDraw()</font> 从这个方法的注释我们就知道,这是绘制子View的。我们知道之后ViewGroup才有可以有子视图,那么我进入ViewGroup#dispatchDraw()方法看下:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
       //判断当前ViewGroup容器是否设置的布局动画
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            //遍历给每个子视图View设置动画效果
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }
            //获得布局动画的控制器
            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }
            //开始布局动画
            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;
            //设置布局动画的监听事回调
            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }

        int clipSaveCount = 0;
        //是否需要剪裁边距  
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            clipSaveCount = canvas.save();
            //对当前视图的画布canvas进行边距裁剪,把不需要绘制内容的边距裁剪掉。
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }

        ......
        
        //遍历绘制当前视图的子视图View
        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) {
                    //将画布传入看是绘制子View    
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        while (transientIndex >= 0) {
            // there may be additional transient views after the normal views
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                break;
            }
        }
        if (preorderedList != null) preorderedList.clear();

        // Draw any disappearing views that have animations
        //当子视图设置了消失动画时,遍历绘制布局容器中需要消失的子视图
        if (mDisappearingChildren != null) {
            final ArrayList<View> disappearingChildren = mDisappearingChildren;
            final int disappearingCount = disappearingChildren.size() - 1;
            // Go backwards -- we may delete as animations finish
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        if (usingRenderNodeProperties) canvas.insertInorderBarrier();

        if (debugDraw()) {
            onDebugDraw(canvas);
        }

        if (clipToPadding) {
            canvas.restoreToCount(clipSaveCount);
        }

        // mGroupFlags might have been updated by drawChild()
        flags = mGroupFlags;

        if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
            invalidate(true);
        }

        if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
                mLayoutAnimationController.isDone() && !more) {
            // We want to erase the drawing cache and notify the listener after the
            // next frame is drawn because one extra invalidate() is caused by
            // drawChild() after the animation is over
            mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
            final Runnable end = new Runnable() {
               @Override
               public void run() {
                   notifyAnimationListener();
               }
            };
            post(end);
        }
    }

关于ViewGroup的dispatchDraw()方法里面的重要方法都留了下来并都有注释。大家可以在自己看看,结合自己的理解。通过遍历每个子View,并调用drawChild(canvas, child, drawingTime)方法来完成对子View的绘制。代码如下:

 /**
     * Draw one child of this View Group. This method is responsible for getting
     * the canvas in the right state. This includes clipping, translating so
     * that the child's scrolled origin is at 0, 0, and applying any animation
     * transformations.
     *
     * @param canvas The canvas on which to draw the child
     * @param child Who to draw
     * @param drawingTime The time at which draw is occurring
     * @return True if an invalidate() was issued
     */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

这里又会调用到了View#draw()方法。这样循环调用完成View的绘制。

5.<font color=#006400 size=3>绘制滑动时边框的渐变效果</font> 这部分我们就是我们上面提到的ListView滑动到最底端时,底部会有一个淡蓝色的半圆形的边框渐变背景效果。这部分通过4个判断并在判断内部完成4个变宽的渐变效果。基本都不会去修改,所以也不用太深入了解。

6.<font color=#006400 size=3>绘制滚动条(onDrawScrollBars)</font>

绘制滚动条的逻辑在onDrawScrollBars方法中,所以我们直接在onDrawForeground()方法中进入此方法,如下:

 protected final void onDrawScrollBars(Canvas canvas) {
        // scrollbars are drawn only when the animation is running
        //滚动条仅在动画运行时绘制
        final ScrollabilityCache cache = mScrollCache;
         //滚动条是否有缓存
        if (cache != null) {
            //获取滚动条的设置状态
            int state = cache.state;
            //滚动条不显示时,直接返回,也就是不绘制滚动条
            if (state == ScrollabilityCache.OFF) {                 //-----------(1)
                return;
            }

            boolean invalidate = false;
             //滚动条是否可见
            if (state == ScrollabilityCache.FADING) {                 //-----------(2)
                // We're fading -- get our fade interpolation
                if (cache.interpolatorValues == null) {
                    cache.interpolatorValues = new float[1];
                }

                float[] values = cache.interpolatorValues;

                // Stops the animation if we're done
                if (cache.scrollBarInterpolator.timeToValues(values) ==
                        Interpolator.Result.FREEZE_END) {
                    cache.state = ScrollabilityCache.OFF;
                } else {
                    cache.scrollBar.mutate().setAlpha(Math.round(values[0]));
                }

                // This will make the scroll bars inval themselves after
                // drawing. We only want this when we're fading so that
                // we prevent excessive redraws
                invalidate = true;
            } else {
                // We're just on -- but we may have been fading before so
                // reset alpha
                 //设置滚动条完全可见
                cache.scrollBar.mutate().setAlpha(255);
            }

            final boolean drawHorizontalScrollBar = isHorizontalScrollBarEnabled();
            final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
                    && !isVerticalScrollBarHidden();            //-----------(3)

            // Fork out the scroll bar drawing for round wearable devices.
            if (mRoundScrollbarRenderer != null) {
                if (drawVerticalScrollBar) {
                    final Rect bounds = cache.mScrollBarBounds;
                    getVerticalScrollBarBounds(bounds);
                    mRoundScrollbarRenderer.drawRoundScrollbars(
                            canvas, (float) cache.scrollBar.getAlpha() / 255f, bounds);
                    if (invalidate) {
                        invalidate();
                    }
                }
                // Do not draw horizontal scroll bars for round wearable devices.
            } else if (drawVerticalScrollBar || drawHorizontalScrollBar) {
                final ScrollBarDrawable scrollBar = cache.scrollBar;
                //绘制水平滚动条
                if (drawHorizontalScrollBar) {
                    scrollBar.setParameters(computeHorizontalScrollRange(),
                            computeHorizontalScrollOffset(),
                            computeHorizontalScrollExtent(), false);
                    final Rect bounds = cache.mScrollBarBounds;
                    getHorizontalScrollBarBounds(bounds);
                    onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                            bounds.right, bounds.bottom);
                    if (invalidate) {
                        invalidate(bounds);
                    }
                }
                //绘制垂直滚动条
                if (drawVerticalScrollBar) {
                    scrollBar.setParameters(computeVerticalScrollRange(),
                            computeVerticalScrollOffset(),
                            computeVerticalScrollExtent(), true);
                    final Rect bounds = cache.mScrollBarBounds;
                    getVerticalScrollBarBounds(bounds);
                    onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                            bounds.right, bounds.bottom);
                    if (invalidate) {
                        invalidate(bounds);
                    }
                }
            }
        }
    }

代码分析:

  • (1)处:判断是否需要绘制当前视图View的滚动条。如果你给当前视图View设置了android:scrollbars=”none”属性,时就不会绘制滚动条,也就是不显示滚动条。
  • (2)处:判断当前视图View的滚动条是否可消失。如果你给当前视图View设置了android:fadeScrollbars=”true”属性时,你不滑动,滚动条隐藏,你滑动时,滚动条显示,有代码可以看出,此处是通过改变滚动条的透明度来实现滚动条隐藏和显示的。
  • (3)处:当前视图View的滚动条设置成完全可见,也就是你设置了该属性android:fadeScrollbars=”false”。不管你是否滑动View,滚动条一直可见。
  • 身下部分就是绘制水平或者垂直滚动条的逻辑。

总结 :

到此View的draw绘制流程6步我们已经完全理清了。其实不是所有的我们都需要掌握其实在我们自定义View的时候只需要注意1,3,4,6即可而在这4个步骤中往往我们只是需要重写第三步或第四步而已。下面按照管理我们上流程图讲我们的流程串联起来,加深理解。如下:

View的绘制流程.png

View绘制6步分析.png

我们在来总结几个关于View绘制相关的知识点:

  • 父类View绘制主要是绘制背景,边框渐变效果,进度条,View具体的内容绘制调用了onDraw方法,通过该方法把View内容的绘制逻辑留给子类去实现。因此,我们在自定义View的时候都一般都需要重写父类的onDraw方法来实现View内容绘制。
  • onDraw,dispatchDraw区别
    • View还是ViewGroup对它们俩的调用顺序都是onDraw()->dispatchDraw()
    • 在ViewGroup中,当它有背景的时候就会调用onDraw()方法,否则就会跳过onDraw()直接调用dispatchDraw();所以如果要在ViewGroup中绘图时,往往是重写dispatchDraw()方法
    • 在View中,onDraw()和dispatchDraw()都会被调用的,所以我们无论把绘图代码放在onDraw()或者dispatchDraw()中都是可以得到效果的,但是由于dispatchDraw()的含义是绘制子控件,所以原则来上讲,在绘制View控件时,我们是重新onDraw()函数
    • 最后总结:在绘制View控件时,需要重写onDraw()函数,在绘制ViewGroup时,需要重写dispatchDraw()函数。
  • .不管任何情况,每一个View视图都会绘制 scrollBars滚动条,且绘制滚动条的逻辑是在父类View中实现,子类无需自己实现滚动条的绘制。其实TextView也是有滚动条的,可以通过代码让其显示滚动条和内容滚动效果。你只需在TextView布局设置android:scrollbars=”vertical”属性,同时在代码中进行如下设置:
textView.setMovementMethod(ScrollingMovementMethod.getInstance()); 
  • ViewGroup绘制的过程会对每个子视图View设置布局容器动画效果,如果你在ViewGroup容器布局里面设置了如下属性的话
android:animateLayoutChanges="true"

结语

至此View的三大流程就结束了。当然里面的只是不管我写的这些,但是我觉的这也应该是比较全了。不过自定义View是个熟能生巧的一个技术,光理解原理是不够的,但是不理解原理写起来出现问题就不好处理。希望大家在对完后自己去多看下源码,一遍不行就多看几遍把他变成自己的东西。对于菜鸟的写这些文章也不容易,只是希望能对入门android的小伙伴有些帮助。如果真的对您有帮助,就关注、评论下。我会更有动力。如果有问题留言,我知道的一定会回复你。最后希望大家都能成为大神。加油!!!

感谢

从ViewRootImpl类分析View绘制的流程

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏杨龙飞前端

css常用居中

2976
来自专栏Sorrower的专栏

界面无小事(五):自定义TextView

1033
来自专栏编程直播室

Canvas的HelloWorld文本的样式文本的测量总结

2606
来自专栏Android常用基础

自定义View(八)-View的工作原理- View的measure

从上一篇中。同Activity的布局加载了解了整个View树加载的流程。最后是通过View的三大流程来实现布局的显示的。那么我们这篇来讲下布局的三大流程之一--...

1401
来自专栏田超学前端

微信小程序开发:canvas 多行文字换行(二)

微信小程序开发中,canvas画出一篇文章,由于句子长短不一,画起来确实是费劲,查了不少资料,总结一下:

5037
来自专栏分享达人秀

TextView属性和方法大全

前面简单学习了一些Android UI的一些基础知识,那么接下来我们一起来详细学习Android的UI界面基本组件。 一、认识TextView 我们知道前面学习...

2325
来自专栏向治洪

react-native 之布局总结

前言 之前我们讲了很多react-native的基础控件,为了方便大家的理解,我们来对react-native的布局做一个总结,观看本节知识,你将看到。 宽度单...

6658
来自专栏Android干货

安卓开发小效果--走马灯

27312
来自专栏Android开发与分享

【React Native】react-native-scrollable-tab-view

56112
来自专栏我的博客

安卓开发之简单组件使用

一、TextView组件(文本框) <TextView android:id=”@+id/firstText” android:text=”第一行“ andro...

2786

扫码关注云+社区

领取腾讯云代金券