最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

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

开发进阶 loading 153浏览 0评论

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

目录

一、前言

二、插值器与估值器

三、源码解析

四、实战

五、写在最后

一、前言

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

1、多维雷达图

在这里插入图片描述

2、表盘指示器

3、活力四射的购物车

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

二、插值器与估值器

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

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

1、插值器(TimeInterpolator)

(1)定义

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

为何如此说?咱们可以这么理解,我们借助 SpringInterpolator 插值器的函数图来讲解。

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

SpringInterpolator 的动态图如下

下面动态图是小盆友专门为插值器更为直观的展示而开发的小工具,如果你感兴趣,可以把自己的插值器也放入这其中进行展示,方便以后开发时需要,代码传送门

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

(2)如何使用

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

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

// 将 插值器 设置进 Animator 
mAnimator.setInterpolator(new AccelerateInterpolator())

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

但是在某些情况下,为了满足动画效果的需要,Android提供的插值器就满足不了我们(其实就是设计师搞事情)了,所以需要找到对应的公式进行自定义插值器。聪明的你,肯定已经发现,我们前面提到的 SpringInterpolator 并不是Android提供的插值器。定义 SpringInterpolator 时,只需要实现 TimeInterpolator 接口,并在 getInterpolation 方法中实现自己的逻辑即可,代码如下

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

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

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

mAnimator.setInterpolator(new SpringInterpolator());

2、估值器(TypeEvaluator)

(1)定义

插值器 中的 y轴数值 转换为我们 需要的值类型 的工具。

emmmm….稍微有点抽象。我们来具体分析下,这句话的意思。

我们以一个 具体的场景 来分析这个定义,方便讲解也更容易理解。我们进行旋转一个View,在1秒内,从 0度 转到 360度。具体代码如下:

// 实例化一个view
View view = new View(this);
// 设置 属性动画 的目标对象、作用的属性、关键帧(即0,360)
// 作用的属性值 rotation 会转为对应的方法名 setRotation,这个是默认的规则。
ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(view, "rotation", 0, 360);
// 设置 插值器
rotationAnimator.setInterpolator(new SpringInterpolator());
// 设置 动画时长
rotationAnimator.setDuration(1_000);
// 开启 动画
rotationAnimator.start();

这里其实有个问题,童鞋们应该也注意到了,插值器 的返回值(即函数图中的 y轴数值)是一个 到 “目的地” 的距离百分比(这里的百分比也就是我们前面所说的进度) ,而非我们例子中需要度数,所以需要进行 转换,而起到转换作用的就是我们的 估值器

怎么转换呢?这里要 分两种情况说明

第一种情况,我们通过以下代码,设置一个估值器,则计算规则由设置的估值器确定

ObjectAnimator mTranslateAnimator = ObjectAnimator.ofObject(view,
													        "position",
													        new BezierEvaluator(),
													        startPoint,
													        endPoint);

Android 系统中提供了一些 常用的估值器

  • ArgbEvaluator:颜色估值器,可以用于从 开始颜色 渐变为 终止颜色;
  • FloatArrayEvaluator:浮点数组估值器,将开始浮点数组 逐渐变为 终止浮点数组;
  • FloatEvaluator:浮点数值估值器,将开始浮点数 逐渐变为 终止浮点数;
  • IntArrayEvaluator:整型数组估值器,将开始整型数组 逐渐变为 终止整型数组;
  • IntEvaluator:整型数值估值器,将开始整型数 逐渐变为 终止整型数;
  • PointFEvaluator:坐标点估值器,将开始坐标点 逐渐变为 终止坐标点;
  • RectEvaluator:范围估值器,将开始范围 逐渐变为 终止范围;

然鹅,在某些情况下(产品大大搞事),我们需要实现一个类似如下的添加到购物车的效果,商品是以 贝塞尔曲线 的路径 “投到” 购物车中的,这时我们就需要 自定义估值器,因为 PointFEvaluator只是线性的将商品从起始点移到终止点,满足不了产品大大的需求,如何自定义呢?请往下走

对贝塞尔曲线感兴趣的童鞋,可以查看小盆友的另一片博文 自带美感的贝塞尔曲线原理与实战

相信你也猜到了,估值器 也是通过实现一个接口,以下便是接口和参数描述

public interface TypeEvaluator<T> {
	/**
	 * @param fraction   插值器返回的值(即函数图中的 y轴数值)
     * @param startValue 动画属性起始值,例子中的 0度
     * @param endValue   动画属性终止值,例子中的 360度
	 */
    public T evaluate(float fraction, T startValue, T endValue);
}

我们只需要实现他,填充自己的逻辑,以这个购物车的路径为例,便是以下代码,这样一个走 贝塞尔曲线 路径的商品就出现了。

private static class BezierEvaluator implements TypeEvaluator<PointF> {

    private final List<PointF> pointList;

    public BezierEvaluator(PointF startPoint, PointF endPoint) {
        this.pointList = new ArrayList<>();

        PointF controlPointF = new PointF(endPoint.x, startPoint.y);

        pointList.add(startPoint);
        pointList.add(controlPointF);
        pointList.add(endPoint);

    }

    @Override
    public PointF evaluate(float fraction, PointF startPoint, PointF endPoint) {
        return new PointF(BezierUtils.calculatePointCoordinate(BezierUtils.X_TYPE, fraction, 2, 0, pointList),
                BezierUtils.calculatePointCoordinate(BezierUtils.Y_TYPE, fraction, 2, 0, pointList));
    }
}

购物车的动画思路如下:

  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));
    }
}

三、源码解析

1、约法三章

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

(1)确认一件事

你已经会使用属性动画,会的意思是你已经能在 不借助文档 或是 “借鉴”别人的代码 的情况下,写出一个属性动画,并让他按照你所需要的效果 正常的运行 起来,效果难易程度不限定。

(2)建立一个场景

源码的阅读在一个具体的场景中更容易理解,虽然会稍微片面些,但是漏掉的情景在懂得了一个场景后,后续使用过程中便会慢慢的补充,而且主线已懂,支线也就不难了。话不多说,我们来看看这个使用场景。

我们针对 ZincView 的 setZinc 方法进行值变动:

  • 值变动范围:从 0 到 2,从 2 到 5(浮点数)
  • 动画时长 2000 毫秒
  • 设置了 FloatEvaluator 估值器 (这里是为了源码解析,所以特意加上去;如果正常情况下,该场景是不需要设置估值器的)
  • 设置了 更新回调器
  • 设置了 生命周期监听器
/**
 * @author Jiang zinc
 * @date 创建时间:2019/1/14
 * @description 属性动画源码分析
 */
public class SimpleAnimationActivity extends Activity {

    private static final String TAG = "SimpleAnimationActivity";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ZincView view = new ZincView(this);
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "zinc", 0f, 2f, 5f);
        // 时长
        objectAnimator.setDuration(2000);
        // 插值器
        objectAnimator.setInterpolator(new TimeInterpolator() {
            @Override
            public float getInterpolation(float input) {
                return input;
            }
        });
        // 估值器
        objectAnimator.setEvaluator(new FloatEvaluator());
        // 更新回调
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue().getClass());
                Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue());
            }
        });
        // 生命周期监听器
        objectAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.i(TAG, "onAnimationStart: ");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.i(TAG, "onAnimationEnd: ");
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.i(TAG, "onAnimationCancel: ");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                Log.i(TAG, "onAnimationRepeat: ");
            }
        });
        // 开启
        objectAnimator.start();
    }

    public static class ZincView extends View {
        public ZincView(Context context) {
            super(context);
        }
        public void setZinc(float value) {
            Log.i(TAG, "setZinc: " + value);
        }
    }

}

接下来我们便 一行行代码的进入源码的世界 ,了解 “属性动画” 的背后秘密。请各位同学打开自己的源码查看器,或是 Android Studio,一边跟着小盆友的思路走,一边在源码间跳跃,才不容易懵。

这里再次强调,以下代码分析都是基于这个场景,所以参数的讲解和逻辑贯穿也会直接带入场景中的数值或对象,且不再做特殊说明。并且以 “FLAG(数字)” 来作为锚,方便代码折回讲解,各位童鞋可以使用浏览器搜索功能迅速定位。

我们的源码版本是26,接下来就开始我们的每行分析。

2、第一行代码

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "zinc", 0f, 2f, 5f);

进入 ObjectAnimator 的 ofFloat 方法

// ObjectAnimator类
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
    // 初始化 ObjectAnimator,将 目标对象 和 属性 进行关联
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    // 设置关键帧 FLAG(1)
    anim.setFloatValues(values);
    return anim;
}

上面代码中的第一行,便是构建一个 ObjectAnimator 实例,同时将 目标对象( ZincView )和 属性方法(zinc)一同传入,具体的内容如下代码,我们还需要再进入一层了解

// ObjectAnimator类
private ObjectAnimator(Object target, String propertyName) {
    setTarget(target);
    // FLAG(2)
    setPropertyName(propertyName);
}

再进入上面第一行代码,便来到以下的代码,该方法主要功能是 停止之前目标对象的动画(如果有之前目标对象的话),然后将目标对象置换为现在的ZincView对象

// ObjectAnimator类
public void setTarget(@Nullable Object target) {
    // 获取之前的目标对象,这里的场景 原来的目标对象 为空
    final Object oldTarget = getTarget();
    // 两个目标对象不同,因为 oldTarget 为 null,target 为 ZincView,进入此if分支
    if (oldTarget != target) {

        // 如果已经是在运行,则进行取消
        // isStarted()方法具体请看下面代码段,只是获取 mStarted 标记位,
        // 该标记初始化为false,动画开启开始之前,该标记一直为false,开启之后,被置为true,稍后会提到
        if (isStarted()) {
        	// FLAG(24)
            cancel();
        }

        // 进行设置 新的目标对象,进行弱引用,防止内存泄漏
        mTarget = target == null ? null : new WeakReference<Object>(target);

        // 将 初始状态置为 false
        mInitialized = false;
    }
}

// ValueAnimator类(ObjectAnimator 继承至 ValueAnimator)
public boolean isStarted() {
	// mStarted 被初始化为 false,动画未开始,该标记为则为false
    return mStarted;
}

弱引用与内存泄漏 方面有兴趣的同学,可以阅读小盆友的另一片博客,内存泄漏与排查流程

跳出 setTarget 方法,我们进入到 setPropertyName 方法,即 FLAG(2) ,可以看到以下代码,这段代码其实就是将 属性方法名(zinc)存至 类成员属性 mPropertyName中,没有其他的有意义操作

// ObjectAnimator类
public void setPropertyName(@NonNull String propertyName) {
	// 此场景中,mValues为空,此 if 分支不会进入,可以先不进行理会
	// 至于 mValues 是什么,在什么时候会初始化,很快就会揭晓
    if (mValues != null) {
        PropertyValuesHolder valuesHolder = mValues[0];
        String oldName = valuesHolder.getPropertyName();
        valuesHolder.setPropertyName(propertyName);
        mValuesMap.remove(oldName);
        mValuesMap.put(propertyName, valuesHolder);
    }
    // 将 属性方法名(zinc) 存至 mPropertyName属性 中
    mPropertyName = propertyName;
    mInitialized = false;
}

小结一下

至此 ObjectAnimator 的构造方法走完,我们先来小结一下,做了两件事:

  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组 进行保存。分别存于:
 * 1、mValues ---------- PropertyValuesHolder组
 * 2、mValuesMap ------- key = PropertyName属性名,value = PropertyValuesHolder
 * <p>
 * 值得注意:
 * PropertyValuesHolder 中已经存有 关键帧
 */
public void setValues(PropertyValuesHolder... values) {
	// 保存 PropertyValuesHolder 的长度,该场景长度为1
    int numValues = values.length;
    // 保存值
    mValues = values;
    mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);

	// 该场景中,这里只循环一次。因为 PropertyValuesHolder 只有一个
    for (int i = 0; i < numValues; ++i) {
        PropertyValuesHolder valuesHolder = values[i];
        // 以 key为属性方法名zinc ----> value为对应的PropertyValuesHolder 保存到map中
        mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
    }

    mInitialized = false;
}

至此,第一行代码就已经解析完毕,主要是进行 目标对象,属性方法名 和 关键帧 的包装和保存。已经在每小段代码后进行小结,这里就不再做冗余操作。

3、第二行代码

// 时长
objectAnimator.setDuration(2000);

来到第二行,进行设置动画时长,进入 setDuration,可以看到下面这段代码,是调用的父类ValueAnimator的setDuration方法,父类的setDuration方法进行值合法判断,然后保存至 mDuration 属性中。

// ObjectAnimator 类
public ObjectAnimator setDuration(long duration) {
    super.setDuration(duration);
    return this;
}

// ValueAnimator 类
private long mDuration = 300;

// ValueAnimator 类
public ValueAnimator setDuration(long duration) {
    // 如果为负数,抛异常
    if (duration < 0) {
        throw new IllegalArgumentException("Animators cannot have negative duration: " +
                duration);
    }

    // 保存时长
    mDuration = duration;
    return this;
}

值得注意

敲黑板啦,童鞋们注意到没,mDuration的初始值为300,也就是说,如果我们不进行动画时长的设置,动画时长就默认为300毫秒。

4、第三行代码

// 插值器
objectAnimator.setInterpolator(new TimeInterpolator() {
    @Override
    public float getInterpolation(float input) {
        return input;
    }
});

接下来来到第三行设置插值器代码,进入后是直接来到 ValueAnimator 的 setInterpolator 方法,也就是说 ObjectAnimator 没有进行重写该方法。如果传入 setInterpolator 方法的参数为 null,则会默认提供 LinearInterpolator 插值器;如果传入了非空插值器,则保存至 mInterpolator 属性中。

// ValueAnimator 类
private static final TimeInterpolator sDefaultInterpolator =
            new AccelerateDecelerateInterpolator();

// ValueAnimator 类
private TimeInterpolator mInterpolator = sDefaultInterpolator;

// ValueAnimator 类
public void setInterpolator(TimeInterpolator value) {
    if (value != null) {
        mInterpolator = value;
    } else {
        mInterpolator = new LinearInterpolator();
    }
}

值得注意

当我们没有进行设置插值器时,默认的为我们初始化了 AccelerateDecelerateInterpolator 插值器,该插值器的走势如下动态图。

5、第四行代码

objectAnimator.setEvaluator(new FloatEvaluator());

第四行代码用于设置估值器,进入源码,这里同样也是进入到父类 ValueAnimator 的 setEvaluator 方法,ObjectAnimator 没有进行重写。估值器传入后,会对其进行合法性验证,例如:估值器非空,mValues非空且长度大于零。如果合法性验证不通过,则直接忽略传入的估值器。注意哦(再敲黑板!)估值器没有进行默认值的设置,至于他是如何正常运转的,其实我们在前面讲 “估值器定义” 一小节中就已经提到,但未深入探讨,当然这里也一样先不探讨,还未到时候,在 “第七行代码” 中会进行说明。

// ValueAnimator 类
PropertyValuesHolder[] mValues;

// ValueAnimator 类
public void setEvaluator(TypeEvaluator value) {
    if (value != null && mValues != null && mValues.length > 0) {
        mValues[0].setEvaluator(value);
    }
}

值得注意

我们设置的估值器只会作用于 mValues 的第一项,但是我们这个场景中,也就只有一个元素。

mValues 是什么?我们在 “第一行代码” 中就已经初始化完毕啦,忘记的童鞋往回看看。看源码就是来来回回看,耐得住性子,才能更加牛x。

6、第五行代码

// 更新回调
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue().getClass());
        Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue());
    }
});

第五行代码用于设置更新回调,进入源码,来到下面这段代码,同样还是直接来到 其父类 ValueAnimator 中,这里就比较简单了,如果 mUpdateListeners 属性未初始化,就创建一个列表,然后将更新监听器添加入列表。

// ValueAnimator 类
ArrayList<AnimatorUpdateListener> mUpdateListeners = null;

// ValueAnimator 类
public void addUpdateListener(AnimatorUpdateListener listener) {
    if (mUpdateListeners == null) {
        mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
    }
    mUpdateListeners.add(listener);
}

值得注意

不知道童鞋们有没有和小盆友一样的错觉,一直以为 AnimatorUpdateListenerAnimator.AnimatorListener(下一小节讲)各自只能设置一个,如果多次设置是会覆盖。看了源码才得知是有序列表持有。只怪自己之前太单纯。

7、第六行代码

// 生命周期监听器
objectAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        Log.i(TAG, "onAnimationStart: ");
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        Log.i(TAG, "onAnimationEnd: ");
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        Log.i(TAG, "onAnimationCancel: ");
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        Log.i(TAG, "onAnimationRepeat: ");
    }
});

第六行代码进行设置生命周期监听器,进入源码,这次是来到 ObjectAnimator 的爷爷类Animator 的 addListener 方法。

这里有必要给出这几个类的继承关系和实现的接口,请记住他,因为在 “第七行代码” 中会提到。这里立个FLAG(6),需要时我们再折回来看看。

graph LR
    A[ObjectAnimator] --> B[ValueAnimator] 
    B[ValueAnimator]  --> C[Animator] 
    B[ValueAnimator] -.-> D[AnimationHandler.AnimationFrameCallback]

这里的逻辑和 “第五行的代码” 的源码逻辑可以说是一样的,都是先判空,如果为空,则进行实例化一个有序列表,然后将监听器放入列表中。

// Animator 类
ArrayList<AnimatorListener> mListeners = null;

// Animator 类
public void addListener(AnimatorListener listener) {
    if (mListeners == null) {
        mListeners = new ArrayList<AnimatorListener>();
    }
    mListeners.add(listener);
}

8、第七行代码

// 开启
objectAnimator.start();

来到了最后的第七行代码,这行代码虽然简短,但却蕴含着最多的秘密,让我们来一点点揭开。进入start方法,看到如下代码。

// ObjectAnimator 类
public void start() {
	// FLAG(7)
    AnimationHandler.getInstance().autoCancelBasedOn(this);
    if (DBG) {
    	// 省略,用于调试时的日志输出,DBG 是被定义为 静态不可修改的 false,所以可以忽略这个分支
    	......
    }
    // FLAG(8)
    super.start();
}

我们先进入第一行代码 的 第一小段,即 getInstance()。看到如下代码,其实看到getInstance这个方法名我们应该就会想到一个设计模式——单例模式,通过方法内的代码也确实验证了这个猜想。但是有些许不同的是单例对象放在 ThreadLocal 中,用于确保的是 线程单例,而非进程中全局单例,换句话说,不同线程的AnimationHandler对象是不相同的

// AnimationHandler 类
/**
 * 保证 AnimationHandler 当前线程单例
 */
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();

// AnimationHandler 类
/**
 * 获取 AnimationHandler 线程单例
 */
public static AnimationHandler getInstance() {
    if (sAnimatorHandler.get() == null) {
        sAnimatorHandler.set(new AnimationHandler());
    }
    return sAnimatorHandler.get();
}

经过上面代码,我们便获取到了 AnimationHandler 对象。AnimationHandler 是一个用于接受脉冲(即 垂直同步信号),让同一线程的动画使用的计算时间是相同的,这样的作用是让同步动画成为可能。 至于 AnimationHandler 是如何接受垂直同步信号,我们继续卖关子,稍后就会知道。

我们折回到 FLAG(7),看第二小段代码,具体代码如下,这里的代码其实在我们设定的场景中是不会运行的,因为 mAnimationCallbacks 此时长度还为0。但我们还是进行深入的分析,具体的每行代码讲解请看注释,总结起来就是 如果 mAnimationCallbacks列表中的元素 和 参数objectAnimator对象 存在相同的目标对象和相同的PropertyValuesHolder,则将mAnimationCallbacks列表中对应的元素进行取消操作。

// AnimationHandler 类
/**
 * AnimationFrameCallback 此场景中就是 我们第一行代码实例化的 ObjectAnimator 对象,
 * 因为 ObjectAnimator 的父类实现了 AnimationFrameCallback 接口,具体继承关系可以看 FLAG(6) 处的类图
 */
private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
        new ArrayList<>();

// AnimationHandler 类
void autoCancelBasedOn(ObjectAnimator objectAnimator) {
	// 场景中,mAnimationCallback 此时长度为0,所以其实此循环不会进入
    for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
        AnimationFrameCallback cb = mAnimationCallbacks.get(i);
        if (cb == null) {
            continue;
        }
        // 将 相同的目标对象 且 PropertyValuesHolder完全一样的动画进行取消操作
        // 取消操作在 “第一行代码” 便已经详细阐述,这里就不再赘述
        if (objectAnimator.shouldAutoCancel(cb)) {
            ((Animator) mAnimationCallbacks.get(i)).cancel();
        }
    }
}

// ObjectAnimator 类
/**
 * 是否可以 进行取消
 */
boolean shouldAutoCancel(AnimationHandler.AnimationFrameCallback anim) {
    // 为空,则返回
    if (anim == null) {
        return false;
    }

    if (anim instanceof ObjectAnimator) {
        ObjectAnimator objAnim = (ObjectAnimator) anim;
        // 该动画可以自动取消 且 当前的对象和anim 的所持的目标对象和PropertyValuesHolder一样
        // 则可以 进行取消,返回true
        if (objAnim.mAutoCancel && hasSameTargetAndProperties(objAnim)) {
            return true;
        }
    }
    // 否则不取消
    return false;
}

// ObjectAnimator 类
/**
 * ObjectAnimator 是否有相同的 目标对象target 和 PropertyValuesHolder
 * PropertyValuesHolder 的初始化在第一行代码已经详细讲述,忘记的童鞋折回去再看一遍
 */
private boolean hasSameTargetAndProperties(@Nullable Animator anim) {
    if (anim instanceof ObjectAnimator) {
    	// 获取 PropertyValuesHolder 
        PropertyValuesHolder[] theirValues = ((ObjectAnimator) anim).getValues();
        // 目标对象相同 且 PropertyValuesHolder长度相同
        if (((ObjectAnimator) anim).getTarget() == getTarget() &&
                mValues.length == theirValues.length) {
			// 循环检测 PropertyValuesHolder 中 属性名是否 “完全相同”,只要有一个不同 则返回false
            for (int i = 0; i < mValues.length; ++i) {
                PropertyValuesHolder pvhMine = mValues[i];
                PropertyValuesHolder pvhTheirs = theirValues[i];
                if (pvhMine.getPropertyName() == null ||
                        !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) {
                    return false;
                }
            }
            // 全部相同,返回true
            return true;
        }
    }
    // 不是 ObjectAnimator 直接返回false
    return false;
}

看完首行代码,我们来到调用 父类ValueAnimator 的 start 方法这行FLAG(8),进入该方法,具体代码如下,可以看到调用了 start() 的重载方法 start(boolean),playBackwards是用于标记是否要反向播放,显然传入的为false,表示正向播放。start方法中做了这几件事:

  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 方法,具体代码如下。我们需要明确的是,传进来的参数是当次的进度,也就是不含循环次数的。

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

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

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

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

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

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

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

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

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

        // 这里的插值器为空,并不会运行该分支
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }

        float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
        return mEvaluator == null ?
                prevValue + intervalFraction * (nextValue - prevValue) :
                ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                        floatValue();

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

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

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

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

    // 正常情况下不应该运行到这
    return ((Number) mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue();
}

以下图片均为手写,勿喷

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

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

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

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

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

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

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

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

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

至此我们得到了 估值器计算出来的我们需要的值

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

至此,我们一个流程走完,但并不代表着就已经完成了,因为这里面还只是第一帧的回调,而后续的帧回调还未阐述,还有动画的终止还未说清。所以我们继续前行,先来 解决后续帧回调问题

还记得我们在讲 Choreographer 时,通过 AnimationHandler 注入了一个回调么?这个时候后续的帧回调就全靠他了。我们前面说过每次 “垂直同步” 信号的到来,回调用到 Choreographer$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();

}

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

四、实战

1、多维雷达图

文章开头出现的就是以下效果图,现在我们来进行拆解实现。

效果图

动画分析

绘制相对应维度的雷达图,在设置完数据后,进行设置属性动画,最后根据属性动画回调值进行每个维度的展开。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;

组装零件

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

2、表盘指示器

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

效果图

五、写在最后

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

如果需要更多的交流与探讨,可以通过以下微信二维码加小盆友好友。

作者:猛猛的小盆友
链接:https://juejin.im/post/5c595158f265da2d9710cb6e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载请注明:Android开发中文站 » 带有活力的属性动画源码分析与实战——Android高级UI

您必须 登录 才能发表评论!