带有活力的属性动画源码分析与实战——Android高级UI

终于在新的一年的第一天完成了本篇文章,小盆友在此祝贺您,万事如意,阖家幸福。😄java

目录android

1、前言git

2、插值器与估值器程序员

3、源码解析github

4、实战canvas

5、写在最后设计模式

1、前言

对于愈来愈追求丰富的交互体验的客户端,一个带有动态效果的界面已是不可避免。属性动画就是这其中必不可少的一把利器,因此今天便来分享下 属性动画融合在实际场景中 的使用,以及进行 源码分析。话很少说,先看看今天的实战效果图,而后开始进行分享之旅。数组

一、多维雷达图 浏览器

在这里插入图片描述

二、表盘指示器 微信

三、活力四射的购物车

老规矩,代码在实战篇会给出,透过 API 看清源码,各类效果即是顺手拈来😄

2、插值器与估值器

对于属性动画来讲,必定是绕不开 插值器估值器。接下来便一一道来

文章更多的使用 我在开发过程当中 自我理解 的词语,而尽可能不使用 教科书式直接翻译注释 语句。若是有 晦涩难懂 或是 理解错误之处,欢迎 评论区 留言,咱们进行探讨。

一、插值器(TimeInterpolator)

(1)定义

是一个控制动画 进度 的工具。

为什么如此说?我们能够这么理解,咱们借助 SpringInterpolator 插值器的函数图来说解。

  • x轴为 时间 。0表示还未开始,1表示时间结束。时间是 不可逆的 ,因此会一直往前推动,即 只会增长
  • y轴为 当前时间点,物体(咱们的控制的动画对象,例如View)距离目的地的进度。0表示进度还未走动,1表示进度已经走至目的地。但值得注意的是,到达目的地(A点)不表明动画已经结束,能够 “冲破”(B点) 目的地,也能够 往回走(C点)结束与否是由时间轴决定。

SpringInterpolator 的动态图以下

下面动态图是小盆友专门为插值器更为直观的展现而开发的小工具,若是你感兴趣,能够把本身的插值器也放入这其中进行展现,方便之后开发时须要,代码传送门

这个插值器,在小盆友的另外一篇博文中有使用到,效果以下图,有兴趣的童鞋能够进入 传送门了解。

(2)如何使用

Android 系统中已经帮我实现了一些比较经常使用插值器,这里就不一一贴图介绍器函数图,须要的童鞋能够进入小盆友下面所提到的 插值器小工具 进行玩耍,这里给一张工具的效果图。

加入到属性动画中也很简单,只需如下一行代码,即可轻松搞定

// 将 插值器 设置进 Animator 
mAnimator.setInterpolator(new AccelerateInterpolator());
复制代码

在源码中插值器是如何做用的呢?先卖个关子,在源码解析小节给出答案。

可是在某些状况下,为了知足动画效果的须要,Android提供的插值器就知足不了咱们(😭其实就是设计师搞事情)了,因此须要找到对应的公式进行自定义插值器。聪明的你,确定已经发现,咱们前面提到的 SpringInterpolator 并非Android提供的插值器。定义 SpringInterpolator 时,只须要实现 TimeInterpolator 接口,并在 getInterpolation 方法中实现本身的逻辑便可,代码以下

/** * @author Jiang zinc * @date 建立时间:2019/1/27 * @description 震旦效果 */
public class SpringInterpolator implements TimeInterpolator {

    /** * 参数 x,即为 x轴的值 * 返回值 即是 y 轴的值 */
    @Override
    public float getInterpolation(float x) {
        float factor = 0.4f;
        return (float) (Math.pow(2, -10 * x) * Math.sin((x - factor / 4) * (2 * Math.PI) / factor) + 1);
    }
}
复制代码

使用时,和 Android提供的插值器 是一摸同样的,以下所示

mAnimator.setInterpolator(new SpringInterpolator());
复制代码

二、估值器(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));
    }
}
复制代码

购物车的动画思路以下:

  1. 点击添加商品后,初始化一个 ShoppingView(继承至ImageView),设置其大小和商品的大小一致,并添加至 decorView 中,这样才能让 ShoppingView 在整个视图中移动。
  2. 经过 getLocationOnScreen 方法,获取商品图片在屏幕中的坐标A 和 购物车在屏幕中的坐标B(值得注意的是,这个坐标是 包含状态栏的高度 ),将 ShoppingView 移至坐标A,而后启动动画。
  3. 进行以 ShoppingView 正中心为原点进行 从0到0.8缩小,接着顺时针倾斜35度,紧接着以 贝塞尔曲线 路径从坐标A移至坐标B,控制点C的x轴为B点的x轴坐标,y轴为A点的y轴坐标;
  4. 动画完成后,将 ShoppingView 从 其父视图中移除,同时回调侦听接口,以便购物车能够进行动画。
  5. 接到回调后,购物车数量加一,同时更换购物车图标,已经显示小红点而且播放其动画,以小红点正中心为原点进行缩放,缩放规则为从 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));
    }
}
复制代码

3、源码解析

一、约法三章

进入源码解析前,有必要先跟各位同窗 确认一件事创建一个场景,不然源码解析过程会 没有目标 ,而迷路。

(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,接下来就开始咱们的每行分析。

二、第一行代码

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 的构造方法走完,咱们先来小结一下,作了两件事:

  1. 目标对象ZincView弱引用的形式保存在 mTarget属性
  2. 属性方法名zinc 保存在 mPropertyName属性

看完构造方法中的秘密,回到 ObjectAnimator 的 ofFloat方法 中,进入接下来的那行代码,即 FLAG(1)这行代码用于关键帧设置,具体以下👇,由于 mValues 此时为空,且 mProperty 也为空,因此最终进入 setValues 那一行代码(即FLAG(3))

// ObjectAnimator类
public void setFloatValues(float... values) {
    // mValues 为空
    if (mValues == null || mValues.length == 0) {
        // mProperty 为空,在这个场景中,进入else分支
        if (mProperty != null) {
            setValues(PropertyValuesHolder.ofFloat(mProperty, values));
        } else {
            // 进行 PropertyValuesHolder 包装
            // 将 关键帧 进行各自的封装成 Keyframe
            // 而后打包成 KeyframeSet 与 mPropertyName 共同保存进 PropertyValuesHolder中
            // FLAG(3)
            setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
        }
    } else {
        super.setFloatValues(values);
    }
}
复制代码

setValues 那一行代码中其实还包含了一句 PropertyValuesHolder 的构建语句,咱们先进入PropertyValuesHolderofFloat 方法中,能看到以下代码段,实例化了一个 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);
}
复制代码

进入 KeyframeSetofFloat 方法(具体代码看下面👇),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 的代码内容就走完了,咱们再来小结一下

  1. PropertyValuesHolder 是一个用于装载 关键帧集合属性动画名 的数据模型。
  2. 关键帧会被一个个存放在 Keyframe 类中,既 有多少个关键帧有多少个Keyframe
  3. Keyframe的fraction为 i / 关键帧的数量-1(i>=1),value 则为对应的关键帧数值。即将1进行等分为(关键帧数量-2)份,头尾两帧fraction则分别为0和1,中间帧则按顺序各占一份。

咱们须要再折到 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);
}
复制代码

值得注意

不知道童鞋们有没有和小盆友同样的错觉,一直觉得 AnimatorUpdateListenerAnimator.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方法中作了这几件事:

  1. 初始化一些属性,例如运行的状态标记(注意此处 开始状态mStarted便置为true);
  2. 帧和动画的播放时间则置为-1;
  3. 添加动画回调,用于接受 垂直同步信号;
  4. 设置当前的播放 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,这里调用了 AnimationHandleraddAnimationFrameCallback,同时把 本身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();
}
复制代码

这样咱们便来到了 AnimationHandleraddAnimationFrameCallback 方法,根据该方法的官方注释可知,注册的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 接口,自行提供不一样的定时脉冲来实现效果,来顶替这里的 MyFrameCallbackProviderAnimationHandler 同时也提供了 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 保存在了 ChoreographermCallbackQueues 中,这里须要在打一个标记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 方法中,具体代码以下,这个方法作了以下几个步骤:

  1. 初始化动画,具体是设置关键帧集合的估值器;
  2. 将运行状态置为true;
  3. 设置 mOverallFraction(这个属性咱们后面用到时在说明是什么做用);
  4. 回调 监听器;

接下来咱们分析第一和第四小点

// 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;
    }
}
复制代码

进入到 PropertyValuesHolderinit 方法中,代码以下,该方法作了一件事,就是初始化 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 方法,具体代码以下。咱们须要明确的是,传进来的参数是当次的进度,也就是不含循环次数的。

咱们会先进入到 ObjectAnimatoranimateValue,可是源码会先进入其父类 ValueAnimatoranimateValue。因此咱们先进入父类的 animateValue

看到第一行代码,你或许就明白了,咱们在 “第三行代码” 设置的插值器就在这个时候发挥做用了,同时会 将插值器返回的值设置回 fraction,起到改变进度的快慢的做用 。(这便揭开了咱们在 “插值器” 一节中卖的关子)

// ObjectAnimator 类
void animateValue(float fraction) {
    final Object target = getTarget();
    // 不会进入,此时的target在 “第一行代码” 时就设置了,因此不为空
    if (mTarget != null && target == null) {
        cancel();
        return;
    }

    // 进入父类 ValueAnimator
    super.animateValue(fraction);
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
    	// FLAG(25)
        mValues[i].setAnimatedValue(target);
    }
}

// ValueAnimator 类
void animateValue(float fraction) {
    // 经过 插值器 进行计算出 fraction
    fraction = mInterpolator.getInterpolation(fraction);

    // 当前次数的进度
    mCurrentFraction = fraction;

    // 循环 全部的 PropertyValuesHolder,进行估值器计算
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].calculateValue(fraction);
    }

    /** * 进行回调 {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} */
    // FLAG(21)
    if (mUpdateListeners != null) {
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            mUpdateListeners.get(i).onAnimationUpdate(this);
        }
    }
}
复制代码

紧接着进行循环调用 mValues 中的元素的 calculateValue 方法(咱们这场景中 mValues 的元素其实只有一个),进入该方法,能够看到以下代码。这里进入的是 PropertyValuesHolder 的子类 FloatPropertyValuesHolder

// FloatPropertyValuesHolder 类
void calculateValue(float fraction) {
	// mFloatKeyframes 在 “第一行代码” 时就已经初始化
    mFloatAnimatedValue = mFloatKeyframes.getFloatValue(fraction);
}
复制代码

咱们接着进入 getFloatValue 方法,其具体实现类是 FloatKeyframeSet。具体代码以下,getFloatValue 中分了几个状况,咱们接下来分状况讨论,往下走。

// FloatKeyframeSet 类
/** * 获取当前 进度数值为fraction的 value * 落实一个场景: 0f, 2f, 5f * mKeyframes 中存了三个 FloatKeyframe * mNumKeyframes 则为 3 * * @param fraction The elapsed fraction of the animation * @return */
@Override
public float getFloatValue(float fraction) {
	// FLAG(18)
    if (fraction <= 0f) {
        // 获取 0f 关键帧
        final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
        // 获取 2f 关键帧
        final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1);
        // 获取 0f 关键帧的值 即 0f
        float prevValue = prevKeyframe.getFloatValue();
        // 获取 2f 关键帧的值 即 1f
        float nextValue = nextKeyframe.getFloatValue();
        // 获取 0f 关键帧的fraction,这里为0
        float prevFraction = prevKeyframe.getFraction();
        // 获取 2f 关键帧的fraction,这里为1/2
        float nextFraction = nextKeyframe.getFraction();

        // 这里的插值器为空,并不会运行该分支
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        
        float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
        return mEvaluator == null ?
                prevValue + intervalFraction * (nextValue - prevValue) :
                ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                        floatValue();

    } else if (fraction >= 1f) {	// FLAG(19)
        final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2);
        final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1);
        float prevValue = prevKeyframe.getFloatValue();
        float nextValue = nextKeyframe.getFloatValue();
        float prevFraction = prevKeyframe.getFraction();
        float nextFraction = nextKeyframe.getFraction();
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
        return mEvaluator == null ?
                prevValue + intervalFraction * (nextValue - prevValue) :
                ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                        floatValue();
    }

	// FLAG(20)
    // 初始化第一帧, 0f
    FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
    // 从第二帧开始循环
    for (int i = 1; i < mNumKeyframes; ++i) {
        // 相对于prevKeyframe,取下一帧
        FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i);

		// 判断是否落在 该区间
        if (fraction < nextKeyframe.getFraction()) {
            final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
            float intervalFraction = (fraction - prevKeyframe.getFraction()) /
                    (nextKeyframe.getFraction() - prevKeyframe.getFraction());

            float prevValue = prevKeyframe.getFloatValue();
            float nextValue = nextKeyframe.getFloatValue();
            if (interpolator != null) {
                intervalFraction = interpolator.getInterpolation(intervalFraction);
            }
            // 估值器计算
            return mEvaluator == null ?
                    prevValue + intervalFraction * (nextValue - prevValue) :
                    ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                            floatValue();
        }
		// 变换前一帧
        prevKeyframe = nextKeyframe;
    }

    // 正常状况下不该该运行到这
    return ((Number) mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue();
}
复制代码

如下图片均为手写,勿喷😄

状况一:fraction = 0。进入FLAG(18)

状况二:fraction = 1/4。进入FLAG(20)

状况三:fraction = 3/4。进入FLAG(20)

状况四:fraction = 1。进入FLAG(19)

通过上面四种状况,咱们能够知道 intervalFraction 值,即为 当前帧段的比例数(帧段即为 0f-2f,2f-5f) 而 返回值 即为 fraction 经过估值器转换为 真实须要的值,即咱们程序员能够拿来用,例如咱们这里须要的是 0f-5f的值。

还记得咱们在 “估值器” 一小节中卖的关子么?状况二中的公式就是咱们用于计算 此处的返回值,在估值器为null时。若是估值器不为null,则按照设置的估值器逻辑计算。

mEvaluator == null ?
				 prevValue + intervalFraction * (nextValue - prevValue) :
				 ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
				         floatValue();
复制代码

你可能会有疑惑,咱们这场景中不是有设置一个 FloatEvaluator 估值器么?确实是,但FloatEvaluator内部逻辑其实就和咱们估值器为null时是如出一辙的。具体代码以下

public class FloatEvaluator implements TypeEvaluator<Number> {
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}
复制代码

至此咱们获得了 估值器计算出来的咱们须要的值

咱们折回 FLAG(21),看到 mUpdateListeners 这属性,童鞋们应该也知道这是在 “第五行代码” 设置的更新监听器,进入该分支,会循环着调用更新监听器的 onAnimationUpdate 方法。这便进入到了咱们设置的更新监听器的代码中。

接下来咱们须要折回到父类 ObjectAnimatoranimateValue,即FLAG(25)。会进行循环调用 mValuessetAnimatedValue,而咱们这里的场景的 mValuesFloatPropertyValuesHolder 类型,因此会调用该类的 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$FrameDisplayEventReceiveronVsync 的,而该方法最终会调用到咱们在FLAG(11)放入回调队列 mCallbackQueues 中的 mFrameCallbackdoFrame 方法。这就回到了咱们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的地方,有两个:

  1. 动画时长为0;
  2. 最后一次迭代完毕,至于判断是否完成最后一次迭代,则经过判断当前进度是否已经大于咱们循环的次数,而且动画不是无限循环播放,判断的代码能够看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();

}
复制代码

至此,属性动画的源码分析便完成了。

4、实战

一、多维雷达图

文章开头出现的就是如下效果图,如今咱们来进行拆解实现。

效果图

动画分析

绘制相对应维度的雷达图,在设置完数据后,进行设置属性动画,最后根据属性动画回调值进行每一个维度的展开。emmm,有些抽象。咱们进行拆解为须要的零件:

  1. 每一个顶点的坐标;
  2. 维度展开的属性动画;

准备零件

(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;
复制代码

组装零件

零件都已经备好了,组装起来就是咱们看到的效果。由于代码稍微较长,但主要点咱们已经攻破了,而且代码注释也比较多,这里就再也不贴出来了,须要的请进传送门

二、表盘指示器

文章最开始出现的第二个就是如下这张效果图,具体的操做其实和 “多维雷达图” 没有太多的出入,只是将维度的展开,变为 画布的旋转后绘制指针,达到指针旋转的效果,再加上插值器的公式辅助,到达摆动回荡的效果。限于文章篇幅过长这里就再也不具体阐述,有兴趣的同窗请入传送门

效果图

5、写在最后

属性动画是高级UI中又一把不可缺乏的利器,使用得当,能让整个界面富有交互感,提升用户体验。最后若是你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中若有理解错误或是晦涩难懂的语句,请评论区留言,咱们进行讨论共同进步。你的鼓励是我前进的最大动力。

高级UI系列的Github地址:请进入传送门,若是喜欢的话给我一个star吧😄

若是须要更多的交流与探讨,能够经过如下微信二维码加小盆友好友。

相关文章
相关标签/搜索