内容简介:目录一、前言二、插值器与估值器
终于在新的一年的第一天完成了本篇文章,小盆友在此祝贺您,万事如意,阖家幸福。:smile:
目录
一、前言
二、插值器与估值器
三、源码解析
四、实战
五、写在最后
一、前言
对于越来越追求丰富的交互体验的客户端,一个带有动态效果的界面已经是不可避免。 属性动画 就是这其中必不可少的一把利器,所以今天便来分享下 属性动画融合在实际场景中 的使用,以及进行 源码分析 。话不多说,先看看今天的实战效果图,然后开始进行分享之旅。
1、多维雷达图
2、表盘指示器
3、活力四射的购物车老规矩,代码在实战篇会给出,透过 API 看清源码,各种效果便是顺手拈来:smile:
二、插值器与估值器
对于属性动画来说,一定是绕不开 插值器 与 估值器 。接下来便一一道来
文章更多的使用 我在开发过程中 自我理解 的词语,而尽量不使用 教科书式 或 直接翻译注释 语句。如果有 晦涩难懂 或是 理解错误之处,欢迎 评论区 留言,我们进行探讨。
1、插值器(TimeInterpolator)
(1)定义
是一个控制动画 进度 的工具。
为何如此说?咱们可以这么理解,我们借助 SpringInterpolator 插值器的函数图来讲解。
- x轴为 时间 。0表示还未开始,1表示时间结束。时间是 不可逆的 ,所以会一直往前推进,即 只会增加 。
- y轴为 当前时间点,物体(我们的控制的动画对象,例如View)距离目的地的进度 。0表示进度还未走动,1表示进度已经走至目的地。但值得注意的是, 到达目的地(A点)不代表动画已经结束 ,可以 “冲破”(B点) 目的地,也可以 往回走(C点) , 结束与否是由时间轴决定。
SpringInterpolator 的动态图如下
下面动态图是小盆友专门为插值器更为直观的展示而开发的小工具,如果你感兴趣,可以把自己的插值器也放入这其中进行展示,方便以后开发时需要, 代码传送门 。
这个插值器,在小盆友的另一篇博文中有使用到,效果如下图,有兴趣的童鞋可以进入 传送门了解。
(2)如何使用
Android 系统中已经帮我实现了一些比较常用插值器,这里就不一一贴图介绍器函数图,需要的童鞋可以进入小盆友下面所提到的 插值器小工具 进行玩耍,这里给一张 工具 的效果图。
加入到属性动画中也很简单,只需以下一行代码,便可轻松搞定
// 将 插值器 设置进 Animator mAnimator.setInterpolator(new AccelerateInterpolator()); 复制代码
在源码中插值器是如何作用的呢?先卖个关子,在源码解析小节给出答案。
但是在某些情况下,为了满足动画效果的需要,Android提供的插值器就满足不了我们(:sob: 其实就是设计师搞事情 )了,所以需要找到对应的公式进行自定义插值器。聪明的你,肯定已经发现,我们前面提到的 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; } 复制代码
上面:point_up_2:代码中的第一行,便是构建一个 ObjectAnimator 实例,同时将 目标对象( ZincView )和 属性方法(zinc)一同传入,具体的内容如下:point_down:代码,我们还需要再进入一层了解
// ObjectAnimator类 private ObjectAnimator(Object target, String propertyName) { setTarget(target); // FLAG(2) setPropertyName(propertyName); } 复制代码
再进入上面:point_up_2:第一行代码,便来到以下:point_down:的代码,该方法主要功能是 停止之前目标对象的动画(如果有之前目标对象的话),然后将目标对象置换为现在的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) ,可以看到以下:point_down:代码,这段代码其实就是将 属性方法名(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) , 这行代码用于关键帧设置 ,具体如下:point_down:,因为 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 方法(具体代码看下面:point_down:), 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类 (看到下面:point_down:这段代码),发现直接是调用父类的构造方法,并且将入参一起往父类抛,所以我们进入父类 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) ,这里在粘贴出来一次(请看下面:point_down:),运行接下来的一行代码,只是将 父类中 mKeyframes 属性从 Keyframes类型 强转为 FloatKeyframes 后保存在 mFloatKeyframes 属性中。
// FloatPropertyValuesHolder 类 public void setFloatValues(float... values) { // 刚才一直是在讲解这一行代码的内容 super.setFloatValues(values); // 这里是讲解这一句啦:smile: 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,可以看到下面:point_down:这段代码,
/** * 将 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 ,可以看到:point_down:下面这段代码,是调用的 父类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 是什么? 我们在 “第一行代码” 中就已经初始化完毕啦 ,忘记的童鞋往回看看:smile:。看源码就是来来回回看,耐得住性子,才能更加牛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 (下一小节讲)各自只能设置一个,如果多次设置是会覆盖。看了源码才得知是有序列表持有。只怪自己之前太单纯:joy:。
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 的初始化在第一行代码已经详细讲述,忘记的童鞋折回去再:eyes:看一遍 */ 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,也就是说:point_up_2:上面设置估值器的代码中, 当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] ,这里不再展开(篇幅太长了:joy:)。
来到 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 方法,具体代码如下。我们需要明确的是,传进来的参数是当次的进度,也就是不含循环次数的。
看到第一行代码,你或许就明白了,我们在 “第三行代码” 设置的插值器就在这个时候发挥作用了,同时会 将插值器返回的值设置回 fraction,起到改变进度的快慢的作用 。(这便揭开了我们在 “插值器” 一节中卖的关子)
// 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(); } 复制代码
以下图片均为手写,勿喷:smile:
情况一: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 方法。这便进入到了我们设置的更新监听器的代码中。
至此,我们一个流程走完,但并不代表着就已经完成了,因为这里面还只是第一帧的回调,而 后续的帧回调 还未阐述,还有 动画的终止 还未说清。所以我们继续前行,先来 解决后续帧回调问题 。
还记得我们在讲 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、表盘指示器
文章最开始出现的第二个就是以下这张效果图,具体的操作其实和 “多维雷达图” 没有太多的出入,只是将维度的展开,变为 画布的旋转后绘制指针,达到指针旋转的效果,再加上插值器的公式辅助,到达摆动回荡的效果。限于文章篇幅过长这里就不再具体阐述,有兴趣的同学请入 传送门 。
以上所述就是小编给大家介绍的《带有活力的属性动画源码分析与实战——Android高级UI》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Java 并发编程 -- 线程池源码实战
- 别人家的 InfluxDB 实战 + 源码剖析
- 新书上市 -《Elasticsearch 源码解析与优化实战》
- ItemDecoration深入解析与实战(一)——源码分析
- Spring Boot系列实战文章合集(附源码)
- 小程序源码反编译实战笔记2018-05-31
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Kafka权威指南
Neha Narkhede、Gwen Shapira、Todd Palino / 薛命灯 / 人民邮电出版社 / 2017-12-26 / 69.00元
每个应用程序都会产生数据,包括日志消息、度量指标、用户活动记录、响应消息等。如何移动数据,几乎变得与数据本身一样重要。如果你是架构师、开发者或者产品工程师,同时也是Apache Kafka新手,那么这本实践指南将会帮助你成为流式平台上处理实时数据的专家。 本书由出身于LinkedIn的Kafka核心作者和一线技术人员共同执笔,详细介绍了如何部署Kafka集群、开发可靠的基于事件驱动的微服务,......一起来看看 《Kafka权威指南》 这本书的介绍吧!