终于在新的一年的第一天完成了本篇文章,小盆友在此祝贺您,万事如意,阖家幸福。😄
目录
一、前言
二、插值器与估值器
三、源码解析
四、实战
五、写在最后
一、前言
对于越来越追求丰富的交互体验的客户端,一个带有动态效果的界面已经是不可避免。属性动画就是这其中必不可少的一把利器,所以今天便来分享下 属性动画融合在实际场景中 的使用,以及进行 源码分析。话不多说,先看看今天的实战效果图,然后开始进行分享之旅。
1、多维雷达图
2、表盘指示器
3、活力四射的购物车
老规矩,代码在实战篇会给出,透过 API 看清源码,各种效果便是顺手拈来😄
二、插值器与估值器
对于属性动画来说,一定是绕不开 插值器 与 估值器。接下来便一一道来
文章更多的使用 我在开发过程中 自我理解 的词语,而尽量不使用 教科书式 或 直接翻译注释 语句。如果有 晦涩难懂 或是 理解错误之处,欢迎 评论区 留言,我们进行探讨。
1、插值器(TimeInterpolator)
(1)定义
是一个控制动画 进度 的工具。
为何如此说?咱们可以这么理解,我们借助 SpringInterpolator 插值器的函数图来讲解。
- x轴为 时间 。0表示还未开始,1表示时间结束。时间是 不可逆的 ,所以会一直往前推进,即 只会增加。
- y轴为 当前时间点,物体(我们的控制的动画对象,例如View)距离目的地的进度。0表示进度还未走动,1表示进度已经走至目的地。但值得注意的是,到达目的地(A点)不代表动画已经结束,可以 “冲破”(B点) 目的地,也可以 往回走(C点) ,结束与否是由时间轴决定。
SpringInterpolator 的动态图如下
下面动态图是小盆友专门为插值器更为直观的展示而开发的小工具,如果你感兴趣,可以把自己的插值器也放入这其中进行展示,方便以后开发时需要,代码传送门。
这个插值器,在小盆友的另一篇博文中有使用到,效果如下图,有兴趣的童鞋可以进入传送门了解。
(2)如何使用
Android 系统中已经帮我实现了一些比较常用插值器,这里就不一一贴图介绍器函数图,需要的童鞋可以进入小盆友下面所提到的 插值器小工具 进行玩耍,这里给一张工具的效果图。
加入到属性动画中也很简单,只需以下一行代码,便可轻松搞定
// 将 插值器 设置进 Animator
mAnimator.setInterpolator(new AccelerateInterpolator());
在源码中插值器是如何作用的呢?先卖个关子,在源码解析小节给出答案。
但是在某些情况下,为了满足动画效果的需要,Android提供的插值器就满足不了我们(😭其实就是设计师搞事情)了,所以需要找到对应的公式进行自定义插值器。聪明的你,肯定已经发现,我们前面提到的 SpringInterpolator 并不是Android提供的插值器。定义 SpringInterpolator 时,只需要实现 TimeInterpolator 接口,并在 getInterpolation 方法中实现自己的逻辑即可,代码如下
/**
* @author Jiang zinc
* @date 创建时间:2019/1/27
* @description 震旦效果
*/
public class SpringInterpolator implements TimeInterpolator {
/**
* 参数 x,即为 x轴的值
* 返回值 便是 y 轴的值
*/
@Override
public float getInterpolation(float x) {
float factor = 0.4f;
return (float) (Math.pow(2, -10 * x) * Math.sin((x - factor / 4) * (2 * Math.PI) / factor) + 1);
}
}
使用时,和 Android提供的插值器 是一摸一样的,如下所示
mAnimator.setInterpolator(new SpringInterpolator());
2、估值器(TypeEvaluator)
(1)定义
将 插值器 中的 y轴数值 转换为我们 需要的值类型 的工具。
emmmm....稍微有点抽象。我们来具体分析下,这句话的意思。
我们以一个 具体的场景 来分析这个定义,方便讲解也更容易理解。我们进行旋转一个View,在1秒内,从 0度 转到 360度。具体代码如下:
// 实例化一个view
View view = new View(this);
// 设置 属性动画 的目标对象、作用的属性、关键帧(即0,360)
// 作用的属性值 rotation 会转为对应的方法名 setRotation,这个是默认的规则。
ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(view, "rotation", 0, 360);
// 设置 插值器
rotationAnimator.setInterpolator(new SpringInterpolator());
// 设置 动画时长
rotationAnimator.setDuration(1_000);
// 开启 动画
rotationAnimator.start();
这里其实有个问题,童鞋们应该也注意到了,插值器 的返回值(即函数图中的 y轴数值)是一个 到 “目的地” 的距离百分比(这里的百分比也就是我们前面所说的进度) ,而非我们例子中需要度数,所以需要进行 转换,而起到转换作用的就是我们的 估值器。
怎么转换呢?这里要 分两种情况说明。
第一种情况,我们通过以下代码,设置一个估值器,则计算规则由设置的估值器确定
ObjectAnimator mTranslateAnimator = ObjectAnimator.ofObject(view,
"position",
new BezierEvaluator(),
startPoint,
endPoint);
Android 系统中提供了一些 常用的估值器
- ArgbEvaluator:颜色估值器,可以用于从 开始颜色 渐变为 终止颜色;
- FloatArrayEvaluator:浮点数组估值器,将开始浮点数组 逐渐变为 终止浮点数组;
- FloatEvaluator:浮点数值估值器,将开始浮点数 逐渐变为 终止浮点数;
- IntArrayEvaluator:整型数组估值器,将开始整型数组 逐渐变为 终止整型数组;
- IntEvaluator:整型数值估值器,将开始整型数 逐渐变为 终止整型数;
- PointFEvaluator:坐标点估值器,将开始坐标点 逐渐变为 终止坐标点;
- RectEvaluator:范围估值器,将开始范围 逐渐变为 终止范围;
然鹅,在某些情况下(产品大大搞事),我们需要实现一个类似如下的添加到购物车的效果,商品是以 贝塞尔曲线 的路径 “投到” 购物车中的,这时我们就需要 自定义估值器,因为 PointFEvaluator只是线性的将商品从起始点移到终止点,满足不了产品大大的需求,如何自定义呢?请往下走
对贝塞尔曲线感兴趣的童鞋,可以查看小盆友的另一片博文 自带美感的贝塞尔曲线原理与实战
相信你也猜到了,估值器 也是通过实现一个接口,以下便是接口和参数描述
public interface TypeEvaluator<T> {
/**
* @param fraction 插值器返回的值(即函数图中的 y轴数值)
* @param startValue 动画属性起始值,例子中的 0度
* @param endValue 动画属性终止值,例子中的 360度
*/
public T evaluate(float fraction, T startValue, T endValue);
}
我们只需要实现他,填充自己的逻辑,以这个购物车的路径为例,便是以下代码,这样一个走 贝塞尔曲线 路径的商品就出现了。
private static class BezierEvaluator implements TypeEvaluator<PointF> {
private final List<PointF> pointList;
public BezierEvaluator(PointF startPoint, PointF endPoint) {
this.pointList = new ArrayList<>();
PointF controlPointF = new PointF(endPoint.x, startPoint.y);
pointList.add(startPoint);
pointList.add(controlPointF);
pointList.add(endPoint);
}
@Override
public PointF evaluate(float fraction, PointF startPoint, PointF endPoint) {
return new PointF(BezierUtils.calculatePointCoordinate(BezierUtils.X_TYPE, fraction, 2, 0, pointList),
BezierUtils.calculatePointCoordinate(BezierUtils.Y_TYPE, fraction, 2, 0, pointList));
}
}
购物车的动画思路如下:
- 点击添加商品后,初始化一个 ShoppingView(继承至ImageView),设置其大小和商品的大小一致,并添加至 decorView 中,这样才能让 ShoppingView 在整个视图中移动。
- 通过 getLocationOnScreen 方法,获取商品图片在屏幕中的坐标A 和 购物车在屏幕中的坐标B(值得注意的是,这个坐标是 包含状态栏的高度 ),将 ShoppingView 移至坐标A,然后启动动画。
- 进行以 ShoppingView 正中心为原点进行 从0到0.8缩小,接着顺时针倾斜35度,紧接着以 贝塞尔曲线 路径从坐标A移至坐标B,控制点C的x轴为B点的x轴坐标,y轴为A点的y轴坐标;
- 动画完成后,将 ShoppingView 从 其父视图中移除,同时回调侦听接口,以便购物车可以进行动画。
- 接到回调后,购物车数量加一,同时更换购物车图标,已经显示小红点并且播放其动画,以小红点正中心为原点进行缩放,缩放规则为从 1 到 1.2 到 1 到 1.1 到 1, 产生一种弹动的效果。
如果对这一动画感兴趣,可以查看具体代码,请进入传送门。
第二种情况,大多数情况下,我们不会进行设置估值器,因为源码中已经帮我们做了这一步的转换。所以当我们没有设置时,系统会以以下的公式进行转换(我们这里以 浮点数 为具体场景)
// 这段代码在 FloatKeyframeSet 的 getFloatValue 方法中
prevValue + intervalFraction * (nextValue - prevValue);
这里再卖个关子,公式的各个参数的意义先不给出,在源码解析一节中一起讲解。
值得注意的是,如果属性动画中需要使用的是自己定义的类型,则必须要使用第一种情况自行定义估值器,否则会crash。
(2)如何使用
如何使用,在上一小节其实已经给出,这里给一个完整的代码
ObjectAnimator mTranslateAnimator = ObjectAnimator.ofObject(this,
"position",
new BezierEvaluator(startPoint, endPoint),
startPoint,
endPoint);
mTranslateAnimator.setDuration(450);
mTranslateAnimator.setInterpolator(new AccelerateInterpolator());
mTranslateAnimator.start();
private static class BezierEvaluator implements TypeEvaluator<PointF> {
private final List<PointF> pointList;
public BezierEvaluator(PointF startPoint, PointF endPoint) {
this.pointList = new ArrayList<>();
PointF controlPointF = new PointF(endPoint.x, startPoint.y);
pointList.add(startPoint);
pointList.add(controlPointF);
pointList.add(endPoint);
}
@Override
public PointF evaluate(float fraction, PointF startPoint, PointF endPoint) {
return new PointF(BezierUtils.calculatePointCoordinate(BezierUtils.X_TYPE, fraction, 2, 0, pointList),
BezierUtils.calculatePointCoordinate(BezierUtils.Y_TYPE, fraction, 2, 0, pointList));
}
}
三、源码解析
1、约法三章
进入源码解析前,有必要先跟各位同学 确认一件事 和 建立一个场景,否则源码解析过程会 没有目标 ,而迷路。
(1)确认一件事
你已经会使用属性动画,会的意思是你已经能在 不借助文档 或是 “借鉴”别人的代码 的情况下,写出一个属性动画,并让他按照你所需要的效果 正常的运行 起来,效果难易程度不限定。
(2)建立一个场景
源码的阅读在一个具体的场景中更容易理解,虽然会稍微片面些,但是漏掉的情景在懂得了一个场景后,后续使用过程中便会慢慢的补充,而且主线已懂,支线也就不难了。话不多说,我们来看看这个使用场景。
我们针对 ZincView 的 setZinc 方法进行值变动:
- 值变动范围:从 0 到 2,从 2 到 5(浮点数)
- 动画时长 2000 毫秒
- 设置了 FloatEvaluator 估值器 (这里是为了源码解析,所以特意加上去;如果正常情况下,该场景是不需要设置估值器的)
- 设置了 更新回调器
- 设置了 生命周期监听器
/**
* @author Jiang zinc
* @date 创建时间:2019/1/14
* @description 属性动画源码分析
*/
public class SimpleAnimationActivity extends Activity {
private static final String TAG = "SimpleAnimationActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ZincView view = new ZincView(this);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "zinc", 0f, 2f, 5f);
// 时长
objectAnimator.setDuration(2000);
// 插值器
objectAnimator.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
return input;
}
});
// 估值器
objectAnimator.setEvaluator(new FloatEvaluator());
// 更新回调
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue().getClass());
Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue());
}
});
// 生命周期监听器
objectAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.i(TAG, "onAnimationStart: ");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.i(TAG, "onAnimationEnd: ");
}
@Override
public void onAnimationCancel(Animator animation) {
Log.i(TAG, "onAnimationCancel: ");
}
@Override
public void onAnimationRepeat(Animator animation) {
Log.i(TAG, "onAnimationRepeat: ");
}
});
// 开启
objectAnimator.start();
}
public static class ZincView extends View {
public ZincView(Context context) {
super(context);
}
public void setZinc(float value) {
Log.i(TAG, "setZinc: " + value);
}
}
}
接下来我们便 一行行代码的进入源码的世界 ,了解 “属性动画” 的背后秘密。请各位同学打开自己的源码查看器,或是 Android Studio,一边跟着小盆友的思路走,一边在源码间跳跃,才不容易懵。
这里再次强调,以下代码分析都是基于这个场景,所以参数的讲解和逻辑贯穿也会直接带入场景中的数值或对象,且不再做特殊说明。并且以 "FLAG(数字)" 来作为锚,方便代码折回讲解,各位童鞋可以使用浏览器搜索功能迅速定位。
我们的源码版本是26,接下来就开始我们的每行分析。
2、第一行代码
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "zinc", 0f, 2f, 5f);
进入 ObjectAnimator 的 ofFloat 方法
// ObjectAnimator类
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
// 初始化 ObjectAnimator,将 目标对象 和 属性 进行关联
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
// 设置关键帧 FLAG(1)
anim.setFloatValues(values);
return anim;
}
上面👆代码中的第一行,便是构建一个 ObjectAnimator 实例,同时将 目标对象( ZincView )和 属性方法(zinc)一同传入,具体的内容如下👇代码,我们还需要再进入一层了解
// ObjectAnimator类
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
// FLAG(2)
setPropertyName(propertyName);
}
再进入上面👆第一行代码,便来到以下👇的代码,该方法主要功能是 停止之前目标对象的动画(如果有之前目标对象的话),然后将目标对象置换为现在的ZincView对象
// ObjectAnimator类
public void setTarget(@Nullable Object target) {
// 获取之前的目标对象,这里的场景 原来的目标对象 为空
final Object oldTarget = getTarget();
// 两个目标对象不同,因为 oldTarget 为 null,target 为 ZincView,进入此if分支
if (oldTarget != target) {
// 如果已经是在运行,则进行取消
// isStarted()方法具体请看下面代码段,只是获取 mStarted 标记位,
// 该标记初始化为false,动画开启开始之前,该标记一直为false,开启之后,被置为true,稍后会提到
if (isStarted()) {
// FLAG(24)
cancel();
}
// 进行设置 新的目标对象,进行弱引用,防止内存泄漏
mTarget = target == null ? null : new WeakReference<Object>(target);
// 将 初始状态置为 false
mInitialized = false;
}
}
// ValueAnimator类(ObjectAnimator 继承至 ValueAnimator)
public boolean isStarted() {
// mStarted 被初始化为 false,动画未开始,该标记为则为false
return mStarted;
}
对 弱引用与内存泄漏 方面有兴趣的同学,可以阅读小盆友的另一片博客,内存泄漏与排查流程
跳出 setTarget 方法,我们进入到 setPropertyName 方法,即 FLAG(2) ,可以看到以下👇代码,这段代码其实就是将 属性方法名(zinc)存至 类成员属性 mPropertyName中,没有其他的有意义操作
// ObjectAnimator类
public void setPropertyName(@NonNull String propertyName) {
// 此场景中,mValues为空,此 if 分支不会进入,可以先不进行理会
// 至于 mValues 是什么,在什么时候会初始化,很快就会揭晓
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
// 将 属性方法名(zinc) 存至 mPropertyName属性 中
mPropertyName = propertyName;
mInitialized = false;
}
小结一下
至此 ObjectAnimator 的构造方法走完,我们先来小结一下,做了两件事:
- 将 目标对象ZincView 以弱引用的形式保存在 mTarget属性 中
- 将 属性方法名zinc 保存在 mPropertyName属性 中
看完构造方法中的秘密,回到 ObjectAnimator 的 ofFloat方法 中,进入接下来的那行代码,即 FLAG(1) ,这行代码用于关键帧设置,具体如下👇,因为 mValues 此时为空,且 mProperty 也为空,所以最终进入 setValues 那一行代码(即FLAG(3))
// ObjectAnimator类
public void setFloatValues(float... values) {
// mValues 为空
if (mValues == null || mValues.length == 0) {
// mProperty 为空,在这个场景中,进入else分支
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
// 进行 PropertyValuesHolder 包装
// 将 关键帧 进行各自的封装成 Keyframe
// 然后打包成 KeyframeSet 与 mPropertyName 共同保存进 PropertyValuesHolder中
// FLAG(3)
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
super.setFloatValues(values);
}
}
setValues 那一行代码中其实还包含了一句 PropertyValuesHolder 的构建语句,我们先进入PropertyValuesHolder 的 ofFloat 方法中,能看到如下代码段,实例化了一个 FloatPropertyValuesHolder 类型的对象,同时又将 属性方法名 和 关键帧的值 传入。
// PropertyValuesHolder类
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
return new FloatPropertyValuesHolder(propertyName, values);
}
进入到构造方法,可以看到如下两行代码,第一行是调用了父类的构造方法,而 FloatPropertyValuesHolder 继承至 PropertyValuesHolder,所以便来到了 PropertyValuesHolder 的构造方法,可以看到就是将 属性方法名zinc 存至 mPropertyName属性 中。
// FloatPropertyValuesHolder类
public FloatPropertyValuesHolder(String propertyName, float... values) {
super(propertyName);
// FLAG(4)
setFloatValues(values);
}
// PropertyValuesHolder类
private PropertyValuesHolder(String propertyName) {
mPropertyName = propertyName;
}
往下运行,进入 setFloatValues 方法,即 FLAG(4) ,便来到了下面这段代码,我们先直接进入 父类的 setFloatValues 方法,这个方法中,主要将关键帧的类型存至 mValueType 属性中,然后进行创建关键帧集合存放至 mKeyframes 属性中。
// FloatPropertyValuesHolder类
public void setFloatValues(float... values) {
// 保存 关键帧
super.setFloatValues(values);
// mKeyframes 已经在 super.setFloatValues(values); 中初始化完毕
// 这里将其强转为 Keyframes.FloatKeyframes 浮点数类型的关键帧
// FLAG(5)
mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}
// PropertyValuesHolder类
public void setFloatValues(float... values) {
// 保存关键帧类型为 float类型
mValueType = float.class;
// 进行拼凑 关键帧集合,最后将其返回
mKeyframes = KeyframeSet.ofFloat(values);
}
进入 KeyframeSet 的 ofFloat 方法(具体代码看下面👇),ofFloat方法主要是将我们传进来的关键帧数值转换为关键帧对象,然后封装成 FloatKeyframeSet类型的关键帧集合,方便后期动画运行时使用(如何使用,在讲解start时会详细讲解)。
// KeyframeSet类
/**
* 创建 关键帧集合 对象
* 传进来的 "0f, 2f, 5f" 每个数值将被封装成 Keyframe 关键帧对象
* 最终被放置 FloatKeyframeSet关键帧集合中 向上转型为 KeyframeSet
*
* @param values 传进来的 0f, 2f, 5f 数值
* @return 返回关键帧集合
*/
public static KeyframeSet ofFloat(float... values) {
// 是否为坏数据标记 (not a number)
boolean badValue = false;
// 关键帧数量,这里长度为 3
int numKeyframes = values.length;
// 保证关键帧的数组长度至少为 2
FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes, 2)];
// 当关键帧数量为1时,需要补足 两个,该场景中,进入 else 分支
if (numKeyframes == 1) {
// 第一个用 空value 进行补充,默认会为0
// 因为 Keyframe 的 mValue属性类型为float,jvm会自动为其填充为 0
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
// 填充 第二帧 为传进来的数值
keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
// 是否为 not a number,如果是则改变标记位
if (Float.isNaN(values[0])) {
badValue = true;
}
} else {
// 关键帧 进行添加进集合
// 传进来的值:0f ------> fraction: 0 keyframes[0]
// 传进来的值:2f ------> fraction: 1/2 keyframes[1]
// 传进来的值:5f ------> fraction: 1 keyframes[2]
// fraction 为ofFloat的第一个参数, value 为ofFloat的第二个参数;
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
// 是否为 not a number,如果是则改变标记位
if (Float.isNaN(values[i])) {
badValue = true;
}
}
}
// 如果为 not a number,则进行提示
if (badValue) {
Log.w("Animator", "Bad value (NaN) in float animator");
}
// 将创建好的 关键帧数组keyframes 包装成 FloatKeyframeSet类型 对象
// 这样封装的好处是 动画运行时更好的调用当前的关键帧
return new FloatKeyframeSet(keyframes);
}
FloatKeyframeSet是如何封装的呢?我们继续进入查看,进入 FloatKeyframeSet类(看到下面👇这段代码),发现直接是调用父类的构造方法,并且将入参一起往父类抛,所以我们进入父类 KeyframeSet 的构造方法。这里需要先说明下这几个类的继承关系,后面也会用到,注意看清这几个类和接口的名字。
graph LR
A[FloatKeyframeSet] --> B[KeyframeSet]
B[KeyframeSet] --> C[Keyframes]
A[FloatKeyframeSet] -.-> D[Keyframes.FloatKeyframes]
graph LR
A[FloatKeyframe] --> B[Keyframe]
进入 KeyframeSet 的构造函数,因为 FloatKeyframe 继承自 Keyframe,所以进入父类后,也就自动向上转型了。在这构造方法中,只是进行保存一些关键帧集合的状态,例如:关键帧集合的长度,将关键帧数组转换为列表等(具体看下面代码注释)
// FloatKeyframeSet 类
public FloatKeyframeSet(FloatKeyframe... keyframes) {
super(keyframes);
}
// KeyframeSet 类
public KeyframeSet(Keyframe... keyframes) {
// 保存 关键帧数量
mNumKeyframes = keyframes.length;
// 将 关键帧数组转 换为 不可变列表
mKeyframes = Arrays.asList(keyframes);
// 保存 第一个 关键帧
mFirstKeyframe = keyframes[0];
// 保存 最后一个 关键帧
mLastKeyframe = keyframes[mNumKeyframes - 1];
// 保存最后一个关键帧的插值器 在这场景中,这里的插值器为null
mInterpolator = mLastKeyframe.getInterpolator();
}
终于走到头了,我们需要折回去,到 FLAG(5),这里在粘贴出来一次(请看下面👇),运行接下来的一行代码,只是将 父类中 mKeyframes 属性从 Keyframes类型 强转为 FloatKeyframes 后保存在 mFloatKeyframes 属性中。
// FloatPropertyValuesHolder 类
public void setFloatValues(float... values) {
// 刚才一直是在讲解这一行代码的内容
super.setFloatValues(values);
// 这里是讲解这一句啦😄
mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}
小结一下
到这里 PropertyValuesHolder.ofFloat 的代码内容就走完了,我们再来小结一下
- PropertyValuesHolder 是一个用于装载 关键帧集合 和 属性动画名 的数据模型。
- 关键帧会被一个个存放在 Keyframe 类中,既 有多少个关键帧 则 有多少个Keyframe。
- Keyframe的fraction为 i / 关键帧的数量-1(i>=1),value 则为对应的关键帧数值。即将1进行等分为(关键帧数量-2)份,头尾两帧fraction则分别为0和1,中间帧则按顺序各占一份。
我们需要再折到 FLAG(3),进行查看 setValues 这一句是如何保存刚刚创建的 PropertyValuesHolder 对象。进入setValues,可以看到下面👇这段代码,
/**
* 将 PropertyValuesHolder组 进行保存。分别存于:
* 1、mValues ---------- PropertyValuesHolder组
* 2、mValuesMap ------- key = PropertyName属性名,value = PropertyValuesHolder
* <p>
* 值得注意:
* PropertyValuesHolder 中已经存有 关键帧
*/
public void setValues(PropertyValuesHolder... values) {
// 保存 PropertyValuesHolder 的长度,该场景长度为1
int numValues = values.length;
// 保存值
mValues = values;
mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
// 该场景中,这里只循环一次。因为 PropertyValuesHolder 只有一个
for (int i = 0; i < numValues; ++i) {
PropertyValuesHolder valuesHolder = values[i];
// 以 key为属性方法名zinc ----> value为对应的PropertyValuesHolder 保存到map中
mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
}
mInitialized = false;
}
至此,第一行代码就已经解析完毕,主要是进行 目标对象,属性方法名 和 关键帧 的包装和保存。已经在每小段代码后进行小结,这里就不再做冗余操作。
3、第二行代码
// 时长
objectAnimator.setDuration(2000);
来到第二行,进行设置动画时长,进入 setDuration,可以看到👇下面这段代码,是调用的父类ValueAnimator的setDuration方法,父类的setDuration方法进行值合法判断,然后保存至 mDuration 属性中。
// ObjectAnimator 类
public ObjectAnimator setDuration(long duration) {
super.setDuration(duration);
return this;
}
// ValueAnimator 类
private long mDuration = 300;
// ValueAnimator 类
public ValueAnimator setDuration(long duration) {
// 如果为负数,抛异常
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
// 保存时长
mDuration = duration;
return this;
}
值得注意
敲黑板啦,童鞋们注意到没,mDuration的初始值为300,也就是说,如果我们不进行动画时长的设置,动画时长就默认为300毫秒。
4、第三行代码
// 插值器
objectAnimator.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
return input;
}
});
接下来来到第三行设置插值器代码,进入后是直接来到 ValueAnimator 的 setInterpolator 方法,也就是说 ObjectAnimator 没有进行重写该方法。如果传入 setInterpolator 方法的参数为 null,则会默认提供 LinearInterpolator 插值器;如果传入了非空插值器,则保存至 mInterpolator 属性中。
// ValueAnimator 类
private static final TimeInterpolator sDefaultInterpolator =
new AccelerateDecelerateInterpolator();
// ValueAnimator 类
private TimeInterpolator mInterpolator = sDefaultInterpolator;
// ValueAnimator 类
public void setInterpolator(TimeInterpolator value) {
if (value != null) {
mInterpolator = value;
} else {
mInterpolator = new LinearInterpolator();
}
}
值得注意
当我们没有进行设置插值器时,默认的为我们初始化了 AccelerateDecelerateInterpolator 插值器,该插值器的走势如下动态图。
5、第四行代码
objectAnimator.setEvaluator(new FloatEvaluator());
第四行代码用于设置估值器,进入源码,这里同样也是进入到父类 ValueAnimator 的 setEvaluator 方法,ObjectAnimator 没有进行重写。估值器传入后,会对其进行合法性验证,例如:估值器非空,mValues非空且长度大于零。如果合法性验证不通过,则直接忽略传入的估值器。注意哦(再敲黑板!)估值器没有进行默认值的设置,至于他是如何正常运转的,其实我们在前面讲 “估值器定义” 一小节中就已经提到,但未深入探讨,当然这里也一样先不探讨,还未到时候,在 “第七行代码” 中会进行说明。
// ValueAnimator 类
PropertyValuesHolder[] mValues;
// ValueAnimator 类
public void setEvaluator(TypeEvaluator value) {
if (value != null && mValues != null && mValues.length > 0) {
mValues[0].setEvaluator(value);
}
}
值得注意
我们设置的估值器只会作用于 mValues 的第一项,但是我们这个场景中,也就只有一个元素。
mValues 是什么?我们在 “第一行代码” 中就已经初始化完毕啦,忘记的童鞋往回看看😄。看源码就是来来回回看,耐得住性子,才能更加牛x。
6、第五行代码
// 更新回调
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue().getClass());
Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue());
}
});
第五行代码用于设置更新回调,进入源码,来到下面这段代码,同样还是直接来到 其父类 ValueAnimator 中,这里就比较简单了,如果 mUpdateListeners 属性未初始化,就创建一个列表,然后将更新监听器添加入列表。
// ValueAnimator 类
ArrayList<AnimatorUpdateListener> mUpdateListeners = null;
// ValueAnimator 类
public void addUpdateListener(AnimatorUpdateListener listener) {
if (mUpdateListeners == null) {
mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
}
mUpdateListeners.add(listener);
}
值得注意
不知道童鞋们有没有和小盆友一样的错觉,一直以为 AnimatorUpdateListener 和 Animator.AnimatorListener(下一小节讲)各自只能设置一个,如果多次设置是会覆盖。看了源码才得知是有序列表持有。只怪自己之前太单纯😂。
7、第六行代码
// 生命周期监听器
objectAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.i(TAG, "onAnimationStart: ");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.i(TAG, "onAnimationEnd: ");
}
@Override
public void onAnimationCancel(Animator animation) {
Log.i(TAG, "onAnimationCancel: ");
}
@Override
public void onAnimationRepeat(Animator animation) {
Log.i(TAG, "onAnimationRepeat: ");
}
});
第六行代码进行设置生命周期监听器,进入源码,这次是来到 ObjectAnimator 的爷爷类Animator 的 addListener 方法。
这里有必要给出这几个类的继承关系和实现的接口,请记住他,因为在 “第七行代码” 中会提到。这里立个FLAG(6),需要时我们再折回来看看。
graph LR
A[ObjectAnimator] --> B[ValueAnimator]
B[ValueAnimator] --> C[Animator]
B[ValueAnimator] -.-> D[AnimationHandler.AnimationFrameCallback]
这里的逻辑和 “第五行的代码” 的源码逻辑可以说是一样的,都是先判空,如果为空,则进行实例化一个有序列表,然后将监听器放入列表中。
// Animator 类
ArrayList<AnimatorListener> mListeners = null;
// Animator 类
public void addListener(AnimatorListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<AnimatorListener>();
}
mListeners.add(listener);
}
8、第七行代码
// 开启
objectAnimator.start();
来到了最后的第七行代码,这行代码虽然简短,但却蕴含着最多的秘密,让我们来一点点揭开。进入start方法,看到如下代码。
// ObjectAnimator 类
public void start() {
// FLAG(7)
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
// 省略,用于调试时的日志输出,DBG 是被定义为 静态不可修改的 false,所以可以忽略这个分支
......
}
// FLAG(8)
super.start();
}
我们先进入第一行代码 的 第一小段,即 getInstance()。看到如下代码,其实看到getInstance这个方法名我们应该就会想到一个设计模式——单例模式,通过方法内的代码也确实验证了这个猜想。但是有些许不同的是单例对象放在 ThreadLocal 中,用于确保的是 线程单例,而非进程中全局单例,换句话说,不同线程的AnimationHandler对象是不相同的。
// AnimationHandler 类
/**
* 保证 AnimationHandler 当前线程单例
*/
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
// AnimationHandler 类
/**
* 获取 AnimationHandler 线程单例
*/
public static AnimationHandler getInstance() {
if (sAnimatorHandler.get() == null) {
sAnimatorHandler.set(new AnimationHandler());
}
return sAnimatorHandler.get();
}
经过上面代码,我们便获取到了 AnimationHandler 对象。AnimationHandler 是一个用于接受脉冲(即 垂直同步信号),让同一线程的动画使用的计算时间是相同的,这样的作用是让同步动画成为可能。 至于 AnimationHandler 是如何接受垂直同步信号,我们继续卖关子,稍后就会知道。
我们折回到 FLAG(7),看第二小段代码,具体代码如下,这里的代码其实在我们设定的场景中是不会运行的,因为 mAnimationCallbacks 此时长度还为0。但我们还是进行深入的分析,具体的每行代码讲解请看注释,总结起来就是 如果 mAnimationCallbacks列表中的元素 和 参数objectAnimator对象 存在相同的目标对象和相同的PropertyValuesHolder,则将mAnimationCallbacks列表中对应的元素进行取消操作。
// AnimationHandler 类
/**
* AnimationFrameCallback 此场景中就是 我们第一行代码实例化的 ObjectAnimator 对象,
* 因为 ObjectAnimator 的父类实现了 AnimationFrameCallback 接口,具体继承关系可以看 FLAG(6) 处的类图
*/
private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
new ArrayList<>();
// AnimationHandler 类
void autoCancelBasedOn(ObjectAnimator objectAnimator) {
// 场景中,mAnimationCallback 此时长度为0,所以其实此循环不会进入
for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
AnimationFrameCallback cb = mAnimationCallbacks.get(i);
if (cb == null) {
continue;
}
// 将 相同的目标对象 且 PropertyValuesHolder完全一样的动画进行取消操作
// 取消操作在 “第一行代码” 便已经详细阐述,这里就不再赘述
if (objectAnimator.shouldAutoCancel(cb)) {
((Animator) mAnimationCallbacks.get(i)).cancel();
}
}
}
// ObjectAnimator 类
/**
* 是否可以 进行取消
*/
boolean shouldAutoCancel(AnimationHandler.AnimationFrameCallback anim) {
// 为空,则返回
if (anim == null) {
return false;
}
if (anim instanceof ObjectAnimator) {
ObjectAnimator objAnim = (ObjectAnimator) anim;
// 该动画可以自动取消 且 当前的对象和anim 的所持的目标对象和PropertyValuesHolder一样
// 则可以 进行取消,返回true
if (objAnim.mAutoCancel && hasSameTargetAndProperties(objAnim)) {
return true;
}
}
// 否则不取消
return false;
}
// ObjectAnimator 类
/**
* ObjectAnimator 是否有相同的 目标对象target 和 PropertyValuesHolder
* PropertyValuesHolder 的初始化在第一行代码已经详细讲述,忘记的童鞋折回去再👀看一遍
*/
private boolean hasSameTargetAndProperties(@Nullable Animator anim) {
if (anim instanceof ObjectAnimator) {
// 获取 PropertyValuesHolder
PropertyValuesHolder[] theirValues = ((ObjectAnimator) anim).getValues();
// 目标对象相同 且 PropertyValuesHolder长度相同
if (((ObjectAnimator) anim).getTarget() == getTarget() &&
mValues.length == theirValues.length) {
// 循环检测 PropertyValuesHolder 中 属性名是否 “完全相同”,只要有一个不同 则返回false
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvhMine = mValues[i];
PropertyValuesHolder pvhTheirs = theirValues[i];
if (pvhMine.getPropertyName() == null ||
!pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) {
return false;
}
}
// 全部相同,返回true
return true;
}
}
// 不是 ObjectAnimator 直接返回false
return false;
}
看完首行代码,我们来到调用 父类ValueAnimator 的 start 方法这行FLAG(8),进入该方法,具体代码如下,可以看到调用了 start() 的重载方法 start(boolean),playBackwards是用于标记是否要反向播放,显然传入的为false,表示正向播放。start方法中做了这几件事:
- 初始化一些属性,例如运行的状态标记(注意此处 开始状态mStarted便置为true);
- 帧和动画的播放时间则置为-1;
- 添加动画回调,用于接受 垂直同步信号;
- 设置当前的播放 fraction;
// ValueAnimator 类
public void start() {
start(false);
}
// ValueAnimator 类
/**
* @param playBackwards ValueAnimator 是否应该开始反向播放。
*/
private void start(boolean playBackwards) {
// 必须要在有 looper 的线程中运行
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
// 是否反向
mReversing = playBackwards;
// 是否接受脉冲,mSuppressSelfPulseRequested初始化为false,所以这里为true,表示接受脉冲
mSelfPulse = !mSuppressSelfPulseRequested;
// 此处 playBackwards 为false,该分支不理会,处理反向播放
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
// 将开始状态(mStarted)置为true
// 暂停状态(mPaused)置为false
// 运行状态(mRunning)置为false
// 是否终止动画状态(mAnimationEndRequested)置为false
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// 重置 mLastFrameTime,这样如果动画正在运行,调用 start() 会将动画置于已启动但尚未到达的第一帧阶段。
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
// 添加动画回调,用于接受 垂直同步信号
addAnimationCallback(0);
// 此场景中,mStartDelay为0,所以进入分支
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// FLAG(14)
startAnimation();
// 设置 mSeekFraction,这个属性是通过 setCurrentPlayTime() 进行设置
if (mSeekFraction == -1) {
// FLAG(16)
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}
属性的初始化和各自意义,我们就不单独讲解,使用到的时候自然就能体会到他的存在意义。所以我们直接进入到第三步,即添加动画回调addAnimationCallback的代码。
这里进行判断是否要接受脉冲,我们上面的代码已经将 mSelfPulse设置为true,表示需要接受脉冲,所以不进入if分支,来到下一行代码,是不是很熟悉?这里获取的便是我们上面已经初始化的 AnimationHandler,这里调用了 AnimationHandler 的 addAnimationFrameCallback,同时把 自己this 和 延时delay(这里为0)一同带入。
// ValueAnimator 类
private void addAnimationCallback(long delay) {
// 如果不接受脉冲,则不会添加回调,这样自然就中断了脉冲带来的更新
// 在 start 方法中已经设置为 true,所以不进入if分支
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
// ValueAnimator 类
public AnimationHandler getAnimationHandler() {
return AnimationHandler.getInstance();
}
这样我们便来到了 AnimationHandler 的 addAnimationFrameCallback 方法,根据该方法的官方注释可知,注册的callback会在下一帧调用,但需要延时指定的delay之后,可是我们这里的delay为0,所以在我们这场景中可以进行忽略,减少干扰因素。
来到第一行,因为 mAnimationCallbacks 此时长度为0,所以进入该if分支。 我们需要先进入 getProvider() 方法,待会再折回来,往下看。
/**
* Register to get a callback on the next frame after the delay.
* 注册回调,可以让下一帧进行回调。
*/
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
/**
* 第一次进来的时候 mAnimationCallbacks 是空的,
* 所以会向 {@link MyFrameCallbackProvider#mChoreographer} 提交一次回调。
*/
if (mAnimationCallbacks.size() == 0) {
// FLAG(9)
getProvider().postFrameCallback(mFrameCallback);
}
/**
* 此处的 callback 即为 ValueAnimator 和 ObjectAnimator
* 因为 ObjectAnimator 继承于 ValueAnimator,ValueAnimator 实现了 AnimationFrameCallback 接口
* 这里 callback 从 {@link ValueAnimator#start()} 传进来,使用了 this
*/
// FLAG(13)
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
// 记录延时的回调 和 延时的时间
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
来到 getProvider 方法,这里初始化一个 MyFrameCallbackProvider 对象,他负责与 Choreographer 进行交互。
这里值得一提的是 MyFrameCallbackProvider 实现了 AnimationFrameCallbackProvider 接口(关系如下图所示),而AnimationHandler中,提供定时的帧回调,并不是规定一定要通过 Choreographer 来接收垂直同步来达到效果,也可以自己行实现 AnimationFrameCallbackProvider 接口,自行提供不同的定时脉冲来实现效果,来顶替这里的 MyFrameCallbackProvider。AnimationHandler 同时也提供了 setProvider 方法来进行设置该 AnimationFrameCallbackProvider 类。
graph LR
A[MyFrameCallbackProvider] -.-> B[AnimationFrameCallbackProvider]
// AnimationHandler 类
private AnimationFrameCallbackProvider getProvider() {
if (mProvider == null) {
mProvider = new MyFrameCallbackProvider();
}
return mProvider;
}
// AnimationHandler 类
/**
* 使用 Choreographer 提供定时脉冲 进行帧回调
*/
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
final Choreographer mChoreographer = Choreographer.getInstance();
// 省略 接口的实现
......
}
话说回来 Choreographer 是什么?
Choreographer 是用于接收定时脉冲(例如 垂直同步),协调 “动画、输入、绘制” 时机的类。 我们这里不展开阐述 Choreographer 内部的运转机制,但是我们必须知道的是,Android手机每秒会有60帧的回调,即约16.66毫秒便会调用一次 Choreographer 中的 类型为FrameDisplayEventReceiver的mDisplayEventReceiver属性 中的 onVsync 方法。后续还会继续用到这里的知识点(敲黑板了,要考的),来讲解属性动画是怎么动起来的,我们先打个标记FLAG(10)。
我们先折回 FLAG(9),看后半段 postFrameCallback 做了什么操作,进入代码,具体如下,这里做了一件很重要的事,就是 注册进Choreographer,接收垂直同步信号。Choreographer 中多次重载了 postFrameCallbackDelayed 方法,最终在FLAG(10)处,将我们从 MyFrameCallbackProvider 传入的 callback 保存在了 Choreographer 的 mCallbackQueues 中,这里需要在打一个标记FLAG(11),后续需要再用到。
// AnimationHandler$MyFrameCallbackProvider 类
@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}
// Choreographer 类
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
// Choreographer 类
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
// Choreographer 类
private void postCallbackDelayedInternal(int callbackType,
Object action,
Object token,
long delayMillis) {
if (DEBUG_FRAMES) {
// 调试时,日志输出
......
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
/**
* 此处将 {@link FrameCallback} 添加到对应的回调队列中
* FLAG(10)
*/
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
我们需要再次折回 FLAG(9),需要说明下传入的参数 mFrameCallback, 实现了 Choreographer.FrameCallback 接口,这里面会调用 doAnimationFrame 方法,这个先不展开,待会讲到帧回调时,在具体剖析。先来到下面的if分支,用于将自己(mFrameCallback)再次添加进 Choreographer,运行的逻辑和上面刚刚阐述的逻辑是一模一样。为什么还要再添加一次呢?这是因为添加进的回调,在每次被调用后就会被移除,如果还想继续接收到垂直信号,则需要将自己再次添加。
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
// FLAG(12)
// 这里后面讲
doAnimationFrame(getProvider().getFrameTime());
/**
* 再次将自己添加进脉冲回调中
* 因为 {@link Choreographer#postFrameCallback(Choreographer.FrameCallback)} 每调用一次
* 就会将添加的回调移除
*/
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
折回到FLAG(13),就是下面这段代码,做了很普通的一件事,就是把我们在 “第一行代码” 实例化的 ObjectAnimator 对象存至 mAnimationCallbacks 回调列表中。接下去的分支,我们这场景中不需理会,因为我们不做延时操作。
/**
* 此处的 callback 即为 ValueAnimator 和 ObjectAnimator
* 因为 ObjectAnimator 继承于 ValueAnimator,ValueAnimator 实现了 AnimationFrameCallback 接口
* 这里 callback 从 {@link ValueAnimator#start()} 传进来,使用了 this
*/
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
我们需要折回到FLAG(14),进入到 startAnimation 方法中,具体代码如下,这个方法做了如下几个步骤:
- 初始化动画,具体是设置关键帧集合的估值器;
- 将运行状态置为true;
- 设置 mOverallFraction(这个属性我们后面用到时在说明是什么作用);
- 回调 监听器;
接下来我们分析第一和第四小点
// ValueAnimator 类
private void startAnimation() {
// 省略跟踪代码
......
mAnimationEndRequested = false;
// 初始化 动画
initAnimation();
// 将运行状态置为 true
mRunning = true;
if (mSeekFraction >= 0) {
mOverallFraction = mSeekFraction;
} else {
// 跟踪动画的 fraction,范围从0到mRepeatCount + 1
// mRepeatCount 为我们设置动画循环次数,我们这里没有设置,则默认为0,只运行一次
mOverallFraction = 0f;
}
// FLAG(15)
if (mListeners != null) {
// 进行开始回调
notifyStartListeners();
}
}
先进行分析第一小点,我们进入 initAnimation 方法,当首次进入时,mInitialized 为false,所以进入该分支,这里循环调用了 mValues元素(PropertyValuesHolder类型) 的 init 方法。
// ValueAnimator 类
void initAnimation() {
// 当首次进入时,mInitialized为false
// 初始化 估值器Evaluator
if (!mInitialized) {
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].init();
}
// 将初始化标记 转为true,防止多次初始化
mInitialized = true;
}
}
进入到 PropertyValuesHolder 的 init 方法中,代码如下,该方法做了一件事,就是初始化 mKeyframes 的 估值器,而这估值器在我们讲述 “第四行代码” 时,就已经置入到 PropertyValuesHolder 中,这一方法是将这个估值器置入关键帧集合中。
// PropertyValuesHolder 类
/**
* 初始化 Evaluator 估值器
*/
void init() {
/**
* 如果 Evaluator 为空,则根据 mValueType 类型进行设置,但是也只是提供
* {@link IntEvaluator} 和 {@link FloatEvaluator}
* 如果均不是这两种类型,则为null
*/
if (mEvaluator == null) {
// We already handle int and float automatically, but not their Object
// equivalents
mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
(mValueType == Float.class) ? sFloatEvaluator :
null;
}
// 如果有估值器,则进行设置
if (mEvaluator != null) {
// KeyframeSet knows how to evaluate the common types - only give it a custom
// evaluator if one has been set on this class
mKeyframes.setEvaluator(mEvaluator);
}
}
这里需要说句题外话,下面下面这段代码,返回的是false,也就是说👆上面设置估值器的代码中,当mEvaluator为空时,如果我们使用的简单类型的float,此处的并不会使用默认的sFloatEvaluator,而是还是为null。
System.out.println(float.class == Float.class); // 输出的是false
接下来进行分析第四小点,折回到FLAG(15),此时 mListeners 已经在 “第六行代码” 时就初始化,并添加了一个监听器,所以会进入该if分支,进入 notifyStartListeners 方法,具体代码如下,这里便回调到了我们 “第六行代码” 设置的生命周期监听器的 onAnimationStart 方法中,同时将自身 ObjectAnimator 对象作为参数带出。
// ValueAnimator 类
private void notifyStartListeners() {
// 有 回调监听器,且从未回调
if (mListeners != null && !mStartListenersCalled) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
/**
* 进行回调开始,这里便 回调到 我们设置
* {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
* 的方法中
*/
tmpListeners.get(i).onAnimationStart(this, mReversing);
}
}
mStartListenersCalled = true;
}
// Animator$AnimatorListener 类
default void onAnimationStart(Animator animation, boolean isReverse) {
onAnimationStart(animation);
}
这里一路调用进来,算是比较深远了,但无大碍,我们回到FLAG(16),继续看 start 方法的最后一行代码 setCurrentPlayTime(0) 的具体内容,代码如下,这方法是为了让如果动画时长小于或等于零时,直接到达动画的末尾,即fraction置为1。
// ValueAnimator 类
public void setCurrentPlayTime(long playTime) {
// 如果设置的 mDuration 为 2000,playTime 为 0,则 fraction 为 0
// 如果设置的 mDuration 为 0,则 fraction 为 1,直接到最后一个关键帧
float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
setCurrentFraction(fraction);
}
接下来看 setCurrentFraction 做了什么操作,第一行的 initAnimation 在之前就已经运行过了,所以并不会再次初始化,clampFraction方法是为了让 fraction 落在合法的区域内,即 [0,mRepeatCount + 1],这里不再展开(篇幅太长了😂)。
来到 if-else,isPulsingInternal方法内判断的mLastFrameTime 是否大于等于0,但是 mLastFrameTime 到目前为止还是 -1,所以进入else分支,将fraction保存至mSeekFraction。
// ValueAnimator 类
public void setCurrentFraction(float fraction) {
// 初始化 动画,但这里其实只是做了 估值器的赋值初始化
initAnimation();
// 获取 合法的fraction
fraction = clampFraction(fraction);
mStartTimeCommitted = true;
// 动画是否已进入动画循环
if (isPulsingInternal()) {
// 获取动画已经使用的时长
long seekTime = (long) (getScaledDuration() * fraction);
// 获取当前动画时间
long currentTime = AnimationUtils.currentAnimationTimeMillis();
// 仅修改动画运行时的开始时间。 Seek Fraction将确保非运行动画跳到正确的开始时间。
mStartTime = currentTime - seekTime;
} else {
// 如果动画循环尚未开始,或者在开始延迟期间。seekTime,一旦延迟过去,startTime会基于seekTime进行调整。
mSeekFraction = fraction;
}
// 总fraction,携带有迭代次数
mOverallFraction = fraction;
// 计算当次迭代的 fraction
final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
// 根据 fraction ,计算出对应的value
// FLAG(17)
animateValue(currentIterationFraction);
}
接下来是 getCurrentIterationFraction 方法,这个方法用于获取当前迭代的 fraction,因为我们需要知道的是,如果设置了多次循环播放动画,即mRepeatCount>0时,则fraction是包含有mRepeatCount次数的。而这个方法就是去除了次数,只剩下当次的进度,即范围为 [0,1] 。
// ValueAnimator 类
private float getCurrentIterationFraction(float fraction, boolean inReverse) {
// 确保 fraction 的范围在合法范围 [0,mRepeatCount+1] 中
fraction = clampFraction(fraction);
// 当前迭代次数
int iteration = getCurrentIteration(fraction);
/**
* fraction 是 包含有 mRepeatCount 的值
* iteration 是 迭代的次数
* 两者相减 fraction - iteration 得出的 currentFraction 则为当前迭代中的 进度
*/
float currentFraction = fraction - iteration;
// 计算最终当次迭代的 fraction 值,主要是受 inReverse 和 REVERSE 的影响
return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;
}
我们回到 FLAG(17),进入 animateValue 方法,具体代码如下。我们需要明确的是,传进来的参数是当次的进度,也就是不含循环次数的。
我们会先进入到 ObjectAnimator 的 animateValue,但是源码会先进入其父类 ValueAnimator 的 animateValue。所以我们先进入父类的 animateValue。
看到第一行代码,你或许就明白了,我们在 “第三行代码” 设置的插值器就在这个时候发挥作用了,同时会 将插值器返回的值设置回 fraction,起到改变进度的快慢的作用 。(这便揭开了我们在 “插值器” 一节中卖的关子)
// ObjectAnimator 类
void animateValue(float fraction) {
final Object target = getTarget();
// 不会进入,此时的target在 “第一行代码” 时就设置了,所以不为空
if (mTarget != null && target == null) {
cancel();
return;
}
// 进入父类 ValueAnimator
super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
// FLAG(25)
mValues[i].setAnimatedValue(target);
}
}
// ValueAnimator 类
void animateValue(float fraction) {
// 通过 插值器 进行计算出 fraction
fraction = mInterpolator.getInterpolation(fraction);
// 当前次数的进度
mCurrentFraction = fraction;
// 循环 所有的 PropertyValuesHolder,进行估值器计算
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
/**
* 进行回调 {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}
*/
// FLAG(21)
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
紧接着进行循环调用 mValues 中的元素的 calculateValue 方法(我们这场景中 mValues 的元素其实只有一个),进入该方法,可以看到如下代码。这里进入的是 PropertyValuesHolder 的子类 FloatPropertyValuesHolder。
// FloatPropertyValuesHolder 类
void calculateValue(float fraction) {
// mFloatKeyframes 在 “第一行代码” 时就已经初始化
mFloatAnimatedValue = mFloatKeyframes.getFloatValue(fraction);
}
我们接着进入 getFloatValue 方法,其具体实现类是 FloatKeyframeSet。具体代码如下,getFloatValue 中分了几个情况,我们接下来分情况讨论,往下走。
// FloatKeyframeSet 类
/**
* 获取当前 进度数值为fraction的 value
* 落实一个场景: 0f, 2f, 5f
* mKeyframes 中存了三个 FloatKeyframe
* mNumKeyframes 则为 3
*
* @param fraction The elapsed fraction of the animation
* @return
*/
@Override
public float getFloatValue(float fraction) {
// FLAG(18)
if (fraction <= 0f) {
// 获取 0f 关键帧
final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
// 获取 2f 关键帧
final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1);
// 获取 0f 关键帧的值 即 0f
float prevValue = prevKeyframe.getFloatValue();
// 获取 2f 关键帧的值 即 1f
float nextValue = nextKeyframe.getFloatValue();
// 获取 0f 关键帧的fraction,这里为0
float prevFraction = prevKeyframe.getFraction();
// 获取 2f 关键帧的fraction,这里为1/2
float nextFraction = nextKeyframe.getFraction();
// 这里的插值器为空,并不会运行该分支
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
return mEvaluator == null ?
prevValue + intervalFraction * (nextValue - prevValue) :
((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
floatValue();
} else if (fraction >= 1f) {// FLAG(19)
final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2);
final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1);
float prevValue = prevKeyframe.getFloatValue();
float nextValue = nextKeyframe.getFloatValue();
float prevFraction = prevKeyframe.getFraction();
float nextFraction = nextKeyframe.getFraction();
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
return mEvaluator == null ?
prevValue + intervalFraction * (nextValue - prevValue) :
((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
floatValue();
}
// FLAG(20)
// 初始化第一帧, 0f
FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
// 从第二帧开始循环
for (int i = 1; i < mNumKeyframes; ++i) {
// 相对于prevKeyframe,取下一帧
FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i);
// 判断是否落在 该区间
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
float intervalFraction = (fraction - prevKeyframe.getFraction()) /
(nextKeyframe.getFraction() - prevKeyframe.getFraction());
float prevValue = prevKeyframe.getFloatValue();
float nextValue = nextKeyframe.getFloatValue();
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
// 估值器计算
return mEvaluator == null ?
prevValue + intervalFraction * (nextValue - prevValue) :
((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
floatValue();
}
// 变换前一帧
prevKeyframe = nextKeyframe;
}
// 正常情况下不应该运行到这
return ((Number) mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue();
}
以下图片均为手写,勿喷😄
情况一:fraction = 0。进入FLAG(18)
情况二:fraction = 1/4。进入FLAG(20)
情况三:fraction = 3/4。进入FLAG(20)
情况四:fraction = 1。进入FLAG(19)
经过上面四种情况,我们可以知道 intervalFraction 值,即为 当前帧段的比例数(帧段即为 0f-2f,2f-5f) 而 返回值 即为 fraction 通过估值器转换为 真实需要的值,即我们程序员可以拿来用,例如我们这里需要的是 0f-5f的值。
还记得我们在 “估值器” 一小节中卖的关子么?情况二中的公式就是我们用于计算 此处的返回值,在估值器为null时。如果估值器不为null,则按照设置的估值器逻辑计算。
mEvaluator == null ?
prevValue + intervalFraction * (nextValue - prevValue) :
((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
floatValue();
你可能会有疑惑,我们这场景中不是有设置一个 FloatEvaluator 估值器么?确实是,但FloatEvaluator内部逻辑其实就和我们估值器为null时是一模一样的。具体代码如下
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
至此我们得到了 估值器计算出来的我们需要的值。
我们折回 FLAG(21),看到 mUpdateListeners 这属性,童鞋们应该也知道这是在 “第五行代码” 设置的更新监听器,进入该分支,会循环着调用更新监听器的 onAnimationUpdate 方法。这便进入到了我们设置的更新监听器的代码中。
接下来我们需要折回到父类 ObjectAnimator 的 animateValue,即FLAG(25)。会进行循环调用 mValues 的 setAnimatedValue,而我们这里的场景的 mValues 为 FloatPropertyValuesHolder 类型,所以会调用该类的 setAnimatedValue, 具体代码如下。而 FLAG(26) 就是将我们刚才上面所计算的 mFloatAnimatedValue 值通过 native方法nCallFloatMethod 设置到对应的对象的方法中,也就是我们此处场景中的 ZincView类的setZinc方法。
// FloatPropertyValuesHolder 类
void setAnimatedValue(Object target) {
if (mFloatProperty != null) {
mFloatProperty.setValue(target, mFloatAnimatedValue);
return;
}
if (mProperty != null) {
mProperty.set(target, mFloatAnimatedValue);
return;
}
if (mJniSetter != 0) {
// FLAG(26)
nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
return;
}
if (mSetter != null) {
try {
mTmpValueArray[0] = mFloatAnimatedValue;
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
至此,我们一个流程走完,但并不代表着就已经完成了,因为这里面还只是第一帧的回调,而后续的帧回调还未阐述,还有动画的终止还未说清。所以我们继续前行,先来 解决后续帧回调问题。
还记得我们在讲 Choreographer 时,通过 AnimationHandler 注入了一个回调么?这个时候后续的帧回调就全靠他了。我们前面说过每次 “垂直同步” 信号的到来,回调用到 Choreographer$FrameDisplayEventReceiver 的 onVsync 的,而该方法最终会调用到我们在FLAG(11)放入回调队列 mCallbackQueues 中的 mFrameCallback 的 doFrame 方法。这就回到了我们FLAG(12)标记的地方。
// AnimationHandler 类
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mCurrentFrameTime = System.currentTimeMillis();
doAnimationFrame(mCurrentFrameTime);
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
进入 doAnimationFrame 方法,便看到对 mAnimationCallbacks 进行了遍历,调用 doAnimationFrame 方法。 而 mAnimationCallbacks 是我们在讲解 addAnimationFrameCallback方法时,就将传进来的 ObjectAnimator 对象放入其中的。
// AnimationHandler 类
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
// isCallbackDue 方法用于剔除需要延时调用的回调
// 如果该 callback 是在延时队列的,并且延时还未完成,不进行回调
if (isCallbackDue(callback, currentTime)) {
// 进入这一行
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}
cleanUpList();
}
当调用 doAnimationFrame 方法,则来到了下面的这段代码,该方法主要是对 启动时间进行容错处理,然后保证动画进行启动,同时在 animateBasedOnTime 方法中进行更新的监听回调(我们接下来分析),最后根据 animateBasedOnTime 的返回值,判断是否动画已经结束,结束的话进行 动画生命周期 的回调(待会也会分析)。
// ValueAnimator 类
public final boolean doAnimationFrame(long frameTime) {
// 初始化第一帧,同时考虑延时
if (mStartTime < 0) {
mStartTime = mReversing ? frameTime : frameTime + (long) (mStartDelay * sDurationScale);
}
// 处理 暂停 和 恢复 的情况,这里我们不考虑
if (mPaused) {
mPauseTime = frameTime;
removeAnimationCallback();
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
mStartTime += (frameTime - mPauseTime);
}
}
// mRunning 在 startAnimation()方法中就被置为了 true
// 但实际代码情况是 先添加回调,再调用 startAnimation方法
// 所以有可能会出现 帧回调 快于 startAnimation方法 先运行,
// 如果出现这种情况,则此时的 mRunning状态值为false,就进入此分支进行处理
if (!mRunning) {
// 处理延时操作,如果未延时,此时的 mStartTime==frameTime,在首行代码便是做这操作
if (mStartTime > frameTime && mSeekFraction == -1) {
return false;
} else {
// 如果还未运行,则先将 mRunning置为true,然后启动动画,startAnimation的逻辑在前面已经阐述
mRunning = true;
startAnimation();
}
}
// 第一次进来时,mLastFrameTime为-1,则进入分支
// mLastFrameTime用于记录 最后一帧到达的时间(以毫秒为单位)
if (mLastFrameTime < 0) {
// 这里是进行 mStartTime 的调整,因为 初始化开始时间 和 实际绘制帧 之间是有可能存在偏差
// 我们这场景中 mSeekFraction 一直为 -1,所以无需理会
if (mSeekFraction >= 0) {
long seekTime = (long) (getScaledDuration() * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false;
}
// 刷新最后一帧到达的时间
mLastFrameTime = frameTime;
// 这一句是为了保证 当前帧时间 必须在开始的时间之后。
// 保证不会逆向而行的出现,但这种情况很少见。
final long currentTime = Math.max(frameTime, mStartTime);
// 这里面 便进行了值的回调,我们接下来具体分析
boolean finished = animateBasedOnTime(currentTime);
// 是否动画已结束,结束的话进行生命周期的回调通知
if (finished) {
endAnimation();
}
return finished;
}
进入 animateBasedOnTime 方法,该方法会通过当前时间计算出当前动画的进度,最后通过 animateValue 方法,进行更新回调,这样就达到了 后续帧 的更新目的。
// ValueAnimator 类
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
// 获取缩放时长,但缩放因子为 1,所以一直为动画时长
final long scaledDuration = getScaledDuration();
// 计算 fraction ,其实就是 已经运行时间占 动画时长的百分比
final float fraction = scaledDuration > 0 ?
(float) (currentTime - mStartTime) / scaledDuration : 1f;
// 获取 动画的整体进度 (带循环次数)
final float lastFraction = mOverallFraction;
// 是否为新的迭代
final boolean newIteration = (int) fraction > (int) lastFraction;
// 最后一次迭代完成
// FLAG(22)
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
// 如果时长为0,则直接结束
if (scaledDuration == 0) {
// 0时长的动画,忽略重复计数 并 结束动画
done = true;
} else if (newIteration && !lastIterationFinished) { // 为新的迭代 且 不是最后一次
// 回调 动画循环次数
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) { // 最后一次
done = true;
}
// 更新 动画的整体进度
mOverallFraction = clampFraction(fraction);
// 当前迭代的fraction(即不包含迭代次数),getCurrentIterationFraction方法在前面已经分析
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
// 进行值更新回调
animateValue(currentIterationFraction);
}
return done;
}
最后就是 动画终止 的问题,我们前面也提到了根据 animateBasedOnTime的返回值来决定是否终止动画,而在 animateBasedOnTime 方法中,返回true的地方,有两个:
- 动画时长为0;
- 最后一次迭代完毕,至于判断是否完成最后一次迭代,则通过判断当前进度是否已经大于我们循环的次数,并且动画不是无限循环播放,判断的代码可以看FLAG(22)。
如果 animateBasedOnTime 返回了true,便执行终止代码,即执行 endAnimation 方法,具体代码如下。可以看到,该方法主要是执行标记位的复位、回调的清楚、生命周期监听器回调。在FLAG(23)的代码,则最终回调到我们在 “第六行代码” 时设置的 生命周期监听器。
private void endAnimation() {
// 如果已经终止了,就不再重复执行
if (mAnimationEndRequested) {
return;
}
// 移除 回调
removeAnimationCallback();
// 将 动画 置为已经 终止
mAnimationEndRequested = true;
mPaused = false;
boolean notify = (mStarted || mRunning) && mListeners != null;
/**
* 如果有 需要回调, 但还未进行运行,说明 需要先回调一次
* {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
*/
if (notify && !mRunning) {
notifyStartListeners();
}
mRunning = false;
mStarted = false;
mStartListenersCalled = false;
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
/**
* 调用回调 {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)}
*/
if (notify && mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
// FLAG(23)
tmpListeners.get(i).onAnimationEnd(this, mReversing);
}
}
mReversing = false;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}
}
最后我们还需要折回去FLAG(24),说下我们经常用来终止动画的 cancel 方法。cancel 方法的具体代码如下,我们会发现如果已经开始动画,但未运行(即mRunning 为 false),则会先走一次 notifyStartListeners 方法,保证调用了 生命周期监听器中的 onAnimationStart 方法,紧接着调用了 onAnimationCancel 方法,最后执行我们上面提到的 endAnimation 方法进行终止动画,并且回调 onAnimationEnd 方法。
@Override
public void cancel() {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
/**
* 如果已经请求结束,则通过前一个end()或cancel()调用,执行空操作
* 直到动画再次启动。
*/
if (mAnimationEndRequested) {
return;
}
/**
* 当动画已经开始 或 已经运行 并且需要回调
*/
if ((mStarted || mRunning) && mListeners != null) {
/**
* 如果还没运行,则先进行回调 {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
*/
if (!mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
notifyStartListeners();
}
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
/**
* 进行回调 {@link android.animation.Animator.AnimatorListener#onAnimationCancel(Animator)}
*/
listener.onAnimationCancel(this);
}
}
// 进行终止动画
endAnimation();
}
至此,属性动画的源码分析便完成了。
四、实战
1、多维雷达图
文章开头出现的就是以下效果图,现在我们来进行拆解实现。
效果图
动画分析
绘制相对应维度的雷达图,在设置完数据后,进行设置属性动画,最后根据属性动画回调值进行每个维度的展开。emmm,有些抽象。我们进行拆解为需要的零件:
- 每个顶点的坐标;
- 维度展开的属性动画;
准备零件
(1)顶点坐标 一图胜千言,我们以六维雷达图为例,以比较有代表性的A,B,C三点来计算其坐标。但这里面有一个前提是,需要将画布的原点移至view的中心。接下来具体的计算请看图,中间涉及到一些简单的三角函数,这里就不过多的说明。
根据图片中的计算规则,我们可以得知以 画布的负y轴 为基准,依次使用 sin(角度) * L 得出该点的 x坐标,用 cos(角度) * L 得出该点的 y坐标 。具体的代码如下:
// 循环遍历计算顶点坐标
for (int i = 0; i < mDimenCount; ++i) {
PointF point = new PointF();
// 当前角度
double curAngle = i * mAngle;
// 转弧度制
double radian = Math.toRadians(curAngle);
// 计算其 x、y 的坐标
// y轴需要进行取反,因为canvas的坐标轴和我们数学中的坐标轴的y轴正好是上下相反的
point.x = (float) (mLength * Math.sin(radian));
point.y = (float) -(mLength * Math.cos(radian));
mVertexList.add(point);
}
(2)维度展开的属性动画 从第一小节我们得到了所有顶点的坐标,再根据传入的数据(数据是以百分比传入,即0f-1f),便可以计算出每个维度的数据的最终顶点坐标,具体代码如下
/**
* 计算数据的顶点坐标
*
* @param isBase 是否为 基础数据
*/
private void calculateDataVertex(boolean isBase) {
List<Data> calDataList = isBase ? mBaseDataList : mDataList;
for (int i = 0; i < calDataList.size(); ++i) {
Data data = calDataList.get(i);
// 获取 比例数据
List<Float> pointDataList = data.getData();
// 设置路径
Path curPath = new Path();
data.setPath(curPath);
curPath.reset();
for (int j = 0; j < pointDataList.size(); ++j) {
// 当前维度的数据比例
float ratio = pointDataList.get(j);
// 当前维度的顶点坐标
PointF curDimenPoint = mVertexList.get(j);
if (j == 0) {
curPath.moveTo(curDimenPoint.x * ratio,
curDimenPoint.y * ratio);
} else {
curPath.lineTo(curDimenPoint.x * ratio,
curDimenPoint.y * ratio);
}
}
curPath.close();
}
}
经过以上代码的计算,得到每个数据中每个维度的最终顶点最坐标,最后就是设置的属性动画起始值和终止值,以及更新处理。
起始值当然是 0,而终止值是 数据量个数 * (维度数-1),动画时长为 每个维度的动画时长 * 终止值。具体如下代码
mTotalLoopCount = (mDimenCount - 1) * mDataList.size();
mAnimator = ValueAnimator.ofFloat(0f, mTotalLoopCount);
mAnimator.setDuration(DURATION * mTotalLoopCount);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
// 整数部分即为当前的动画数据下标
mCurLoopCount = (int) value;
// 小数部分极为当前维度正在展开的进度百分比
mAnimCurValue = value - mCurLoopCount;
invalidate();
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// 动画结束,将状态置为初始状态,并再刷新一次,让最后的数据全部显示
mCurState = INIT;
invalidate();
}
});
最后就是如何将这个值使用起来,因为我们传入的是浮点数,所以在 AnimatorUpdateListener 回调时,获得的数会有 整数部分 和 小数部分 ,对 整数部分 进行 除以(维度数-1),得到 当前的数据量下标;对 整数部分 进行 (维度数-1)取余,再加1,得到 当前数据的维度数,而小数部分就是我们的维度进度。 代码如下:
// 当前数据的下标(-1因为第一个维度不用动画)
int curIndex = mCurLoopCount / (mDimenCount - 1);
// 当前数据的维度(-1因为第一个维度不用动画)
int curDimen = (mCurLoopCount % (mDimenCount - 1)) + 1;
组装零件
零件都已经备好了,组装起来就是我们看到的效果。因为代码稍微较长,但主要点我们已经攻破了,并且代码注释也比较多,这里就不再贴出来了,需要的请进传送门。
2、表盘指示器
文章最开始出现的第二个就是以下这张效果图,具体的操作其实和 “多维雷达图” 没有太多的出入,只是将维度的展开,变为 画布的旋转后绘制指针,达到指针旋转的效果,再加上插值器的公式辅助,到达摆动回荡的效果。限于文章篇幅过长这里就不再具体阐述,有兴趣的同学请入传送门。
效果图
五、写在最后
属性动画是高级UI中又一把不可缺少的利器,使用得当,能让整个界面富有交互感,提高用户体验。最后如果你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中如有理解错误或是晦涩难懂的语句,请评论区留言,我们进行讨论共同进步。你的鼓励是我前进的最大动力。
高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄