Drawable绘制过程源码分析和自定义Drawable实现动画

1. 概述

对于Drawable,相信大家都不陌生,而且用起来非常方便。在Android中Drawable代表可以在canvas上被绘制的一般抽象,与View或者其子类相比,不需要进行measure、layout操作,仅仅只需要进行draw操作。

除了简单的draw操作之外,Drawable还提供了许多通用的机制,以便与正在绘制的图形进行交互:
1> 为了告诉Drawable绘制的位置以及它的大小,setBounds方法必须在绘制之前被调用。通常可以通过getIntrinsicHeight和getIntrinsicWidth方法找到某些Drawable的首选大小,比如BitmapDrawable通过getIntrinsicHeight和getIntrinsicWidth方法获取bitmap的高和宽。

2> getPadding方法可以从某些Drawables返回关于如何放置其内容的信息。 例如,一个想要作为button widget的Drawable将需要返回将label正确放置在其内部的padding;LayerDrawable通过子Drawable的getPadding方法获取子Drawable的padding,从而进行padding的积累,后面会讲解LayerDrawable 中padding的积累导致的设计缺陷。

3> setState方法可以告知Drawable在哪个状态下绘制,例如“focus”,“selected”等。一些drawables可以根据所选状态修改其图像,比如StateListDrawable,关于StateListDrawable的使用可以参考Drawable Resources与Color State List Resource 中的StateListDrawable部分,如果想要知道StateListDrawable中是如何根据状态绘制的,可以参考StateListDrawable 类的onStateChange方法。

4> setLevel方法允许提供单个连续控制器来修改Drawable的显示,例如电池电量。 一些drawables可以根据当前level修改其图像,比如LevelListDrawable,关于LevelListDrawable的使用可以参考可绘制对象资源,如果想要知道LevelListDrawable中是如何根据level绘制的,可以参考LevelListDrawable类的onLevelChange方法。

5> Drawable可以通过Drawable.Callback接口回调其client来执行动画。 所有client都应该支持此interface(通过setCallback),以便动画可以正常工作。 一个简单的方法是通过系统设施,如android.view.View#setBackground(Drawable)和 android.widget.ImageView。在下面自定义Drawable的讲解中会详细介绍通过Drawable.Callback怎么实现动画。

从API 24开始,自定义的Drawable也可以在XML中使用,考虑到兼容性的问题,该特性目前并没有什么用,希望以后Google可以将该特性兼容到低版本。

2. 预备知识

2.1 Paint中setPathEffect(PathEffect effect)方法的使用

顾名思义,PathEffect是用来对Paint绘制的Path设置样式的,PathEffect本身并没有具体实现,我们通常使用其子类:



接下来就来看看这六个子类的具体效果:



上图中第一条曲线是原始的Path,从第二条开始依次使用了DashPathEffect、CornerPathEffect、DiscretePathEffect、PathDashPathEffect、ComposePathEffect和SumPathEffect。
实现代码如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test_path);
    pathView = findViewById(R.id.view_path);
    dashPathEffectView = findViewById(R.id.view_dash_path_effect);
    cornerPathEffectView = findViewById(R.id.view_corner_path_effect);
    discretePathEffectView = findViewById(R.id.view_discrete_path_effect);
    pathDashPathEffectView = findViewById(R.id.view_path_dash_path_effect);
    composePathEffectView = findViewById(R.id.view_compose_path_effect);
    sumPathEffectView = findViewById(R.id.view_sum_path_effect);

    Path path = new Path();
    path.moveTo(50, 30);
    path.lineTo(200, 230);
    path.lineTo(400, 30);
    path.lineTo(600, 230);
    path.lineTo(800, 30);
    path.lineTo(1000, 230);

    float dashGap = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
    float dashWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
    DashPathEffect dashPathEffect = new DashPathEffect(new float[]{dashGap, dashWidth}, 0);
    CornerPathEffect cornerPathEffect = new CornerPathEffect(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
    DiscretePathEffect discretePathEffect = new DiscretePathEffect(3, 5);
    PathDashPathEffect pathDashPathEffect = new PathDashPathEffect(
            makeConvexArrow(24.0f, 14.0f),    // "stamp"
            36.0f,                            // advance, or distance between two stamps
            0.0f,                             // phase, or offset before the first stamp
            PathDashPathEffect.Style.ROTATE); // how to transform each stamp
    ComposePathEffect composePathEffect = new ComposePathEffect(dashPathEffect, cornerPathEffect);
    SumPathEffect sumPathEffect = new SumPathEffect(cornerPathEffect, dashPathEffect);

    TrackShape trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
    TrackShapeDrawable trackShapeDrawable = new TrackShapeDrawable(trackShape);
    pathView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), dashPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    dashPathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), cornerPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    cornerPathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), discretePathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    discretePathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), pathDashPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    pathDashPathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), composePathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    composePathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), sumPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    sumPathEffectView.setBackground(trackShapeDrawable);
}

private Path makeConvexArrow(float length, float height) {
    Path p = new Path();
    p.moveTo(0.0f, -height / 2.0f);
    p.lineTo(length - height / 4.0f, -height / 2.0f);
    p.lineTo(length, 0.0f);
    p.lineTo(length - height / 4.0f, height / 2.0f);
    p.lineTo(0.0f, height / 2.0f);
    p.lineTo(0.0f + height / 4.0f, 0.0f);
    p.close();
    return p;
}

上面代码中的TrackShapeDrawable和TrackShape在下面的自定义Drawable中会详细讲解,最终PathEffect会通过paint.setPathEffect(pathEffect);方式进行应用。
下面我们来分析一下PathEffect六个子类的使用方法:

DashPathEffect -- 用来实现Path的虚线效果
构造方法签名如下:
public DashPathEffect(float intervals[], float phase) 
虚线是由一系列的线段组成,intervals数组长度必须是大于等于2的偶数,偶数索引指定线段长度,而奇数索引指定线段之间间隔长度。
例如 PathEffect effects = new DashPathEffect(new float[] { 1, 2, 4, 8}, 1);表示
绘制长度1的线段,再绘制长度2的空白,再绘制长度4的线段,再绘制长度8的空白,依次重复。
phase参数表示起始位置的偏移量(通过这个值可以实现虚线滚动的动画)

CornerPathEffect -- 用来实现Path拐角处的圆角效果
构造方法签名如下:
public CornerPathEffect(float radius)
1. radius 圆角的半径

DiscretePathEffect -- 用来实现Path的离散效果
构造方法签名如下:
public DiscretePathEffect(float segmentLength, float deviation)
将Path分割成长度为segmentLength的片段,然后在原始Path的基础上发生deviation大小的随机偏离

PathDashPathEffect -- 用来实现Path的虚线效果(虚线中每一个线段都用Path对应的图形代替)
构造方法签名如下:
public PathDashPathEffect(Path shape, float advance, float phase, Style style)
1. shape参数:虚线中每一个线段对应的Path
2. advance参数:每一个shape之间的间距
3. phase参数:起始位置的偏移量(通过这个值可以实现虚线滚动的动画)
4. style参数:代表每一个shape之间是如何衔接的,一共有如下三种类型:
             TRANSLATE  平移的方式
             ROTATE         旋转的方式
             MORPH         变形的方式
   上面三种方式的具体效果,大家有兴趣可以尝试一下

ComposePathEffect、SumPathEffect -- 用来将上面提到的PathEffect进行组合,然后作用于Path上
它们之间不同在于组合的方式,ComposePathEffect(PathEffect outerpe, PathEffect innerpe)会先将
innerpe的效果应用到Path上,然后再将outerpe的效果应用到 应用了innerpe效果的Path 上,
即outer(inner(path));而SumPathEffect(PathEffect first, PathEffect second)则会依次应用
两个效果到Path上,即 first(path) + second(path)。

2.2 Paint中setShader(Shader shader)方法的使用

顾名思义,Paint绘制任何对象(除了bitmap)时都是通过Shader得到color值,Shader本身并没有具体实现,我们通常使用其子类:



在讲解这五个子类之前,必须先了解一下Shader和Shader中的TileMode:

Shader:前面说过Paint绘制任何对象(除了bitmap)时都是通过Shader得到color值,
Shader获取指定坐标的color值是基于View布局区域(屏幕区域中排出状态栏区域和应用标题栏区域后
剩余的区域)对应的坐标系。

Shader的原始区域:也就是Shader子类(除了BitmapShader)的渐变区域,
BitmapShader的原始区域就是第一张bitmap绘制的区域。

CLAMP : 如果绘制区域超出Shader的原始区域,则通过复制边缘颜色来填充超过的区域。

REPEAT : 如果绘制区域超出Shader的原始区域,则通过在水平和垂直方向重复Shader的原始区域
来填充超过的区域。

MIRROR : 和REPEAT类似,不过是以镜像的方式重复。

注意:
1 BitmapShader可以在水平和垂直方向上同时设置TileMode,那么Shader会先在垂直方向上进行
TileMode对应的操作,然后再在水平方向上进行TileMode对应的操作。
2 绘制区域是不可能超过SweepGradient的原始区域。
3 LinearGradient和RadialGradient只在一个方向上进行渐变,因此应用一个TileMode就足够了。
4 在创建Shader子类的实例时,TileMode不可以为null,否则会报NullPointerException异常。

原理讲了这么多,接下来我们就通过例子来加以验证:

1. BitmapShader

水平和垂直方向上的TileMode都是CLAMP时:



上图中左上角的图片就是BitmapShader的原始区域。

实现代码如下

public class BitmapShaderView extends View {

    private Paint mPaint;

    public BitmapShaderView(Context context) {
        super(context);
    }

    public BitmapShaderView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BitmapShaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bitmap_shader);
        mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(0, 0, 1000, 1800, mPaint);
    }
}

结合运行截图和代码可以证明:
对于CLAMP,如果绘制区域超出Shader的原始区域,则通过复制边缘颜色来填充超过的区域。

水平和垂直方向上的TileMode分别是CLAMP,MIRROR:



上图中左上角的图片就是BitmapShader的原始区域。

实现代码和上面基本相同,不同的地方如下:

mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR));

结合运行截图和代码可以证明:
1> 对于MIRROR,如果绘制区域超出Shader的原始区域,则通过在水平和垂直方向以镜像的方式重复Shader的原始区域来填充超过的区域。
2> BitmapShader可以在水平和垂直方向上同时设置TileMode,那么Shader会先在垂直方向上进行TileMode对应的操作,然后再在水平方向上进行TileMode对应的操作

水平和垂直方向上的TileMode都是REPEAT:



上图中左上角的图片就是BitmapShader的原始区域。

实现代码和上面基本相同,不同的地方如下:

mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));

结合运行截图和代码可以证明:
对于REPEAT,如果绘制区域超出Shader的原始区域,则通过在水平和垂直方向重复Shader的原始区域来填充超过的区域。

2. LinearGradient 线性渐变

上图第二条倾斜的渐变区域就是LinearGradient的原始区域。
实现代码与BitmapShader中的基本相同,不同的地方如下:

mPaint.setShader(new LinearGradient(200, 200, 400, 400, new int[]{Color.RED, Color.YELLOW}, null, Shader.TileMode.REPEAT));

代码中LinearGradient的前四个参数决定了LinearGradient的原始区域为上图第二条倾斜的渐变区域,第五个参数代表渐变的颜色值,关于最后连个参数,有兴趣的同学可以自己研究一下。

3. RadialGradient 径向渐变

上图最小的圆区域就是RadialGradient的原始区域。
实现代码和上面基本相同,不同的地方如下:

mPaint.setShader(new RadialGradient(400, 400, 100, new int[]{Color.RED, Color.YELLOW}, null, Shader.TileMode.REPEAT));

代码中RadialGradient的前三个参数决定了RadialGradient的原始区域为上图最小的圆区域,第四个参数代表渐变的颜色值,关于最后连个参数,有兴趣的同学可以自己研究一下。

4. SweepGradient 梯度渐变

实现代码和上面基本相同,不同的地方如下:

mPaint.setShader(new SweepGradient(400, 400, new int[]{Color.RED, Color.YELLOW}, null));

代码中SweepGradient的前两个参数决定了SweepGradient的原始区域为无限大区域(从而证明了 绘制区域是不可能超过SweepGradient的原始区域),第四个参数代表渐变的颜色值,关于最后一个参数,有兴趣的同学可以自己研究一下。

5. ComposeShader

顾名思义,ComposeShader是用来组合使用上面的任意两种Shader。



实现代码和上面基本相同,不同的地方如下:

mPaint.setShader(new ComposeShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT),
        new LinearGradient(200, 200, 400, 400, new int[]{Color.RED, Color.TRANSPARENT}, null, Shader.TileMode.REPEAT), PorterDuff.Mode.SRC_OVER));

代码中ComposeShader的前两个参数代表两个被组合的Shader,第三个参数代表组合的方式,首先我们通过下图直观的看一下所有组合的方式:



代码中ComposeShader的第一个参数对应上图中的Des,第二个参数对应上图中的Src,讲到这里相信大家就应该可以看的懂上面的代码了。

讲到这里,大家有没有这样的疑问:怎么对通过Shader绘制的图形进行缩放、平移、旋转和错切操作呢?那么这是就要用到Shader的setLocalMatrix方法了,使用方式和下面2.3中讲到的用法一样,这里就不再赘叙了。

2.3 Path中transform方法的使用

Path中的transform方法是实现Path的scale、rotate、translate和skew动画的关键,下面举个例子来说明一下transform方法是如何使用的,首先看一下运行截图:



实现代码如下:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test_path_transform);
    // 用来显示坐标轴的
    coordinateView = findViewById(R.id.view_coordinate);
    // 用来显示左上角的矩形
    testPathTransformView = findViewById(R.id.view_test_path);
    // 用来显示右上角的矩形
    testPathScaleView = findViewById(R.id.view_test_path_scale);
    // 用来显示左下角的矩形
    testPathRotateView = findViewById(R.id.view_test_path_rotate);
    // 用来显示右下角的矩形
    testPathSkewView = findViewById(R.id.view_test_path_skew);

    // 构建坐标轴对应的Path
    Path coordinate = new Path();
    coordinate.moveTo(0, 100);
    coordinate.lineTo(1000, 100);
    coordinate.lineTo(980, 80);
    coordinate.moveTo(1000, 100);
    coordinate.lineTo(980, 120);
    coordinate.moveTo(100, 0);
    coordinate.lineTo(100, 1000);
    coordinate.lineTo(80, 980);
    coordinate.moveTo(100, 1000);
    coordinate.lineTo(120, 980);
    TrackShape coordinateShape = new TrackShape(coordinate, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()));
    TrackShapeDrawable coordinateShapeDrawable = new TrackShapeDrawable(coordinateShape);
    coordinateView.setBackground(coordinateShapeDrawable);

    // 构建左上角矩形对应的Path
    Path rectPath = new Path();
    rectPath.addRect(new RectF(200, 200, 400, 400), Path.Direction.CW);
    TrackShape rectTrackShape = new TrackShape(rectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable rectShapeDrawable = new TrackShapeDrawable(rectTrackShape);
    rectShapeDrawable.getPaint().setShader(new SweepGradient(300, 300, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathTransformView.setBackground(rectShapeDrawable);

    Matrix translateMatrix1 = new Matrix();
    translateMatrix1.setTranslate(400, 0);
    Path scaleRectPath = new Path();
    // 将左上角矩形对应的Path向右平移400个像素
    rectPath.transform(translateMatrix1, scaleRectPath);
    Matrix scaleMatrix = new Matrix();
    scaleMatrix.setScale(0.5f, 0.5f, 700, 300);
    // 将平移后得到的Path相对于矩形的中心点缩小一半
    scaleRectPath.transform(scaleMatrix);
    TrackShape scaleRectTrackShape = new TrackShape(scaleRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable scaleRectShapeDrawable = new TrackShapeDrawable(scaleRectTrackShape);
    scaleRectShapeDrawable.getPaint().setShader(new SweepGradient(700, 300, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathScaleView.setBackground(scaleRectShapeDrawable);

    Matrix translateMatrix2 = new Matrix();
    translateMatrix2.setTranslate(0, 400);
    Path rotateRectPath = new Path();
    // 将左上角矩形对应的Path向下平移400个像素
    rectPath.transform(translateMatrix2, rotateRectPath);
    Matrix rotateMatrix = new Matrix();
    rotateMatrix.setRotate(60, 300, 700);
    // 将平移后得到的Path相对于矩形的中心点旋转180度
    rotateRectPath.transform(rotateMatrix);
    TrackShape rotateRectTrackShape = new TrackShape(rotateRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable rotateRectShapeDrawable = new TrackShapeDrawable(rotateRectTrackShape);
    rotateRectShapeDrawable.getPaint().setShader(new SweepGradient(300, 700, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathRotateView.setBackground(rotateRectShapeDrawable);

    Matrix translateMatrix3 = new Matrix();
    translateMatrix3.setTranslate(400, 400);
    Path skewRectPath = new Path();
    // 将左上角矩形对应的Path向上向下分别平移400个像素
    rectPath.transform(translateMatrix3, skewRectPath);
    Matrix skewMatrix = new Matrix();
    skewMatrix.setSkew(-0.5f, 0, 700, 700);
    // 将平移后得到的Path相对于矩形的中心点在水平方向上错切矩形宽度的一半
    skewRectPath.transform(skewMatrix);
    TrackShape skewRectTrackShape = new TrackShape(skewRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable skewRectShapeDrawable = new TrackShapeDrawable(skewRectTrackShape);
    skewRectShapeDrawable.getPaint().setShader(new SweepGradient(700, 700, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathSkewView.setBackground(skewRectShapeDrawable);
}

activity_test_path_transform.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/view_coordinate"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path_scale"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path_rotate"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path_skew"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

上面的注释非常清晰了,这里就不再赘叙了。上面代码中的TrackShapeDrawable和TrackShape在下面的自定义Drawable中会详细讲解。

3. Drawable绘制过程源码分析

前面已经讲到过,Drawable与View或者其子类相比,不需要进行measure、layout操作,仅仅只需要进行draw操作,因此我们只要明白Drawable的绘制过程,基本上就可以实现自定义Drawable。Drawable是用来在View上显示的,因此View的绘制过程中就会绘制View上要显示的Drawable(即调用Drawable的onDraw方法)。在前面的一篇博客Android View的测量、布局、绘制流程源码分析及自定义View实例演示中讲解了View的测量、布局、绘制流程,希望有兴趣的同学可以看看,下面我就直接从View的drawBackground方法(该方法绘制View的绘制过程中被调用)入手研究Drawable的绘制过程,顾名思义,drawBackground方法是用来绘制View的背景的,首先我们先看一下绘制View的背景时序图:

View背景绘制时序图

上图表明绘制View的背景的任务主要分为两部分:
1> 设置View的背景对应的Drawable的大小,即设置Drawable绘制区域的大小 (上图中的1、2、3、4步)
2> 在画布上绘制View的背景对应的Drawable (上图中的5步)
首先看一下上图中第1步的drawBackground方法的源码:

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 设置View的背景对应的Drawable的大小,即设置Drawable绘制区域的大小
    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null
            && mAttachInfo.mHardwareRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }

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

上面的注释很清楚,就不做赘叙了。
接着我们来看上图中第2步中setBackgroundBounds方法的源码:

void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        // 将View的背景对应的Drawable的大小设置为View的大小,即设置Drawable绘制区域的大小 
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

上面的注释很清楚,就不做赘叙了。
接着我们来看看第3步中的源码:

/**
 * Specify a bounding rectangle for the Drawable. This is where the drawable
 * will draw when its draw() method is called.
 */
public void setBounds(int left, int top, int right, int bottom) {
    Rect oldBounds = mBounds;

    if (oldBounds == ZERO_BOUNDS_RECT) {
        oldBounds = mBounds = new Rect();
    }

    if (oldBounds.left != left || oldBounds.top != top ||
            oldBounds.right != right || oldBounds.bottom != bottom) {
        if (!oldBounds.isEmpty()) {
            // first invalidate the previous bounds
            invalidateSelf();
        }
        mBounds.set(left, top, right, bottom);
        // 在自定义Drawable时就是通过重载这个方法获取到自定义Drawable的绘制区域
        onBoundsChange(mBounds);
    }
}

上面的注释很清楚,就不做赘叙了。
回到第1步的drawBackground方法中,设置完View的背景对应的Drawable的绘制区域后(即setBackgroundBounds方法调用结束后),接着就是在绘制区域上绘制图像(即上图中的第5步,Drawable的draw方法会被调用),Drawable的draw方法是一个抽象方法,因此自定义Drawable时必须实现该方法:

public abstract void draw(@NonNull Canvas canvas);

4. 自定义Drawable

首先让大家看看我通过自定义Drawable实现的一个动画:



大多数开发者者通常第一个会想到通过自定义View来实现上面的动画效果,虽然可以实现上面的动画,但是通用性不是很好,而且实现的复杂度也很高;通过自定义Drawable的方式实现上面的动画效果的好处如下:

  • 通用性好:实现的动画效果可以适用于View及其子类
  • 实现起来很简单:与自定义View相比,不需要进行measure、layout操作,仅仅只需要进行draw操作

1> 分析上面的动画

  • 动画的初始状态:是两个图层叠加的想过,底层是一个50%透明度的白色圆形图层(后面统一称为A层),上层是一个居中显示闹钟图片的图层(后面统一称为B层)。
  • 动画的中间过程:A层的透明度变成50%,然后B层中闹钟图片上方显示了一个与闹钟图片大小相同的透明度为20%的白色圆形图层(后面统一称为C层),然后在C层上面动态的放大一个与C层上圆形半径相同的灰色的圆环(后面统一称为D层),然后在然后在D层上面动态的绘制一个与D层上圆形半径相同的绿色圆环(后面统一称为E层),接着在E层的中心位置动态绘制一个从小变大的绿色对勾(后面统一称作F层),接着就是显示动画的结束状态。
  • 动画的结束状态:A层的透明度变成0,B层与初始状态一样, C、D、E、F层不在显示。

通过上面的分析可知,上面的动画效果要通过6个图层实现,因此可以通过6个Drawable的叠加来实现,A、B、C层由于没有动画效果(A层只不过是在动画开始和结束时改变了透明度),因此实现起来很简单。

2> 实现A、B、C层

实现代码如下所示:

/*
* A层
*/
float radius = DisplayUtils.dip2px(getContext(), 38f);
GradientDrawable bigBgCircle = new GradientDrawable();
float[] radii = new float[]{radius, radius, radius, radius,
        radius, radius, radius, radius};
bigBgCircle.setColor(0xAAFFFFFF);
bigBgCircle.setCornerRadii(radii);

/*
* B层
*/
int padding = DisplayUtils.dip2px(getContext(), 19f);
InsetDrawable clock = new InsetDrawable(getResources().getDrawable(R.drawable.ic_clock), padding);

/*
* C层
*/
float radius2 = DisplayUtils.dip2px(getContext(), 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
        radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
int padding2 = DisplayUtils.dip2px(getContext(), 19f);
InsetDrawable smallBgCircle = new InsetDrawable(smallBgCircleCenter, padding2);

LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{bigBgCircle, clock, smallBgCircle});
animatorDrawableView.setBackground(layerDrawable);

运行截图如下:



怎么只有A层和B层,而没有C层,其实这是LayerDrawable的设计缺陷,padding在LayerDrawable中是累积的,也就是说,在某一层的padding影响所有较高层的bounds,可以通过LayerDrawable的onBoundsChange方法的源码看出来:

@Override
protected void onBoundsChange(Rect bounds) {
    final ChildDrawable[] array = mLayerState.mChildren;
    final int N = mLayerState.mNum;
    int padL=0, padT=0, padR=0, padB=0;
    for (int i=0; i<N; i++) {
        final ChildDrawable r = array[i];
        r.mDrawable.setBounds(bounds.left + r.mInsetL + padL,
                              bounds.top + r.mInsetT + padT,
                              bounds.right - r.mInsetR - padR,
                              bounds.bottom - r.mInsetB - padB);
        padL += mPaddingL[i];
        padR += mPaddingR[i];
        padT += mPaddingT[i];
        padB += mPaddingB[i];
    }
}

因为InsetDrawable用inset作为padding,因此导致在B层上面的C层中的圆的半径被缩小了19dp,因此C层中的圆就不见了。
在API 21的时候,Google意识到了这个设计缺陷,因此在LayerDrawable中添加了setPaddingMode方法,通过此方法可以设置LayerDrawable的PaddingMode为如下两种:

/**
 * 默认的PaddingMode,将每一层嵌入上一层的padding内。
 *
 * @see #setPaddingMode(int)
 */
public static final int PADDING_MODE_NEST = 0;

/**
 * 将每一层直接堆叠在上一层之上。
 *
 * @see #setPaddingMode(int)
 */
public static final int PADDING_MODE_STACK = 1;

上面的注释非常清楚,将LayerDrawable的PaddingMode的PaddingMode设置为PADDING_MODE_STACK就好了;虽然这种方法可以解决问题,但是遗憾的是,为了兼容API 21以下的版本,这个方法是不可行的,因此为了避免这个问题,我自定义了一个PaddingDrawable来取代InsetDrawable,源码如下:

public class PaddingDrawable extends Drawable {
    /**
     * 对应要被设置padding的drawable
     */
    private Drawable drawable = null;
    private Paint paint = null;
    private Rect padding = null;

    public PaddingDrawable(Drawable drawable) {
        this.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        this.drawable = drawable;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPadding(Rect padding) {
        if (padding == null) {
            return;
        }
        if (this.padding == null) {
            this.padding = new Rect();
        }
        this.padding.set(padding);
        invalidateSelf();
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        if (null == drawable) {
            return;
        }
        if (null == padding) {
            drawable.setBounds(bounds);
        } else {
            drawable.setBounds(bounds.left, bounds.top,
                    bounds.right - padding.left - padding.right, bounds.bottom - padding.top - padding.bottom);
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        canvas.drawColor(Color.TRANSPARENT);
        drawDrawable(canvas);
    }

    private void drawDrawable(@NonNull Canvas canvas) {
        if (null == drawable) {
            return;
        }
        if (null != padding) {
            canvas.save();
            canvas.translate(padding.left, padding.top);
            drawable.draw(canvas);
            canvas.restore();
        } else {
            drawable.draw(canvas);
        }
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        this.paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        this.paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

PaddingDrawable是通过自定义Drawable实现的,下面会详细介绍自定义Drawable的,因此大家只关注onBoundsChange和draw方法的实现就行了,在2中Drawable绘制过程源码分析已经详细介绍了onBoundsChange和draw方法的作用,相信大家应该可以理解上面代码的实现;接下来就是对上图的实现代码中的InsetDrawable替换成PaddingDrawable,代码如下:

/*
* A层和上面相同,这里不再重复。
*/

/*
* B层
*/
PaddingDrawable clock = new PaddingDrawable(getResources().getDrawable(R.drawable.ic_clock));
int padding = DisplayUtils.dip2px(getContext(), 19f);
clock.setPadding(new Rect(padding, padding, padding, padding));

/*
* C层
*/
float radius2 = DisplayUtils.dip2px(getContext(), 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
        radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
PaddingDrawable smallBgCircle = new PaddingDrawable(smallBgCircleCenter);
int padding2 = DisplayUtils.dip2px(getContext(), 19f);
smallBgCircle.setPadding(new Rect(padding2, padding2, padding2, padding2));

LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] {bigBgCircle, clock, smallBgCircle});
animatorDrawableView.setBackground(layerDrawable);

运行截图如下:



C层出现了,是我们想要的效果。

3> 实现D、E、F层

实现有动画效果的D、E、F层就要用到自定义Drawable了,首先我们应该绘制与D、E、F层对应的圆环和对勾,接着就是通过属性动画让D、E、F层中的圆环和对勾动起来。

绘制与D、E、F层对应的圆环和对勾

对于绘制圆环和对勾我借鉴了ShapeDrawable做法,那就是继承Shape类实现对于圆环和对勾的绘制,代码如下:

/**
 * TrackShape 基本的轨迹图形类
 * 通过轨迹(即Path)来定义Shape,因此任何Shape的绘制过程就是绘制一条Path,
 * 即draw方法是一样的并且在基类TrackShape中实现。
 */
public class TrackShape extends Shape {

    protected Path track = null;

    /**
     * 代表缩放或者选择后的track
     */
    private Path transformTrack = null;

    /**
     * 轨迹的厚度
     */
    protected float thickness = 0;

    /**
     * 设置轨迹为虚线
     */
    private PathEffect pathEffect = null;

    public TrackShape(Path track, float thickness) {
        this(track, thickness, null);
    }

    public TrackShape(Path track, float thickness, PathEffect pathEffect) {
        this.track = track;
        this.thickness = thickness;
        this.pathEffect = pathEffect;
    }

    public void scale(float widthScale, float heightScale) {
        scale(widthScale, heightScale, getWidth() / 2, getHeight() / 2);
    }

    /**
     * 当发生缩放时被调用
     *
     * @param widthScale  横向缩放的比例
     * @param heightScale 纵向缩放的比例
     * @param px          缩放中心点的x轴坐标
     * @param py          缩放中心点的y轴坐标
     */
    public void scale(float widthScale, float heightScale, float px, float py) {
        if (widthScale < 0) {
            return;
        }
        if (heightScale < 0) {
            return;
        }
        Matrix matrix = new Matrix();
        matrix.setScale(widthScale, heightScale, px, py);
        if (null == this.transformTrack) {
            this.transformTrack = new Path();
        }
        this.track.transform(matrix, this.transformTrack);
    }

    public void rotate(float degrees) {
        rotate(degrees, getWidth() / 2, getHeight() / 2);
    }

    /**
     * 当发生旋转时被调用
     *
     * @param degrees 旋转的角度
     * @param px      旋转中心点的x轴坐标
     * @param py      旋转中心点的y轴坐标
     */
    public void rotate(float degrees, float px, float py) {
        Matrix matrix = new Matrix();
        matrix.setRotate(degrees, px, py);
        if (null == this.transformTrack) {
            this.transformTrack = new Path();
        }
        this.track.transform(matrix, this.transformTrack);
    }

    /**
     * 当发生平移时被调用
     *
     * @param dx 横向平移的距离
     * @param dy 纵向平移的距离
     */
    public void translate(float dx, float dy) {
        Matrix matrix = new Matrix();
        matrix.setTranslate(dx, dy);
        if (null == this.transformTrack) {
            this.transformTrack = new Path();
        }
        this.track.transform(matrix, this.transformTrack);
    }

    @Override
    public void draw(Canvas canvas, Paint paint) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(thickness);
        if (null != pathEffect) {
            paint.setPathEffect(pathEffect);
        }
        if (null == transformTrack) {
            canvas.drawPath(track, paint);
        } else {
            canvas.drawPath(transformTrack, paint);
        }
    }

    public Path getTrack() {
        return this.track;
    }

    public void setTrack(Path track) {
        this.track = track;
    }
}

/**
 * 用来绘制圆环的轨迹图形
 */
public class RingTrackShape extends TrackShape {

    private float centerX = 0;
    private float centerY = 0;

    /**
     * 内圆的半径
     */
    private float innerRadius = 0;


    /**
     * 弧形起始角度
     */
    private float startAngle = 0;

    /**
     * 弧形扫过的角度(正数代表顺时针扫, 负数代表逆时针扫)
     */
    private float sweepAngle = 0;

    public RingTrackShape(float thickness, float startAngle, float sweepAngle) {
        this(thickness, startAngle, sweepAngle, null);
    }

    public RingTrackShape(float thickness, float startAngle, float sweepAngle, DashPathEffect dashPathEffect) {
        super(null, thickness, dashPathEffect);
        this.startAngle = startAngle;
        this.sweepAngle = sweepAngle;
    }

    @Override
    protected void onResize(float width, float height) {
        super.onResize(width, height);
        this.centerX = width / 2;
        this.centerY = height / 2;
        this.innerRadius = Math.min(centerX - thickness, centerY - thickness);
        this.track = new Path();
        this.track.addArc(new RectF(centerX -  innerRadius - thickness/ 2,
                centerY - innerRadius - thickness / 2,
                centerX + innerRadius + thickness / 2,
                centerY + innerRadius + thickness / 2), startAngle, sweepAngle);
    }
}

D、E层的圆环是通过上面的RingTrackShape绘制的,F层的对勾是通过TrackShape绘制的。

通过属性动画让D、E、F层中的圆环和对勾动起来

上面提到的TrackShape和RingTrackShape将绘制圆环和对勾的过程进行了封装,然后在自定义的TrackShapeDrawable中对TrackShape和RingTrackShape进行绘制,说到自定义的Drawable,必须要了解一下Drawable类:

/**
 * 指定drawable的alpha值。 0表示完全透明,255表示完全不透明。
 */
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);

/**
 * 为drawable指定可选的color filter。
 *
 * 如果Drawable具有ColorFilter,则Drawables绘图内容的每个输出像素在被混合到
 * Canvas的render目标之前将被color filter进行修改。
 *
 * 通过传递null参数删除任何现有的color filter。
 * 注意: 设置非null的color filter将使{@link #setTintList(ColorStateList)tint}无效。
 * 
 * @param colorFilter 要应用的color filter,或null参数删除现有的彩色滤镜
 */
public abstract void setColorFilter(@Nullable ColorFilter colorFilter);

/**
 * 返回Drawable的不透明度。 返回的值是如下常量之一:
 * {@link android.graphics.PixelFormat}:
 * {@link android.graphics.PixelFormat#UNKNOWN},
 * {@link android.graphics.PixelFormat#TRANSLUCENT},
 * {@link android.graphics.PixelFormat#TRANSPARENT}, or
 * {@link android.graphics.PixelFormat#OPAQUE}.
 *
 * OPAQUE drawable是在其bounds范围内绘制所有内容,完全覆盖了drawable后面的任何内容。 
 * TRANSPARENT drawable是在其bounds范围内不绘制任何内容的,可以让其后面的任何东西显示出来。
 * TRANSLUCENT drawable是在其bounds范围内绘制一些但不是全部的内容,并且至少在drawable后面的一些内容将可见。 
 * 如果无法确定可绘制内容的可见性,则最安全/最佳返回值为TRANSLUCENT。
 *
 * 一般来说,返回值应该尽可能保守(使用resolveOpacity方法可以做到)。
 * 例如,如果它包含多个child drawables,并且一次仅显示其中一个,如果只有一个子项是TRANSLUCENT,
 * 而其他子项是OPAQUE,则应返回TRANSLUCENT。
 *
 * 请注意,返回的值不一定考虑通过setAlpha或setColorFilter方法应用的自定义Alpha或color filter。
 * 某些子类(例如 BitmapDrawable,ColorDrawable和GradientDrawable)确实存在setAlpha的值,
 * 但一般行为取决于子类的实现。
 *
 * @return int  Drawable的不透明度类型。
 *
 * @see android.graphics.PixelFormat
 */
public abstract @PixelFormat.Opacity int getOpacity();

/**
 * 再其边界内绘制(通过setBounds设置),可以使用alpha(通过setAlpha设置)和
 * color filter(通过setColorFilter设置)等可选效果。
 *
 * @param canvas The canvas to draw into
 */
public abstract void draw(@NonNull Canvas canvas);

接着来看一下TrackShapeDrawable是如何对TrackShape和RingTrackShape进行绘制的:

/**
 * 用于绘制TrackShape的Drawable
 */
public class TrackShapeDrawable extends Drawable {

    /**
     * 对应要绘制的轨迹图形
     */
    protected TrackShape shape = null;

    protected Paint paint;

    public TrackShapeDrawable(TrackShape shape) {
        this.shape = shape;
        this.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    }

    public Paint getPaint() {
        return paint;
    }

    public Path getTrack() {
        if (null == shape) {
            return null;
        }
        return shape.getTrack();
    }

    public void setTrack(Path track) {
        if (null == shape || null == track) {
            return;
        }
        shape.setTrack(track);
        invalidateSelf();
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        updateShape(bounds);
    }

    private void updateShape(Rect bounds) {
        if (null != shape) {
            shape.resize(bounds.width(), bounds.height());
        }
    }

    public void scale(float widthScale, float heightScale) {
        if (null == shape) {
            return;
        }
        shape.scale(widthScale, heightScale);
        invalidateSelf();
    }

    /**
     * 当发生缩放时被调用
     *
     * @param widthScale  横向缩放的比例
     * @param heightScale 纵向缩放的比例
     * @param px          缩放中心点的x轴坐标
     * @param py          缩放中心点的y轴坐标
     */
    public void scale(float widthScale, float heightScale, float px, float py) {
        if (null == shape) {
            return;
        }
        shape.scale(widthScale, heightScale, px, py);
        invalidateSelf();
    }

    public void rotate(float degrees) {
        if (null == shape) {
            return;
        }
        shape.rotate(degrees);
        invalidateSelf();
    }

    /**
     * 当发生旋转时被调用
     *
     * @param degrees 旋转的角度
     * @param px      旋转中心点的x轴坐标
     * @param py      旋转中心点的y轴坐标
     */
    public void rotate(float degrees, float px, float py) {
        if (null == shape) {
            return;
        }
        shape.rotate(degrees, px, py);
        invalidateSelf();
    }

    /**
     * 当发生平移时被调用
     *
     * @param dx 横向平移的距离
     * @param dy 纵向平移的距离
     */
    public void translate(float dx, float dy) {
        if (null == shape) {
            return;
        }
        shape.translate(dx, dy);
        invalidateSelf();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        if (null == shape) {
            return;
        }
        shape.draw(canvas, paint);
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        this.paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        this.paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

上面代码中的看出scale、rotate、translate、setTrack方法是用来实现对TrackShape和RingTrackShape中的轨迹进行scale、rotate、translate和替换,具体可以参考TrackShape中的scale、rotate、translate、setTrack方法。讲到这里,大家应该有了实现动画的思路了吧,就是通过属性动画来动态调用TrackShapeDrawable的scale、rotate、translate、setTrack方法即可。

接下来为了实现D、E、F层中的圆环和对勾对应的缩放动画和轨迹动画,我实现了ScaleAnimatorDrawable、TrackAnimatorDrawable两个自定义Drawable,这两个自定义Drawable就是对 属性动画动态调用TrackShapeDrawable的scale、setTrack方法 的封装,其中ScaleAnimatorDrawable不仅支持TrackShapeDrawable也支持其它类型的Drawable,实现代码如下:

/**
 * 作为自定义动画Drawable的基类
 */
public abstract class AnimatorDrawable extends Drawable implements Animatable, Drawable.Callback, Animator.AnimatorListener {

    /**
     * 在Drawable绘制区域的基础上设置padding
     */
    Rect padding = new Rect();

    /**
     * 要被动画的drawable
     */
    protected Drawable drawable = null;

    private ValueAnimator valueAnimator = null;

    /**
     * 表示动画是不是已经被启动
     */
    private boolean isFirst = true;

    /**
     * 轨迹动画的时长,默认200毫秒
     */
    protected long duration = 200;

    /**
     * 轨迹动画延迟执行的时长,默认为0,即不延迟
     */
    protected long startDelay = 0;

    /**
     * 代表动画执行完成之后是否停留在最后一帧
     * true  代表停留在最后一帧
     * false 代表不会显示如何东西
     */
    private boolean fillAfter = true;

    public AnimatorDrawable(Drawable drawable, long duration) {
        this(drawable, duration, true);
    }

    public AnimatorDrawable(Drawable drawable, long duration, boolean fillAfter) {
        if (null == drawable) {
            return;
        }
        this.drawable = drawable;
        this.drawable.setCallback(this);// 防止drawable的invalidateSelf方法无法重绘
        if (duration > 0) {
            this.duration = duration;
        }
        this.fillAfter = fillAfter;
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        updateDrawable();
    }

    private void updateDrawable() {
        if (null == drawable) {
            return;
        }

        Rect bounds = getBounds();
        if (null == padding) {
            drawable.setBounds(bounds);
        } else {
            drawable.setBounds(bounds.left, bounds.top,
                    bounds.right - padding.left - padding.right, bounds.bottom - padding.top - padding.bottom);
        }
    }

    /**
     * Sets padding for this shape, defined by a Rect object. Define the padding
     * in the Rect object as: left, top, right, bottom.
     */
    public void setPadding(Rect padding) {
        if (padding == null) {
            return;
        }
        this.padding = padding;
        updateDrawable();
        invalidateSelf();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        if (isFirst) {
            start();
            isFirst = false;
        } else {
            if (isRunning() || null == valueAnimator) {
                onDraw(canvas);
            }
        }
    }

    public void onDraw(@NonNull Canvas canvas) {
        if (null != padding) {
            canvas.save();
            canvas.translate(padding.left, padding.top);
            canvas.clipRect(0, 0, drawable.getBounds().width(), drawable.getBounds().height());
            drawTrack(canvas);
            canvas.restore();
        } else {
            canvas.save();
            drawTrack(canvas);
            canvas.restore();
        }
    }

    public abstract void drawTrack(@NonNull Canvas canvas);

    /**
     * 获取轨迹动画的时长
     */
    public long getDuration() {
        return duration;
    }

    public void setStartDelay(long startDelay) {
        if (startDelay < 0) {
            return;
        }
        this.startDelay = startDelay;
    }

    public long getStartDelay() {
        return startDelay;
    }

    void setValueAnimator(ValueAnimator valueAnimator) {
        if (null == valueAnimator) {
            this.valueAnimator.cancel();
            this.valueAnimator = null;
        } else {
            this.valueAnimator = valueAnimator;
        }
    }

    @Override
    public void start() {
        if (null != valueAnimator && (valueAnimator.isRunning() || valueAnimator.isStarted())) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        setupAnimators();
        if (null != valueAnimator) {
            valueAnimator.start();
        }
    }

    public abstract void setupAnimators();

    @Override
    public void stop() {
        if (null != valueAnimator && (valueAnimator.isRunning() || valueAnimator.isStarted())) {
            valueAnimator.cancel();
        }
    }

    @Override
    public boolean isRunning() {
        return null != valueAnimator && valueAnimator.isRunning();
    }

    @Override
    public void invalidateDrawable(@NonNull Drawable who) {
        invalidateSelf();
    }

    @Override
    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
        scheduleSelf(what, when);
    }

    @Override
    public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
        unscheduleSelf(what);
    }

    @Override
    public void onAnimationStart(Animator animation) {
        if (null != this.onAnimatorListener) {
            this.onAnimatorListener.onAnimationStart(this);
        }
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        valueAnimator.removeAllUpdateListeners();
        valueAnimator.removeAllListeners();
        if (fillAfter) {
            setValueAnimator(null);
        }
        if (null != this.onAnimatorListener) {
            this.onAnimatorListener.onAnimationEnd(this);
        }
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {}

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {}

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    private OnAnimatorListener onAnimatorListener = null;

    public void setOnAnimatorListener(OnAnimatorListener onAnimatorListener) {
        this.onAnimatorListener = onAnimatorListener;
    }

    public interface OnAnimatorListener {
        void onAnimationStart(AnimatorDrawable animatorDrawable);
        void onAnimationEnd(AnimatorDrawable animatorDrawable);
    }
}

/**
 * 对Drawable进行Scale动画
 */
public class ScaleAnimatorDrawable extends AnimatorDrawable {

    /**
     * 代表Drawable缩放的比例
     */
    private float scale = 0;

    private Rect initPadding = new Rect();


    public ScaleAnimatorDrawable(Drawable drawable, long duration) {
        super(drawable, duration);
    }

    @Override
    public void setupAnimators() {
        // 在启动动画之前记录最开始的padding
        initPadding = padding;

        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                scale = (float) animation.getAnimatedValue();
                preDraw();
            }
        });
        valueAnimator.addListener(this);
        valueAnimator.setDuration(duration).setStartDelay(startDelay);
        setValueAnimator(valueAnimator);
    }

    private void preDraw() {
        if (drawable instanceof TrackShapeDrawable) {
            ((TrackShapeDrawable) drawable).scale(scale, scale);
        } else {
            Rect bounds = getBounds();
            setPadding(new Rect((int) (initPadding.left + (bounds.width() - initPadding.left - initPadding.right) * 0.5 * (1 - scale)),
                    (int) (initPadding.top + (bounds.height() - initPadding.top - initPadding.bottom) * 0.5 * (1 - scale)),
                    (int) (initPadding.right + (bounds.width() - initPadding.left - initPadding.right) * 0.5 * (1 - scale)),
                    (int) (initPadding.bottom + (bounds.height() - initPadding.top - initPadding.bottom) * 0.5 * (1 - scale))));
        }
    }

    @Override
    public void drawTrack(@NonNull Canvas canvas) {
        drawable.draw(canvas);
    }
}

/**
 * 对TrackShapeDrawable进行轨迹动画
 */
public class TrackAnimatorDrawable extends AnimatorDrawable {

    /**
     * 代表将要被绘制的轨迹片段
     */
    private Path trackSegment = null;

    public TrackAnimatorDrawable(TrackShapeDrawable drawable, long duration) {
        super(drawable, duration);
    }

    @Override
    public void setupAnimators() {
        final ValueAnimator valueAnimator = ValueAnimator.ofObject(new PathEvaluator(), new Path(), ((TrackShapeDrawable) drawable).getTrack());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                trackSegment = (Path) animation.getAnimatedValue();
                preDraw();
            }
        });
        valueAnimator.addListener(this);
        valueAnimator.setDuration(duration).setStartDelay(startDelay);
        setValueAnimator(valueAnimator);
        trackSegment = new Path();
    }

    private void preDraw() {
        ((TrackShapeDrawable) drawable).setTrack(trackSegment);
    }

    @Override
    public void drawTrack(@NonNull Canvas canvas) {
        drawable.draw(canvas);
    }
}

接着就是使用上面自定义的ScaleAnimatorDrawable、TrackAnimatorDrawable实现动画效果了:

// A层除了颜色值变为0xAAFFFFFF其它和2>中相同,B层也和2>中相同,这里不再赘叙。

// C层
float radius2 = DisplayUtils.dip2px(this, 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
        radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
PaddingDrawable smallBgCircle = new PaddingDrawable(smallBgCircleCenter);
int padding3 = DisplayUtils.dip2px(this, 19f);
smallBgCircle.setPadding(new Rect(padding3, padding3, padding3, padding3));

// D层
ScaleAnimatorDrawable scaledCircle = (ScaleAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, getResources().getDrawable(R.drawable.ic_drawable_ring), AnimatorDrawableFactory.AnimatorDrawableType.SCALE);
int padding4 = DisplayUtils.dip2px(this, 19f);
scaledCircle.setPadding(new Rect(padding4, padding4, padding4, padding4));

// E层
TrackShapeDrawable scaleRingDrawable = new TrackShapeDrawable(new RingTrackShape(DisplayUtils.dip2px(this, 2), 0, 360));
scaleRingDrawable.getPaint().setColor(Color.GREEN);
TrackAnimatorDrawable trackAnimatorDrawable = (TrackAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, scaleRingDrawable, AnimatorDrawableFactory.AnimatorDrawableType.TRACK);
int padding5 = DisplayUtils.dip2px(this, 20f);
trackAnimatorDrawable.setPadding(new Rect(padding5, padding5, padding5, padding5));

// F层
Path track = new Path();
track.moveTo(0f, DisplayUtils.dip2px(this, 10f));
track.lineTo(DisplayUtils.dip2px(this, 10f), DisplayUtils.dip2px(this, 20f));
track.lineTo(DisplayUtils.dip2px(this, 20f), 0f);
TrackShape trackShape = new TrackShape(track, DisplayUtils.dip2px(this, 2));
TrackShapeDrawable trackShapeDrawable3 = new TrackShapeDrawable(trackShape);
trackShapeDrawable3.getPaint().setColor(Color.GREEN);
ScaleAnimatorDrawable scaleAnimatorDrawable = (ScaleAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, trackShapeDrawable3, AnimatorDrawableFactory.AnimatorDrawableType.SCALE);
int padding6 = DisplayUtils.dip2px(this, 28f);
scaleAnimatorDrawable.setPadding(new Rect(padding6, padding6, padding6, padding6));

TrackAnimatorManager trackAnimatorManager = new TrackAnimatorManager();
trackAnimatorManager.setOnAnimatorListener(this);
LayerDrawable layerDrawable = trackAnimatorManager.createLayerDrawable(TrackAnimatorManager.Order.SEQUENTIALLY,
        bigBgCircle, clock, smallBgCircle, scaledCircle, trackAnimatorDrawable, scaleAnimatorDrawable);
animatorDrawableView.setBackground(layerDrawable);

上面代码中的trackAnimatorManager.createLayerDrawable是将多个Drawable放到一个LayerDrawable中,如果其中有AnimatorDrawable的子类就会将其按照其在数组中顺序依次进行动画,实现代码如下:

public LayerDrawable createLayerDrawable(Order order, Drawable... drawables) {
    if (null == drawables || drawables.length <= 0) {
        return null;
    }
    this.drawables = drawables;
    LayerDrawable layerDrawable = new LayerDrawable(this.drawables);
    int startDelay = 0;
    for (Drawable drawable : this.drawables) {
        if (drawable instanceof AnimatorDrawable) {
            startDelay += ((AnimatorDrawable) drawable).getStartDelay();
            ((AnimatorDrawable) drawable).setOnAnimatorListener(this);
            switch (order) {
                case SEQUENTIALLY: {
                    ((AnimatorDrawable) drawable).setStartDelay(startDelay);
                    break;
                }
            }
            startDelay += ((AnimatorDrawable) drawable).getDuration();
        }
    }
    return layerDrawable;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容