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()
目录
相关文章
|
12月前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
687 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
12月前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
224 0
|
12月前
|
Android开发 开发者
Android自定义view之利用drawArc方法实现动态效果
本文介绍了如何通过Android自定义View实现动态效果,重点使用`drawArc`方法完成圆弧动画。首先通过`onSizeChanged`进行测量,初始化画笔属性,设置圆弧相关参数。核心思路是不断改变圆弧扫过角度`sweepAngle`,并调用`invalidate()`刷新View以实现动态旋转效果。最后附上完整代码与效果图,帮助开发者快速理解并实践这一动画实现方式。
267 0
|
12月前
|
Android开发 数据安全/隐私保护 开发者
Android自定义view之模仿登录界面文本输入框(华为云APP)
本文介绍了一款自定义输入框的实现,包含静态效果、hint值浮动动画及功能扩展。通过组合多个控件完成界面布局,使用TranslateAnimation与AlphaAnimation实现hint文字上下浮动效果,支持密码加密解密显示、去除键盘回车空格输入、光标定位等功能。代码基于Android平台,提供完整源码与attrs配置,方便复用与定制。希望对开发者有所帮助。
238 0
|
12月前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
489 65
Android自定义view之网易云推荐歌单界面
|
12月前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
876 84
|
12月前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
320 3
|
12月前
|
Android开发 开发者
Android自定义view之围棋动画(化繁为简)
本文介绍了Android自定义View的动画实现,通过两个案例拓展动态效果。第一个案例基于`drawArc`方法实现单次动画,借助布尔值控制动画流程。第二个案例以围棋动画为例,从简单的小球直线运动到双向变速运动,最终实现循环动画效果。代码结构清晰,逻辑简明,展示了如何化繁为简实现复杂动画,帮助读者拓展动态效果设计思路。文末提供完整源码,适合初学者和进阶开发者学习参考。
219 0
Android自定义view之围棋动画(化繁为简)
|
12月前
|
Java Android开发 开发者
Android自定义view之围棋动画
本文详细介绍了在Android中自定义View实现围棋动画的过程。从测量宽高、绘制棋盘背景,到创建固定棋子及动态棋子,最后通过属性动画实现棋子的移动效果。文章还讲解了如何通过自定义属性调整棋子和棋盘的颜色及动画时长,并优化视觉效果,如添加渐变色让白子更明显。最终效果既可作为围棋动画展示,也可用作加载等待动画。代码完整,适合进阶开发者学习参考。
265 0
|
12月前
|
传感器 Android开发 开发者
Android自定义view之3D正方体
本文介绍了如何通过手势滑动操作实现3D正方体的旋转效果,基于Android自定义View中的GLSurfaceView。相较于使用传感器控制,本文改用事件分发机制(onTouchEvent)处理用户手势输入,调整3D正方体的角度。代码中详细展示了TouchSurfaceView的实现,包括触控逻辑、OpenGL ES绘制3D正方体的核心过程,以及生命周期管理。适合对Android 3D图形开发感兴趣的开发者学习参考。
240 0

热门文章

最新文章