View绘制Draw过程

一 概述

我们知道 Android 视图的绘制流程分为:Measure,Layout 和 Draw,之前我们已经分析了:Measure 和 Layout,他们最主要的任务是:确定 View/ViewGroup 可绘制的矩形区域。接下来我们分析 Draw 是如何在这给定的区域内绘制想要的图形。

二 为什么要自定义View

Android 提供了关于 View 最基础的两个类:

ViewGroup 与 View

然而 ViewGroup 并没有约定其内部的子 View 是如何布局的,是叠加在一起呢?还是横向摆放、纵向摆放等。同样的 View 也没有约定其展示的内容是啥样,是矩形、圆形、三角形、一张图片、一段文字抑或是不规则的形状?这些都要我们自己去实现吗?

不尽然,值得高兴的是 Android 已经考虑到上述需求了,为了开发方便已经预制了一些常用的 ViewGroup、View,比如:

继承自 ViewGroup 的子类

  • FrameLayout --> 里面的子 View 是层叠摆放的
  • LinearLayout -->里边的子 View 是可以纵向/横向排列的
  • RelativeLayout -->里边的子 View 可以相对布局
  • RecyclerView -->里边的子 View 以列表形式展示

等等…

继承自 View 的子类

  • TextView --> 用于绘制一段文本
  • ImageView --> 用于绘制一张图片
  • EditText -->用于绘制输入框
  • Button --> 用户绘制按钮

等等…

虽然以上衍生的 View/ViewGroup 子类已经大大为我们提供了便利,但也仅仅是通用场景下的通用控件,我们想实现一些较为复杂的效果,比如波浪形状进度条、会发光的球体等,这些系统控件就无能为力了,也没必要去预制千奇百怪的控件。想要达到此效果,我们需要自定义 View/ViewGroup。

通常来说自定义 View/ViewGroup 有以下几种:

  • 如果你觉得系统提供的 ViewGroup 子类基本符合你需求,但你想将一些功能封装到一个组件里,那么就直接继承 FrameLayout、LinearLayout 等。这样一来,继承了他们的特性,也将自己的逻辑封装了。
  • 如果你觉得系统提供的 View 子类基本符合你的需求,但你想将一些功能封装到一个控件里,比如显示 Emoji,那么直接继承自 TextView (AppCompatTextView 兼容)。
  • 如果你看不起系统预制的 ViewGroup 子类,直接继承自 ViewGroup,那么你需要重写 onMeasure()、onLayout() 等方法。
  • 如果不想用系统预制的 View 子类,直接继承自 View,那么你需要自己绘制内容,重写 onDraw() 方法。

第三种情况一般不怎么用,除非布局比较特殊。其它情况是我们常用的手段,对于我们通常来说的 “自定义View” 一般指的是第四种情况。

三  View Draw过程

来看看 View 默认的 onDraw() 方法:

View.java

    protected void onDraw(Canvas canvas) {
    }

发现是个空实现,因此继承自 View 的类必须重写 onDraw() 方法才能实现绘制。该方法传入参数为:Canvas 类型,Canvas 翻译过来一般叫做画布,在重写的 onDraw() 里拿到 Canvas 对象后,有了画布我们还需要一支笔,这只笔即为 Paint,翻译过来一般称作画笔。两者结合,就可以愉快的作画 (绘制) 了.

你可能发现了,在 Demo 里调用

canvas.drawColor(Color.RED);

并没有传入 Paint 啊,是不是 Paint 不是必须的?实际上调用该方法后,底层会自动生成 Paint 对象。

SkCanvas.cpp

void SkCanvas::drawColor(SkColor c, SkBlendMode mode) {
    SkPaint paint;
    paint.setColor(c);
    paint.setBlendMode(mode);
    this->drawPaint(paint);
}

可以看到,底层初始化了 Paint,并且给其设置的颜色为在 Java 层设置的颜色。

onDraw() 比较简单,开局一个 Canvas,效果全靠画。试想,这个 Canvas 怎么来的呢,换句话说是谁调用了 onDraw()。发挥一下联想功能,在 Measure、Layout 过程有提到过两者套路很像:

  • measure()、layout() 一般不需要重写
  • onMeasure()、onLayout() 需要重写
  • measure() 里调用了 onMeasure()
  • layout() 里调用了 onLayout()

那么 Draw 过程是否也是如此套路呢?看见了 onDraw(),那么 draw() 还远吗?没错,他们是一样的套路。我们来看 draw() 方法,其中的主要的步骤如下:

  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)

View.java

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        //打上已绘制标记
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        int saveCount;
        //第一步 绘制背景
        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;
        
        //条件分支A
        if (!verticalEdges && !horizontalEdges) {
            //第三步 调用onDraw(),绘制View内容 (1)
            onDraw(canvas);

            //第四步 分发Draw,绘制子布局(2)
            dispatchDraw(canvas);
            //绘制自动填充的高亮(默认不会绘制)
            drawAutofilledHighlight(canvas);

            //mOverlay 绘制在内容之上,在前景色之下 (3)
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            //第六步,绘制装饰,如前景、滚动条等 (4)
            onDrawForeground(canvas);

            //第七步,绘制默认高亮,在touch mode模式基本不生效
            drawDefaultFocusHighlight(canvas);
            //调试用的,可以忽略
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            //绘制完成,直接返回
            return;
        }
        ......
        //条件分支B
        //下面还有一大堆源码,主要就是做了一件事:绘制边缘渐变
        //大部分情况下都不会走到这
        //绘制步骤大体分为 七 个步骤,而上面只列出了1、3、4、6、7,剩下的步骤在此完成
        //如果设置了边缘渐变,那么绘制步骤就会比不设置时多两个步骤,多出来的步骤是:2、5
        //用文字简单概述一下
        //1--->绘制背景
        //2--->canvas.getSaveCount(); 记录canvas状态,为绘制边缘渐变做准备(canvas坐标要改变,因此先保存)
        //3--->绘制内容
        //4--->分发Draw,绘制子布局
        //5--->绘制边缘渐变
        //6--->绘制装饰
        //7--->绘制默认高亮
    }

可以看出,draw() 主要分为两个部分:

  • 条件分支 A–> 大部分情况下都会走该分支
  • 条件分支 B—> 极小部分情况会走该分支
  • B 分支比 A 分支多了个2个步骤,目的是为了绘制边缘渐变效果

不管是 A 分支还是 B 分支,都进行了好几步的绘制。通常来说,单一一个 View 的层次分为:

  • 背景–>内容–>前景

后面绘制的可能会遮挡前边绘制的。对于一个 ViewGroup 来说,层次分为:

  • 背景–>内容–>子布局的层次–>前景

来看看 A 分支标注的重点步骤:

onDraw(canvas)

前面分析过,对于单一的 View,onDraw() 是空实现,需要由我们自定义绘制。而对于 ViewGroup,也并没有具体实现,如果在自定义 ViewGroup 里重写 onDraw(),它会执行吗?默认是不会执行的,相关分析请移步:
Android ViewGroup onDraw为什么没调用

dispatchDraw(canvas)
 

    protected void dispatchDraw(Canvas canvas) {

    }

发现是个空实现,再看看 ViewGroup.java 里的实现:

    protected void dispatchDraw(Canvas canvas) {
        ......
        //遍历子布局,发起 Draw 过程
        ......
    }

也即是说,对于单一 View,因为没有子布局,因此没必要再分发 Draw,而对于 ViewGroup 来说,需要触发其子布局发起 Draw 过程(此过程后续分析),可以类比事件分发过程 View、ViewGroup 的处理。

OverLay

顾名思义就是"盖在某个东西上面",此处是在绘制内容之后,绘制前景之前。怎么用呢?

        View viewGroup = findViewById(R.id.myviewgroup);
        //给 overLay 指定一个 Drawable
        Drawable drawable = ContextCompat.getDrawable(this, R.drawable.shapeme);
        //设置 Drawable 的尺寸
        drawable.setBounds(0, 0, 400, 58);
        //为 overLay 添加 Drawable
        viewGroup.getOverlay().add(drawable);

以上是给一个 ViewGroup 设置 overLay,效果如下:

黑色部分为 ViewGroup 背景,红色矩形+黄色圆圈为子布局,黄色矩形即为为 ViewGroup 添加的 overLay,可以看出 overLay 绘制在内容之上。

onDrawForeground()

绘制前景,使用方法如下:

        View viewGroup = findViewById(R.id.myviewgroup);
        Drawable drawable = ContextCompat.getDrawable(this, R.drawable.shapeme);
        drawable.setBounds(0, 0, 400, 58);
        viewGroup.setForeground(drawable);

 你可能发现了,这和设置 overLay 差不多的嘛,实际还是有差别的。在 onDrawForeground() 里会重新调整 Drawable 的尺寸,该尺寸与 View 大小一致,之前给 Drawable 设置的尺寸会失效。运行效果如下:


可以看出,ViewGroup 都被前景盖住了。

以上是分支 A 的主要流程。

四 ViewGroup Draw过程

对于 View 里的 onDraw()、draw(),ViewGroup 里并没有重写。而对于 dispatchDraw(),在 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;
        //动画相关
        ......
        int clipSaveCount = 0;
        //设置了 padding 后,绘制的子布局不能超过 padding (1)
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            //因此需要对canvas坐标进行变换,先保存其状态
            clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }

        //重置相关标记
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
        ......
        for (int i = 0; i < childrenCount; 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); (2)
            }
        }
        ......
    }

来看看标记的2点:

设置 padding

目的是为了让子布局留出一定的空隙出来,因此当设置了 padding 后,子布局的 canvas 需要根据 padding 进行裁减。判断标记为:

(flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK

protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;

FLAG_CLIP_TO_PADDING 默认设置为 true,FLAG_PADDING_NOT_NULL 只要有 padding 不为 0,该标记就会打上,也就是说:只要设置了padding 不为 0,子布局显示区域需要裁减。那么能不能不让子布局裁减显示区域呢?答案是可以的。

考虑到一种场景:使用 RecyclerView 的时候,我们需要设置 paddingTop = 20px,效果是:RecyclerView Item 展示时离顶部有 20px,但是滚动的时候永远滚不到顶部,看起来不是那么友好。这就是上述的裁减起作用了,需要将此动作禁止。通过设置:

setClipToPadding(false)

当然也可以在xml里设置:

android:clipToPadding="false"

drawChild()

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

从方法名上看是调用子布局进行绘制。child.draw() 里分两种情况:

  • 硬件加速绘制
  • 软件绘制

这两者具体作用与区别会在下篇文章分析,不管是硬件加速绘制还是软件绘制,最终都会调用 View.draw() 方法,该方法上面已经分析过。注意,draw(x1,x2,x3) 与 draw (xx) 并不一样,参数不一样,两个不同的方法,不要搞混了。

五 View/ViewGroup 常用方法分析

用图表示:

 

一般来说,我们通常会自定义 View,并且重写其 onDraw() 方法,有没有绘制内容的 ViewGroup 需求呢?是有的,举个例子,大家可以去看看 RecyclerView ItemDecoration 的绘制,其中运用到了 ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx) 绘制的先后顺序来实现分割线,分组头部悬停等功能。
 

 

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值