终于在新的一年的第一天完成了本篇文章,小盆友在此祝贺您,万事如意,阖家幸福。😄java
目录android
1、前言git
2、插值器与估值器程序员
3、源码解析github
4、实战canvas
5、写在最后设计模式
对于愈来愈追求丰富的交互体验的客户端,一个带有动态效果的界面已是不可避免。属性动画就是这其中必不可少的一把利器,因此今天便来分享下 属性动画融合在实际场景中 的使用,以及进行 源码分析。话很少说,先看看今天的实战效果图,而后开始进行分享之旅。数组
一、多维雷达图 浏览器
二、表盘指示器 微信
老规矩,代码在实战篇会给出,透过 API 看清源码,各类效果即是顺手拈来😄
对于属性动画来讲,必定是绕不开 插值器 与 估值器。接下来便一一道来
文章更多的使用 我在开发过程当中 自我理解 的词语,而尽可能不使用 教科书式 或 直接翻译注释 语句。若是有 晦涩难懂 或是 理解错误之处,欢迎 评论区 留言,咱们进行探讨。
是一个控制动画 进度 的工具。
为什么如此说?我们能够这么理解,咱们借助 SpringInterpolator 插值器的函数图来说解。
下面动态图是小盆友专门为插值器更为直观的展现而开发的小工具,若是你感兴趣,能够把本身的插值器也放入这其中进行展现,方便之后开发时须要,代码传送门。
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());
复制代码
将 插值器 中的 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 系统中提供了一些 经常使用的估值器
然鹅,在某些状况下(产品大大搞事),咱们须要实现一个相似以下的添加到购物车的效果,商品是以 贝塞尔曲线 的路径 “投到” 购物车中的,这时咱们就须要 自定义估值器,由于 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));
}
}
复制代码
购物车的动画思路以下:
若是对这一动画感兴趣,能够查看具体代码,请进入传送门。
第二种状况,大多数状况下,咱们不会进行设置估值器,由于源码中已经帮咱们作了这一步的转换。因此当咱们没有设置时,系统会以如下的公式进行转换(咱们这里以 浮点数 为具体场景)
// 这段代码在 FloatKeyframeSet 的 getFloatValue 方法中
prevValue + intervalFraction * (nextValue - prevValue);
复制代码
这里再卖个关子,公式的各个参数的意义先不给出,在源码解析一节中一块儿讲解。
值得注意的是,若是属性动画中须要使用的是本身定义的类型,则必需要使用第一种状况自行定义估值器,不然会crash。
如何使用,在上一小节其实已经给出,这里给一个完整的代码
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));
}
}
复制代码
进入源码解析前,有必要先跟各位同窗 确认一件事 和 创建一个场景,不然源码解析过程会 没有目标 ,而迷路。
你已经会使用属性动画,会的意思是你已经能在 不借助文档 或是 “借鉴”别人的代码 的状况下,写出一个属性动画,并让他按照你所须要的效果 正常的运行 起来,效果难易程度不限定。
源码的阅读在一个具体的场景中更容易理解,虽然会稍微片面些,可是漏掉的情景在懂得了一个场景后,后续使用过程当中便会慢慢的补充,并且主线已懂,支线也就不难了。话很少说,咱们来看看这个使用场景。
咱们针对 ZincView 的 setZinc 方法进行值变更:
/** * @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,接下来就开始咱们的每行分析。
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 的构造方法走完,咱们先来小结一下,作了两件事:
看完构造方法中的秘密,回到 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 的代码内容就走完了,咱们再来小结一下
咱们须要再折到 FLAG(3),进行查看 setValues 这一句是如何保存刚刚建立的 PropertyValuesHolder 对象。进入setValues,能够看到下面👇这段代码,
/** * 将 PropertyValuesHolder组 进行保存。分别存于: * 一、mValues ---------- PropertyValuesHolder组 * 二、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;
}
复制代码
至此,第一行代码就已经解析完毕,主要是进行 目标对象,属性方法名 和 关键帧 的包装和保存。已经在每小段代码后进行小结,这里就再也不作冗余操做。
// 时长
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毫秒。
// 插值器
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 插值器,该插值器的走势以下动态图。
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。
// 更新回调
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(下一小节讲)各自只能设置一个,若是屡次设置是会覆盖。看了源码才得知是有序列表持有。只怪本身以前太单纯😂。
// 生命周期监听器
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);
}
复制代码
// 开启
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方法中作了这几件事:
// 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 方法中,具体代码以下,这个方法作了以下几个步骤:
接下来咱们分析第一和第四小点
// 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)
还记得咱们在 “估值器” 一小节中卖的关子么?状况二中的公式就是咱们用于计算 此处的返回值,在估值器为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的地方,有两个:
若是 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();
}
复制代码
至此,属性动画的源码分析便完成了。
文章开头出现的就是如下效果图,如今咱们来进行拆解实现。
绘制相对应维度的雷达图,在设置完数据后,进行设置属性动画,最后根据属性动画回调值进行每一个维度的展开。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;
复制代码
组装零件
零件都已经备好了,组装起来就是咱们看到的效果。由于代码稍微较长,但主要点咱们已经攻破了,而且代码注释也比较多,这里就再也不贴出来了,须要的请进传送门。
文章最开始出现的第二个就是如下这张效果图,具体的操做其实和 “多维雷达图” 没有太多的出入,只是将维度的展开,变为 画布的旋转后绘制指针,达到指针旋转的效果,再加上插值器的公式辅助,到达摆动回荡的效果。限于文章篇幅过长这里就再也不具体阐述,有兴趣的同窗请入传送门。
属性动画是高级UI中又一把不可缺乏的利器,使用得当,能让整个界面富有交互感,提升用户体验。最后若是你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中若有理解错误或是晦涩难懂的语句,请评论区留言,咱们进行讨论共同进步。你的鼓励是我前进的最大动力。
高级UI系列的Github地址:请进入传送门,若是喜欢的话给我一个star吧😄
若是须要更多的交流与探讨,能够经过如下微信二维码加小盆友好友。