阅读 207

Android自定义控件 | View绘制原理(画什么?)

View绘制就好比画画,抛开Android概念,如果要画一张图,首先会想到哪几个基本问题:

  • 画多大?
  • 画在哪?
  • 怎么画?

Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分别藏在:

  • 测量(measure)
  • 定位(layout)
  • 绘制(draw)

这一篇将从源码的角度分析“绘制(draw)”。View绘制系统中的draw其实是讲的是绘制的顺序,至于具体画什么东西是各个子View自己决定的。

这是Android自定义控件系列文章的第三篇,系列文章目录如下:

  1. Android自定义控件 | View绘制原理(画多大?)
  2. Android自定义控件 | View绘制原理(画在哪?)
  3. Android自定义控件 | View绘制原理(画什么?)
  4. Android自定义控件 | 源码里有宝藏之自动换行控件
  5. Android自定义控件 | 小红点的三种实现(上)
  6. Android自定义控件 | 小红点的三种实现(下)
  7. Android自定义控件 | 小红点的三种实现(终结)

View.draw()

在分析View测量定位时,发现它们都是自顶向下进行地,即总是由父控件来触发子控件的测量或定位。不知道“绘制”是不是也是这样?,以View.draw()为切入点,一探究竟:

    public void draw(Canvas canvas) {
        /*
         * 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);

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

            // Step 7, draw the default focus highlight
            //第七步:绘制默认高亮
            drawDefaultFocusHighlight(canvas);

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

            // we’re done...
            return;
        }
    }
复制代码

这个方法实在太长了。。。还好有注释帮我们提炼了一条主线。注释说绘制一共有6个步骤,他们分别是:

  1. 绘制控件背景
  2. 保存画布层
  3. 绘制控件自身内容
  4. 绘制子控件
  5. 绘制褪色效果并恢复画布层(感觉这一步和第二步是对称的)
  6. 绘制装饰物

为啥提炼了主线后还是觉得好复杂。。。还好注释又帮我们省去了一些步骤,注释说“通常情况下第二步和第五步会跳过。”在剩下的步骤中有三个步骤最最重要:

  1. 绘制控件背景
  2. 绘制控件自身内容
  3. 绘制子控件

读到这里可以得出结论:View绘制顺序是先画背景(drawBackground()),再画自己(onDraw()),接着画孩子(dispatchDraw())。晚画的东西会盖在上面。

先看下drawBackground()

    /**
     * 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();
        ...
        //绘制Drawable
        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);
        }
    }
复制代码

背景是一张Drawable类型的图片,直接调用Drawable.draw()将其绘制在画布上。接着看下onDraw()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
}
复制代码

View.onDraw()是一个空实现。想想也对,View是一个基类,它只负责抽象出绘制的顺序,具体绘制什么由子类来决定,看一下ImageView.onDraw()

public class ImageView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ...
        //绘制drawable
        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            mDrawable.draw(canvas);
        } else {
            final int saveCount = canvas.getSaveCount();
            canvas.save();

            if (mCropToPadding) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
                canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
                        scrollX + mRight - mLeft - mPaddingRight,
                        scrollY + mBottom - mTop - mPaddingBottom);
            }

            canvas.translate(mPaddingLeft, mPaddingTop);

            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
}
复制代码

ImageView的绘制方法和View绘制背景一样,都是直接绘制Drawable

ViewGroup.dispatchDraw()

View.dispatchDraw()也是一个空实现,想想也对,View是叶子结点,它没有孩子:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }
}
复制代码

所以ViewGroup实现了dispatchDraw()

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        //当没有硬件加速时,使用预定义的绘制列表(根据z-order值升序排列所有子控件)
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        //自定义绘制顺序
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        //遍历所有子控件
        for (int i = 0; i < childrenCount; i++) {
            ...
            //如果没有自定义绘制顺序和预定义绘制列表,则按照索引i递增顺序遍历子控件
            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);
            }
        }
        ...
    }
    
    private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
        final int childIndex;
        if (customOrder) {
            final int childIndex1 = getChildDrawingOrder(childrenCount, i);
            if (childIndex1 >= childrenCount) {
                throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                        + "returned invalid index " + childIndex1
                        + " (child count is " + childrenCount + ")");
            }
            childIndex = childIndex1;
        } else {
            //1.如果没有自定义绘制顺序,遍历顺序和i递增顺序一样
            childIndex = i;
        }
        return childIndex;
    }
    
    private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
            int childIndex) {
        final View child;
        if (preorderedList != null) {
            child = preorderedList.get(childIndex);
            if (child == null) {
                throw new RuntimeException("Invalid preorderedList contained null child at index "
                        + childIndex);
            }
        } else {
            //2.如果没有预定义绘制列表,则按i递增顺序遍历子控件
            child = children[childIndex];
        }
        return child;
    }
    
}
复制代码

结合注释相信你一定看懂了:父控件会在dispatchDraw()中遍历所有子控件并触发其绘制自己。 而且还可以通过某种手段来自定义子控件的绘制顺序(对于本篇主题来说,这不重要)。

沿着调用链继续往下:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    /**
     * 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.
     * 绘制ViewGroup的一个孩子
     *
     * @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);
    }
}

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchDraw(canvas);
        } else {
            //绘制
            draw(canvas);
        }
        ...
    }
复制代码

ViewGroup.drawChild()最终会调用View.draw()。所以,View的绘制是自顶向下递归的过程,“递”表示父控件在ViewGroup.dispatchDraw()中遍历子控件并调用View.draw()触发其绘制自己,“归”表示所有子控件完成绘制后父控件继续后序绘制步骤`

总结

经过三篇文章的分析,对View绘制流程有了一个大概的了解:

  • View绘制流程就好比画画,它按先后顺序解决了三个问题 :
    1. 画多大?(测量measure)
    2. 画在哪?(定位layout)
    3. 怎么画?(绘制draw)
  • 测量、定位、绘制都是从View树的根结点开始自顶向下进行地,即都是由父控件驱动子控件进行地。父控件的测量在子控件件测量之后,但父控件的定位和绘制都在子控件之前。
  • 父控件测量过程中ViewGroup.onMeasure(),会遍历所有子控件并驱动它们测量自己View.measure()。父控件还会将父控件的布局要求与子控件的布局诉求相结合形成一个MeasureSpec对象传递给子控件以指导其测量自己。View.setMeasuredDimension()是测量过程的终点,它表示View大小有了确定值。
  • 父控件在完成自己定位之后,会调用ViewGroup.onLayout()遍历所有子控件并驱动它们定位自己View.layout()。子控件总是相对于父控件左上角定位。View.setFrame()是定位过程的终点,它表示视图矩形区域以及相对于父控件的位置已经确定。
  • 控件按照绘制背景,绘制自身,绘制孩子的顺序进行。父控件在完成绘制自身之后,会调用ViewGroup.dispatchDraw()遍历所有子控件并驱动他们绘制自己View.draw()
关注下面的标签,发现更多相似文章
评论