77

理解属性动画从仿写开始

 5 years ago
source link: https://chsmy.github.io/2019/04/14/technology/理解属性动画从仿写开始/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

属性动画的本质:

改变某一个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移动起来。下面我们自己来实现一个这个效果。

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

看下面的流程图

bUJfQnr.png!web

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

当我们开始调用一个动画的时候,比如 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<MyFloatKeyframe> 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<Number> {
    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<View> 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<View> 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<AnimatorFrameCallBack> 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();

执行效果:

FFNj6fj.gif

源代地址


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK