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

栏目: IOS · Android · 发布时间: 5年前

内容简介:目录一、前言二、插值器与估值器

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

目录

一、前言

二、插值器与估值器

三、源码解析

四、实战

五、写在最后

一、前言

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

1、多维雷达图

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

2、表盘指示器

带有活力的属性动画源码分析与实战——Android高级UI
3、活力四射的购物车
带有活力的属性动画源码分析与实战——Android高级UI

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

二、插值器与估值器

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

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

1、插值器(TimeInterpolator)

(1)定义

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

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

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

SpringInterpolator 的动态图如下

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

带有活力的属性动画源码分析与实战——Android高级UI
这个插值器,在小盆友的另一篇博文中有使用到,效果如下图,有兴趣的童鞋可以进入 传送门

了解。

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

(2)如何使用

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

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

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

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

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

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

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

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

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

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

2、估值器(TypeEvaluator)

(1)定义

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

小结一下

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

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

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

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

setValues那一行代码中其实还包含了一句 PropertyValuesHolder 的构建语句,我们先进入 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 方法(具体代码看下面:point_down:), ofFloat 方法主要是将我们传进来的 关键帧数值 转换为 关键帧对象 ,然后封装成 FloatKeyframeSet 类型的 关键帧集合 ,方便后期动画运行时使用(如何使用,在讲解start时会详细讲解)。

// KeyframeSet类
/**
 * 创建 关键帧集合 对象
 * 传进来的 "0f, 2f, 5f" 每个数值将被封装成 Keyframe 关键帧对象
 * 最终被放置 FloatKeyframeSet关键帧集合中 向上转型为 KeyframeSet
 *
 * @param values 传进来的 0f, 2f, 5f 数值
 * @return 返回关键帧集合
 */
public static KeyframeSet ofFloat(float... values) {
	// 是否为坏数据标记 (not a number) 
    boolean badValue = false;

    // 关键帧数量,这里长度为 3
    int numKeyframes = values.length;
    // 保证关键帧的数组长度至少为 2
    FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes, 2)];

    // 当关键帧数量为1时,需要补足 两个,该场景中,进入 else 分支
    if (numKeyframes == 1) {
        // 第一个用 空value 进行补充,默认会为0
        // 因为 Keyframe 的 mValue属性类型为float,jvm会自动为其填充为 0
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
        // 填充 第二帧 为传进来的数值
        keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);

        // 是否为 not a number,如果是则改变标记位
        if (Float.isNaN(values[0])) {
            badValue = true;
        }
    } else {

        // 关键帧 进行添加进集合
        // 传进来的值:0f ------> fraction: 0    keyframes[0]
        // 传进来的值:2f ------> fraction: 1/2  keyframes[1]
        // 传进来的值:5f ------> fraction: 1    keyframes[2]
        // fraction 为ofFloat的第一个参数, value 为ofFloat的第二个参数;
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
        for (int i = 1; i < numKeyframes; ++i) {
            keyframes[i] =
                    (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
            
            // 是否为 not a number,如果是则改变标记位
            if (Float.isNaN(values[i])) {
                badValue = true;
            }
        }
    }
    // 如果为 not a number,则进行提示
    if (badValue) {
        Log.w("Animator", "Bad value (NaN) in float animator");
    }
    // 将创建好的 关键帧数组keyframes 包装成 FloatKeyframeSet类型 对象
    // 这样封装的好处是 动画运行时更好的调用当前的关键帧
    return new FloatKeyframeSet(keyframes);
}
复制代码

FloatKeyframeSet是如何封装的呢?我们继续进入查看,进入 FloatKeyframeSet类 (看到下面:point_down:这段代码),发现直接是调用父类的构造方法,并且将入参一起往父类抛,所以我们进入父类 KeyframeSet 的构造方法。这里需要先说明下这几个类的继承关系,后面也会用到,注意看清这几个类和接口的名字。

graph LR
    A[FloatKeyframeSet] --> B[KeyframeSet] 
    B[KeyframeSet] --> C[Keyframes]
    A[FloatKeyframeSet] -.-> D[Keyframes.FloatKeyframes] 
复制代码
graph LR
    A[FloatKeyframe] --> B[Keyframe] 
复制代码

进入 KeyframeSet 的构造函数,因为 FloatKeyframe 继承自 Keyframe ,所以进入父类后,也就自动向上转型了。在这构造方法中,只是进行保存一些关键帧集合的状态,例如:关键帧集合的长度,将关键帧数组转换为列表等(具体看下面 代码注释

// FloatKeyframeSet 类
public FloatKeyframeSet(FloatKeyframe... keyframes) {
    super(keyframes);
}

// KeyframeSet 类
public KeyframeSet(Keyframe... keyframes) {
    // 保存 关键帧数量
    mNumKeyframes = keyframes.length;
    // 将 关键帧数组转 换为 不可变列表
    mKeyframes = Arrays.asList(keyframes);
    // 保存 第一个 关键帧
    mFirstKeyframe = keyframes[0];
    // 保存 最后一个 关键帧
    mLastKeyframe = keyframes[mNumKeyframes - 1];
    // 保存最后一个关键帧的插值器  在这场景中,这里的插值器为null
    mInterpolator = mLastKeyframe.getInterpolator();
}
复制代码

终于走到头了,我们需要折回去,到 FLAG(5) ,这里在粘贴出来一次(请看下面:point_down:),运行接下来的一行代码,只是将 父类中 mKeyframes 属性从 Keyframes类型 强转为 FloatKeyframes 后保存在 mFloatKeyframes 属性中。

// FloatPropertyValuesHolder 类
public void setFloatValues(float... values) {
    // 刚才一直是在讲解这一行代码的内容
    super.setFloatValues(values);
    // 这里是讲解这一句啦:smile: 
    mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}
复制代码

小结一下

到这里 PropertyValuesHolder.ofFloat 的代码内容就走完了,我们再来 小结一下

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

我们需要再折到 FLAG(3) ,进行查看 setValues 这一句是如何保存刚刚创建的 PropertyValuesHolder 对象。进入setValues,可以看到下面:point_down:这段代码,

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

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

    mInitialized = false;
}
复制代码

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

3、第二行代码

// 时长
objectAnimator.setDuration(2000);
复制代码

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

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

// ValueAnimator 类
private long mDuration = 300;

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

    // 保存时长
    mDuration = duration;
    return this;
}
复制代码

值得注意

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

4、第三行代码

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

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

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

// ValueAnimator 类
private TimeInterpolator mInterpolator = sDefaultInterpolator;

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

值得注意

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

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

5、第四行代码

objectAnimator.setEvaluator(new FloatEvaluator());
复制代码

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

// ValueAnimator 类
PropertyValuesHolder[] mValues;

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

值得注意

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

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

6、第五行代码

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

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

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

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

值得注意

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

7、第六行代码

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

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

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

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

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

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

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

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

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

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

8、第七行代码

// 开启
objectAnimator.start();
复制代码

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

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

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

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

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

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

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

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

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

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

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

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

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

  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,也就是说:point_up_2:上面设置估值器的代码中, 当mEvaluator为空时 ,如果我们使用的简单类型的float,此处的 并不会使用默认的sFloatEvaluator ,而是还是为null。

System.out.println(float.class == Float.class);  // 输出的是false
复制代码

接下来进行分析第四小点,折回到FLAG(15),此时 mListeners 已经在 “第六行代码” 时就初始化,并添加了一个监听器,所以会进入该if分支,进入 notifyStartListeners 方法,具体代码如下,这里便回调到了我们 “第六行代码” 设置的生命周期监听器的 onAnimationStart 方法中,同时将自身 ObjectAnimator 对象作为参数带出。

// ValueAnimator 类
private void notifyStartListeners() {
    // 有 回调监听器,且从未回调
    if (mListeners != null && !mStartListenersCalled) {
        ArrayList<AnimatorListener> tmpListeners =
                (ArrayList<AnimatorListener>) mListeners.clone();
        int numListeners = tmpListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            /**
             * 进行回调开始,这里便 回调到 我们设置
             * {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
             * 的方法中
             */
            tmpListeners.get(i).onAnimationStart(this, mReversing);
        }
    }

    mStartListenersCalled = true;
}

// Animator$AnimatorListener 类
default void onAnimationStart(Animator animation, boolean isReverse) {
   onAnimationStart(animation);
}
复制代码

这里一路调用进来,算是比较深远了,但无大碍,我们回到FLAG(16),继续看 start 方法的最后一行代码 setCurrentPlayTime(0) 的具体内容,代码如下,这方法是为了让 如果动画时长小于或等于零时,直接到达动画的末尾,即fraction置为1。

// ValueAnimator 类
public void setCurrentPlayTime(long playTime) {
    // 如果设置的 mDuration 为 2000,playTime 为 0,则 fraction 为 0
    // 如果设置的 mDuration 为 0,则 fraction 为 1,直接到最后一个关键帧
    float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
    setCurrentFraction(fraction);
}
复制代码

接下来看 setCurrentFraction 做了什么操作,第一行的 initAnimation 在之前就已经运行过了,所以并不会再次初始化, clampFraction 方法是为了让 fraction 落在合法的区域内,即 [0,mRepeatCount + 1] ,这里不再展开(篇幅太长了:joy:)。

来到 if-else, isPulsingInternal 方法内判断的 mLastFrameTime 是否大于等于0,但是 mLastFrameTime 到目前为止还是 -1,所以进入else分支,将fraction保存至mSeekFraction。

// ValueAnimator 类
public void setCurrentFraction(float fraction) {
    // 初始化 动画,但这里其实只是做了 估值器的赋值初始化
    initAnimation();

    // 获取 合法的fraction
    fraction = clampFraction(fraction);

    mStartTimeCommitted = true;

    // 动画是否已进入动画循环
    if (isPulsingInternal()) {
        // 获取动画已经使用的时长
        long seekTime = (long) (getScaledDuration() * fraction);
        // 获取当前动画时间
        long currentTime = AnimationUtils.currentAnimationTimeMillis();
        // 仅修改动画运行时的开始时间。 Seek Fraction将确保非运行动画跳到正确的开始时间。
        mStartTime = currentTime - seekTime;
    } else {
        // 如果动画循环尚未开始,或者在开始延迟期间。seekTime,一旦延迟过去,startTime会基于seekTime进行调整。
        mSeekFraction = fraction;
    }

    // 总fraction,携带有迭代次数
    mOverallFraction = fraction;

    // 计算当次迭代的 fraction
    final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
    
    // 根据 fraction ,计算出对应的value
    // FLAG(17)
    animateValue(currentIterationFraction);
}
复制代码

接下来是 getCurrentIterationFraction 方法,这个方法用于获取 当前迭代的 fraction ,因为我们需要知道的是, 如果设置了多次循环播放动画,即mRepeatCount>0时,则fraction是包含有mRepeatCount次数的 。而这个方法就是去除了次数,只剩下当次的进度,即范围为 [0,1] 。

// ValueAnimator 类
private float getCurrentIterationFraction(float fraction, boolean inReverse) {
    // 确保 fraction 的范围在合法范围 [0,mRepeatCount+1] 中
    fraction = clampFraction(fraction);
    // 当前迭代次数
    int iteration = getCurrentIteration(fraction);
    /**
     * fraction 是 包含有 mRepeatCount 的值
     * iteration 是 迭代的次数
     * 两者相减 fraction - iteration 得出的 currentFraction 则为当前迭代中的 进度
     */
    float currentFraction = fraction - iteration;
    // 计算最终当次迭代的 fraction 值,主要是受 inReverse 和 REVERSE 的影响
    return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;
}
复制代码

我们回到 FLAG(17),进入 animateValue 方法,具体代码如下。我们需要明确的是,传进来的参数是当次的进度,也就是不含循环次数的。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

以下图片均为手写,勿喷:smile:

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

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

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

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

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

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

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

带有活力的属性动画源码分析与实战——Android高级UI
经过上面四种情况,我们可以知道 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、多维雷达图

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

效果图

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

动画分析

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

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

准备零件

(1)顶点坐标一图胜千言,我们以六维雷达图为例,以比较有代表性的A,B,C三点来计算其坐标。但这里面有一个前提是,需要将 画布的原点移至view的中心 。接下来 具体的计算请看图 ,中间涉及到一些简单的三角函数,这里就不过多的说明。

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

根据图片中的计算规则,我们可以得知以 画布的负y轴 为基准,依次使用 sin(角度) * L 得出该点的 x坐标 ,用 cos(角度) * L 得出该点的 y坐标 。具体的代码如下:

// 循环遍历计算顶点坐标
for (int i = 0; i < mDimenCount; ++i) {
    PointF point = new PointF();
    // 当前角度
    double curAngle = i * mAngle;
    // 转弧度制
    double radian = Math.toRadians(curAngle);
    // 计算其 x、y 的坐标
    // y轴需要进行取反,因为canvas的坐标轴和我们数学中的坐标轴的y轴正好是上下相反的
    point.x = (float) (mLength * Math.sin(radian));
    point.y = (float) -(mLength * Math.cos(radian));
    mVertexList.add(point);
}
复制代码

(2)维度展开的属性动画从第一小节我们得到了所有顶点的坐标,再根据传入的数据(数据是以百分比传入,即0f-1f),便可以计算出每个维度的数据的最终顶点坐标,具体代码如下

/**
 * 计算数据的顶点坐标
 *
 * @param isBase 是否为 基础数据
 */
private void calculateDataVertex(boolean isBase) {

    List<Data> calDataList = isBase ? mBaseDataList : mDataList;

    for (int i = 0; i < calDataList.size(); ++i) {

        Data data = calDataList.get(i);

        // 获取 比例数据
        List<Float> pointDataList = data.getData();

        // 设置路径
        Path curPath = new Path();
        data.setPath(curPath);

        curPath.reset();
        for (int j = 0; j < pointDataList.size(); ++j) {

            // 当前维度的数据比例
            float ratio = pointDataList.get(j);
            // 当前维度的顶点坐标
            PointF curDimenPoint = mVertexList.get(j);

            if (j == 0) {
                curPath.moveTo(curDimenPoint.x * ratio,
                        curDimenPoint.y * ratio);
            } else {
                curPath.lineTo(curDimenPoint.x * ratio,
                        curDimenPoint.y * ratio);
            }

        }
        curPath.close();

    }

}
复制代码

经过以上代码的计算,得到每个数据中每个维度的最终顶点最坐标,最后就是设置的属性动画起始值和终止值,以及更新处理。

起始值当然是 0 ,而终止值是 数据量个数 * (维度数-1) ,动画时长为 每个维度的动画时长 * 终止值 。具体如下代码

mTotalLoopCount = (mDimenCount - 1) * mDataList.size();
mAnimator = ValueAnimator.ofFloat(0f, mTotalLoopCount);
mAnimator.setDuration(DURATION * mTotalLoopCount);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {

        float value = (float) animation.getAnimatedValue();

        // 整数部分即为当前的动画数据下标
        mCurLoopCount = (int) value;

        // 小数部分极为当前维度正在展开的进度百分比
        mAnimCurValue = value - mCurLoopCount;

        invalidate();
    }
});
mAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        // 动画结束,将状态置为初始状态,并再刷新一次,让最后的数据全部显示
        mCurState = INIT;
        invalidate();
    }
});
复制代码

最后就是如何将这个值使用起来,因为我们传入的是浮点数,所以在 AnimatorUpdateListener 回调时,获得的数会有 整数部分小数部分 ,对 整数部分 进行 除以(维度数-1) ,得到 当前的数据量下标 ;对 整数部分 进行 (维度数-1)取余,再加1 ,得到 当前数据的维度数 ,而 小数部分就是我们的维度进度。 代码如下:

// 当前数据的下标(-1因为第一个维度不用动画)
int curIndex = mCurLoopCount / (mDimenCount - 1);
// 当前数据的维度(-1因为第一个维度不用动画)
int curDimen = (mCurLoopCount % (mDimenCount - 1)) + 1;
复制代码

组装零件

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

2、表盘指示器

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


以上所述就是小编给大家介绍的《带有活力的属性动画源码分析与实战——Android高级UI》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Kafka权威指南

Kafka权威指南

Neha Narkhede、Gwen Shapira、Todd Palino / 薛命灯 / 人民邮电出版社 / 2017-12-26 / 69.00元

每个应用程序都会产生数据,包括日志消息、度量指标、用户活动记录、响应消息等。如何移动数据,几乎变得与数据本身一样重要。如果你是架构师、开发者或者产品工程师,同时也是Apache Kafka新手,那么这本实践指南将会帮助你成为流式平台上处理实时数据的专家。 本书由出身于LinkedIn的Kafka核心作者和一线技术人员共同执笔,详细介绍了如何部署Kafka集群、开发可靠的基于事件驱动的微服务,......一起来看看 《Kafka权威指南》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具