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的背景的任务主要分为两部分:
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;
}