理解属性动画从仿写开始

属性动画的本质:

改变某一个View某一个时间点的属性值,比如一个View在0.1秒的时候在100px的位置,0.2秒的时候到了200px的位置,这就会让我们感觉到一个动画的效果,实际上就时每隔一段时间调用view.setX()方法。

看一下系统属性动画的简单用法

Button button = findViewById(R.id.button);
 ObjectAnimator animator = ObjectAnimator.ofFloat(button,"translationX",0,200);
        animator.setDuration(2000);
        animator.start();
        animator.setInterpolator(new LinearInterpolator());

通过上面的方法,就会让一个button移动起来。下面我们自己来实现一个这个效果。

动画是需要时间来完成的,不同的时间节点,控件的状态也不一样。所以需要把一个动画分解成一个一个的关键帧。

看下面的流程图

此图是我自己的理解,如果问题欢迎指正。

当我们开始调用一个动画的时候,比如 ObjectAnimator.ofFloat(button,"translationX",0,200);

  1. 首先会把我们传入的View保存起来
  2. 然后会初始化一个Holder对象,这个Holder对象是用来干啥的呢?我们传入了一个translationX值,Holder对象中提供一个方法,把translationX拼接成一个set方法,setTranslationX,然后通过反射找到View中的此方法。当数值计算出来之后,执行获取的这个方法。
  3. KeyframeSet是一个关键帧的集合,最后我们传入0-200就是关键帧
  4. 插值器,用来计算某一时间中动画长度播放的百分比
  5. 估值器,根据插值器计算的百分比,来计算某个时间,动画所要更新的值。然后交给Holder执行动画
  6. 属性动画会监听系统发出的VSYNC信号,每收到一次信号就执行一次。

什么是FPS?FPS代表每秒的帧数,当FPS>=60的时候,我们的肉眼就不会感觉到卡顿了,FPS=60是个什么概念呢,1000/60≈16.6也就是说,想要不卡顿,我们需要在16毫秒以内绘制一帧出来。

什么是VSYNC?VSYNC是垂直同步的缩写,每16毫秒发送一个VSYNC信号,系统拿到这个信号之后开始刷新屏幕。

OK上面的概念大体了解了开干吧。

先来个简单的实体类Keyframe,这里以FloatKeyframe为例

public class MyFloatKeyframe {
    float mFraction;
    Class mValueType;
    float mValue;

    public MyFloatKeyframe(float fraction, float value) {
        mFraction = fraction;
        mValueType = float.class;
        mValue = value;
    }

    public float getValue() {
        return mValue;
    }

    public void setValue(float value) {
        mValue = value;
    }

    public float getFraction() {
        return mFraction;
    }
}

它就是个实体类,只要包含三个对象,当前执行的百分比,关键帧中的值的类型和动画在mFraction时刻的值

下面在来个对象,关键帧的集合

public class MyKeyframeSet {
    /**
     * 类型估值器
     */
    TypeEvaluator mEvaluator;
    /**
     * 第一帧
     */
    MyFloatKeyframe mFirstKeyframe;
    /**
     * 帧的集合
     */
    List mKeyframes;

    private MyKeyframeSet(MyFloatKeyframe ... keyframes){
        mKeyframes = Arrays.asList(keyframes);
        mEvaluator = new FloatEvaluator();
        mFirstKeyframe = keyframes[0];
    }

    public static MyKeyframeSet ofFloat(float[] values) {
        //开始组装每一帧
        int numKeyframes = values.length;
        MyFloatKeyframe keyframes[] = new MyFloatKeyframe[numKeyframes];
        //先放入第一帧
        keyframes[0] = new MyFloatKeyframe(0, values[0]);
        for (int i = 1; i < numKeyframes; i++) {
            keyframes[i] = new MyFloatKeyframe((float)i/(numKeyframes-1),values[i]);
        }
        return new MyKeyframeSet(keyframes);
    }

    /**
     * 根据当前百分比获取响应的值
     * @param fraction 百分比
     * @return
     */
    public Object getValue(float fraction){
        MyFloatKeyframe preKeyFrame = mFirstKeyframe;
        for (int i = 1; i < mKeyframes.size(); ++i) {
            MyFloatKeyframe nextKeyFrame = mKeyframes.get(i);
            if(fraction<nextKeyFrame.getFraction()){
                return mEvaluator.evaluate(fraction,preKeyFrame.getValue(),nextKeyFrame.getValue());
            }
            preKeyFrame = nextKeyFrame;
        }
        return null;
    }
}

这里面有两个比较关键的方法

  • ofFloat:这是个静态的方法,用于构造帧集合对象,根据我们传入的关键帧的个数,来组件一个关键帧的数组,然后创建出关键帧帧集合对象并传入创建的关键帧数组。
  • getValue:根据当前百分比调用估值器获取响应的值,这里的估值器直接使用系统的估值器里面实现很简单源码如下
public class FloatEvaluator implements TypeEvaluator {
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}

我们知道估值器返回的是当前View需要移动的距离,上面的估值器就是返回开始的值加上(还剩的值乘以需要执行的百分比)就是当前View需要执行的数值。

OK,下面来看我们的入口类

public class MyObjectAnimator implements VSYNCManager.AnimatorFrameCallBack{
    /**
     * 动画执行的时长
     */
    private long mDuration = 0;
     /**
     * 插值器
     */
    private TimeInterpolator interpolator;
    private MyFloatPropertyValuesHolder mPropertyValuesHolder;
    /**
     * View是个比较重量级的对象,放到WeakReference中方便回收
     */
    private WeakReference target;

    private Long mStartTime = -1L;
    /**
     * 执行到哪里
     */
    private float index = 0;

    public void setDuration(long duration) {
        mDuration = duration;
    }
    public void setInterpolator(TimeInterpolator interpolator) {
        this.interpolator = interpolator;
    }

    private MyObjectAnimator(View view,String propertyName, float... values){
        target = new WeakReference(view);
        mPropertyValuesHolder = new MyFloatPropertyValuesHolder(propertyName,values);
    }

    public static MyObjectAnimator ofFloat(View view,String propertyName, float... values){
       return new MyObjectAnimator(view,propertyName,values);
    }


    public void start() {
        mPropertyValuesHolder.setupSetter(target);
        mStartTime = System.currentTimeMillis();
        VSYNCManager.getInstance().add(this);
    }

    @Override
    public void doAnimator(long currentTime) {
        float total= mDuration / 16;
        //执行的百分比
        float fraction = (index++)/total;

        //通过插值器,改变百分比的值
        if(interpolator != null){
            interpolator.getInterpolation(fraction);
        }
        //循环播放
        if(index>=total){
            index = 0;
        }
        mPropertyValuesHolder.setAnimatedValue(target.get(),fraction);
    }
}

它实现了一个VSYNCManager的对调对象,用来在回调方法doAnimator中执行动画

构造方法中将传入的View保存起来,View是个比较重量级的对象,放到WeakReference中方便回收

然后初始化了Holder对象,开始的时候我们知道Hodler对象是用来找到我们传入View的相关属性的set方法的。如下:

public class MyFloatPropertyValuesHolder {

    String mPropertyName;
    Class mValueType;
    MyKeyframeSet mKeyframes;
    Method mSetter = null;

    public MyFloatPropertyValuesHolder(String propertyName, float... values) {
        mPropertyName = propertyName;
        mValueType = float.class;
        mKeyframes = MyKeyframeSet.ofFloat(values);
    }

    /**
     * 执行View 的相关的set 方法
     * @param target view
     */
    public void setupSetter(WeakReference target) {
        //第一个字符大写 比如传过来的 translationX
        char firstLetter = Character.toUpperCase(mPropertyName.charAt(0));
        String theRest = mPropertyName.substring(1);
        //拼成 setTranslationX 方法
        String methodName = "set"+firstLetter+theRest;
        try {
            //通过反射拿到这个View的setTranslationX方法
            mSetter = View.class.getMethod(methodName, float.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    /**
     *  设置动画的值 执行setTranslationX方法
     * @param target view
     * @param fraction 百分比
     */
    public void setAnimatedValue(View target, float fraction) {
        Object value = mKeyframes.getValue(fraction);
        try {
            mSetter.invoke(target,value);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Holder类中有两个方法:

  • setupSetter:就是通过我们传入的字符串,拼接成一个set方法,然后通过反射找到这个方法,保存到成员变量中。
  • setAnimatedValue:找到当前动画需要设置的值,然后调用目标View的相关set方法。

最后是VSYNC信号,由于我们无法拿到系统的VSYNC信号,这里通过一个线程来模拟发从信号。

public class VSYNCManager {

    AnimatorFrameCallBack mAnimatorFrameCallBack;
    /**
     * 可能会有多个动画同事使用,所以弄个集合
     */
    private List list = new ArrayList();

    private VSYNCManager(){
        new Thread(mRunnable).start();
    }

    public static VSYNCManager getInstance(){
        return new VSYNCManager();
    }

    public void add(AnimatorFrameCallBack callBack){
        list.add(callBack);
    }

    Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
        while (true){
            try {
                Thread.sleep(16);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (AnimatorFrameCallBack callback : list) {
                callback.doAnimator(System.currentTimeMillis());
            }
           }
        }
    };

    interface AnimatorFrameCallBack{
        void doAnimator(long currentTime);
    }

}

很简单,开启一个线程,每睡16毫秒执行一个回调方法。因为可能不止一个View在监听这个信号,所以这里使用一个集合来保存回调对象,发送信号的时候循环遍历执行回调。

类中有个回调接口供我们的入口类MyObjectAnimator实现,发送信号的时候,执行回调方法doAnimator。

下面在看一下MyObjectAnimator中的回调方法doAnimator

 public void doAnimator(long currentTime) {
        float total= mDuration / 16;
        //执行的百分比
        float fraction = (index++)/total;

        //通过插值器,改变百分比的值
        if(interpolator != null){
            interpolator.getInterpolation(fraction);
        }
        //循环播放
        if(index>=total){
            index = 0;
        }
        mPropertyValuesHolder.setAnimatedValue(target.get(),fraction);
    }
    //系统的匀速插值器
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return input;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

  • 我们传入的时间除以16,就是总共需要执行的次数
  • 使用index来表示我们当前已经走了的次数,它跟总次数相除就是当前执行的百分比
  • 通过插值器,改变百分比的值,如果是匀速的,当前的插值器返回的就是当前的值不变,从上面系统的匀速插值器LinearInterpolator中的getInterpolation方法也可以看到,我们传入啥就返回啥。如果不是匀速的,返回的百分比的值也就不一样,后面通过这个百分比算出来的需要执行的值也就不一样,我们看到的View动画执行速度也就不是匀速了。
  • 最后调用mPropertyValuesHolder.setAnimatedValue方法传入百分比来执行动画。

OK,简易的动画到这里就写完了,怎么用呢,来到Activity中

MyObjectAnimator animator = MyObjectAnimator.ofFloat(button, "translationX", 0, 200);
              animator.setInterpolator(new LinearInterpolator());
              animator.setDuration(2000);
              animator.start();

执行效果:

源代地址