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

简介: 从源码的角度分析“绘制(draw)”。View绘制只决定绘制的顺序,具体绘制内容由各个子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) {
        // Step 1, draw the background, if needed
        //第一步:绘制背景
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

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

    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 {
    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 {
    // 绘制ViewGroup的一个孩子
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
}

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    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()
目录
相关文章
|
1月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
1月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
76 0
|
14天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
28 5
|
20天前
|
缓存 Java 数据库
Android的ANR原理
【10月更文挑战第18天】了解 ANR 的原理对于开发高质量的 Android 应用至关重要。通过合理的设计和优化,可以有效避免 ANR 的发生,提升应用的性能和用户体验。
48 8
|
22天前
|
缓存 数据处理 Android开发
在 Android 中使用 RxJava 更新 View
【10月更文挑战第20天】使用 RxJava 来更新 View 可以提供更优雅、更高效的解决方案。通过合理地运用操作符和订阅机制,我们能够轻松地处理异步数据并在主线程中进行 View 的更新。在实际应用中,需要根据具体情况进行灵活运用,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在 Android 中使用 RxJava 更新 View 的技巧和方法,为开发高质量的 Android 应用提供有力支持。
|
22天前
|
缓存 调度 Android开发
Android 在子线程更新 View
【10月更文挑战第21天】在 Android 开发中,虽然不能直接在子线程更新 View,但通过使用 Handler、AsyncTask 或 RxJava 等方法,可以实现子线程操作并在主线程更新 View 的目的。在实际应用中,需要根据具体情况选择合适的方法,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在子线程更新 View 的技巧和方法,为开发高质量的 Android 应用提供支持。
30 2
|
23天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
26天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
24 2
|
8天前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
1月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件基础与进阶
【10月更文挑战第5天】在Android应用开发中,自定义控件是提升用户体验和界面个性化的重要手段。本文将通过浅显易懂的语言和实例,引导你了解自定义控件的基本概念、创建流程以及高级应用技巧,帮助你在开发过程中更好地掌握自定义控件的使用和优化。
38 10