自定义属性动画框架

作者: Joker_Wan | 来源:发表于2019-12-09 12:50 被阅读0次

    通过本篇文章,你将会了解

    • 安卓属性动画的基本架构
    • 插值器和估值器在动画中的作用
    • 手撸属性动画

    设想一下,如果你是google的工程师,让你去设计一个属性动画,你该如何设计?在设计属性动画时我们应该要考虑哪些问题?

    • 生成动画的api调用约简单越好
    • 一个View可以有多个动画,但同时只能有一个在运行
    • 动画的执行不能依赖自身的for循环
    • 如何让动画动起来

    我们先来看下属性动画的种类

    • 平移动画
    • 透明度动画
    • 缩放动画
    • 旋转动画
    • 帧动画

    属性动画的使用

    ObjectAnimator animator = ObjectAnimator.ofFloat(view,"scale",1f,2f,3f);
    animator.setInterpolator(new LinearInterpolator());
    animator.setDuration(500);
    animator.start() 
    

    动画的本质

         动画实际上是改变View在某一时间点上的样式属性,比如在0.1s的时候View的x坐标为50px,在0.2s的时候View的x坐标变为150px,在0.3s的时候View的x坐标变为250px,肉眼看就会感觉View在向右移动。

        实际上是通过一个线程每隔一段时间通过调用view.setX(index++)来改变属性值产生动画效果。

        动画实际上是一个复杂的流程,需要考虑的因素比较多,在开发者层面不建议直接调用view.setX()来实现动画。

    动画架构分析

    image.png

          根据上面的架构图,我们将动画任务拆成若干个关键帧,每个关键帧在不同的时间点执行自己的动画,最终将整个动画完成,但每两个关键帧之间是有时间间隔的,我们要实现一个补帧的操作来过渡两个关键帧动画,使动画看起来衔接平滑自然。
          这里可能大家会有一个疑问:为什么要将动画分解成不同的关键帧?原因是动画完成是需要时间开销的。如果不给出关键帧动画,动画的过程将无法控制,而且在不同的时间点,控件的状态也不一样。

    代码设计架构图

    image.png

    撸代码

    1、首先我们来模拟VSync信号,每隔16ms发送一个信号去遍历animationFrameCallbackList执行动画Callback,定义一个VSyncManager类来模拟

    public class VSyncManager {
        private List<AnimationFrameCallback> list = new ArrayList<>();
    
        public static VSyncManager getInstance() {
            return Holder.instance;
        }
    
        private VSyncManager() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(16);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        for (AnimationFrameCallback animationFrameCallback : list) {
                            animationFrameCallback.doAnimationFrame(System.currentTimeMillis());
                        }
                    }
                }
            }).start();
        }
    
        interface AnimationFrameCallback {
            boolean doAnimationFrame(long currentTime);
        }
    
        public void add(AnimationFrameCallback animationFrameCallback) {
            list.add(animationFrameCallback);
        }
    
        static class Holder {
            static final VSyncManager instance = new VSyncManager();
        }
    }
    

    定义一个时间插值器TimeInterpolator

    public interface TimeInterpolator {
        float getInterpolator(float input);
    }
    

    创建一个线性插值器LinearInterpolator实现TimeInterpolator,插值器的输出我们定义为输入的一般,你可以设置你想要的任何值

    public class LinearInterpolator implements TimeInterpolator {
        @Override
        public float getInterpolator(float input) {
            return 0.5f*input;
        }
    }
    

    接着定义我们的关键帧实体类MyFloatKeyFrame,主要用来存储三个属性:当前动画执行的进度百分比,当前帧对应的View的属性值,当前帧对应的属性值的类型

    public class MyFloatKeyFrame {
        //当前的百分比
        float fraction;
        //当前帧对应的属性值
        float mValue;
        //当前帧对应得值得类型
        Class mValueType;
    
        public MyFloatKeyFrame(float fraction, float mValue) {
            this.fraction = fraction;
            this.mValue = mValue;
            mValueType = float.class;
        }
    
        public float getValue() {
            return mValue;
        }
    
        public void setValue(float mValue) {
            this.mValue = mValue;
        }
    
        public float getFraction() {
            return fraction;
        }
    
        public void setFraction(float fraction) {
            this.fraction = fraction;
        }
    }
    

    再接着定义关键帧集合,用来初始化关键帧信息并且返回对应的View的属性值

    public class MyKeyframeSet {
        //类型估值器
        TypeEvaluator mEvaluator;
        List<MyFloatKeyFrame> mKeyFrames;
    
        public MyKeyframeSet(MyFloatKeyFrame... keyFrame) {
            this.mEvaluator = new FloatEvaluator();
            mKeyFrames = Arrays.asList(keyFrame);
        }
    
        //关键帧初始化
        public static MyKeyframeSet ofFloat(float[] values) {
            if (values.length <= 0) {
                return null;
            }
            int numKeyframes = values.length;
            //循环赋值
            MyFloatKeyFrame keyFrame[] = new MyFloatKeyFrame[numKeyframes];
            keyFrame[0] = new MyFloatKeyFrame(0, values[0]);
            for (int i = 1; i < numKeyframes; i++) {
                keyFrame[i] = new MyFloatKeyFrame((float) i / (numKeyframes - 1), values[i]);
            }
            return new MyKeyframeSet(keyFrame);
        }
    
        //获取当前百分比对应得具体属性值
        public Object getValue(float fraction) {
            MyFloatKeyFrame prevKeyFrame = mKeyFrames.get(0);
            for (int i = 0; i < mKeyFrames.size(); i++) {
                MyFloatKeyFrame nextKeyFrame = mKeyFrames.get(i);
                if (fraction < nextKeyFrame.getFraction()) {
                    //当前百分比在此之间
                    //计算间隔百分比
                    float intervalFraction = (fraction - prevKeyFrame.getFraction())
                            / (nextKeyFrame.getFraction() - prevKeyFrame.getFraction());
                    //通过估值器返回对应得值
                    return mEvaluator == null ?
                            prevKeyFrame.getValue() + intervalFraction * (nextKeyFrame.getValue() - prevKeyFrame.getValue()) :
                            ((Number) mEvaluator.evaluate(intervalFraction, prevKeyFrame.getValue(), nextKeyFrame.getValue())).floatValue();
                }
                prevKeyFrame = nextKeyFrame;
            }
            //对应得帧不够
            return mKeyFrames.get(mKeyFrames.size() - 1).getValue();
        }
    }
    

    根据当前动画执行进度百分比fraction获取对应得具体属性值的相关计算逻辑可以参考下图


    image.png

    接下来我们来定义动画任务属性值管理类MyFloatPropertyValuesHolder,主要作用是通过反射获取控件对应的方法,然后通过调用该方法(如setScale)给控件设置相应的属性值

    public class MyFloatPropertyValuesHolder {
        //属性名
        String mPropertyName;
        //属性类型 float
        Class mValueType;
        //反射
        Method mSetter = null;
        //关键帧管理类
        MyKeyframeSet mKeyframeSet;
    
        public MyFloatPropertyValuesHolder(String propertyName, float... values) {
            this.mPropertyName = propertyName;
            mValueType = float.class;
            //交给关键帧管理初始化
            mKeyframeSet = MyKeyframeSet.ofFloat(values);
        }
    
        //通过反射获取控件对应的方法
        public void setupSetter() {
            char firstLetter = Character.toUpperCase(mPropertyName.charAt(0));
            String theRest = mPropertyName.substring(1);
            //setScaleX
            String methodName = "set" + firstLetter + theRest;
            try {
                mSetter = View.class.getMethod(methodName, float.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    
        //给控件设置相应的属性值
        public void setAnimatedValue(View view, float fraction) {
            Object value = mKeyframeSet.getValue(fraction);
            try {
                mSetter.invoke(view, value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    最后定义我们对开发者暴露的MyObjectAnimator类,功能类似Android源码的的ObjectAnimator类,给开发人员调用设置属性动画的api

    public class MyObjectAnimator implements VSYNCManager.AnimationFrameCallback {
        //动画时长
        private long mDuration = 0;
        //需要执行动画的对象
        private WeakReference<View> mTarget;
        //属性值管理类
        private MyFloatPropertyValuesHolder mFloatPropertyValuesHolder;
        private int index = 0;
        private TimeInterpolator interpolator;
    
    
        public long getDuration() {
            return mDuration;
        }
    
        public void setDuration(long mDuration) {
            this.mDuration = mDuration;
        }
    
        public int getIndex() {
            return index;
        }
    
        public void setIndex(int index) {
            this.index = index;
        }
    
        public TimeInterpolator getInterpolator() {
            return interpolator;
        }
    
        public void setInterpolator(TimeInterpolator interpolator) {
            this.interpolator = interpolator;
        }
    
    
        public MyObjectAnimator(View target, String propertyName, float... values) {
            mTarget = new WeakReference<>(target);
            mFloatPropertyValuesHolder = new MyFloatPropertyValuesHolder(propertyName, values);
        }
    
        public static MyObjectAnimator ofFloat(View target, String propertyName, float... values) {
            MyObjectAnimator anim = new MyObjectAnimator(target, propertyName, values);
            return anim;
        }
    
        //每隔16ms执行一次
        @Override
        public boolean doAnimationFrame(long currentTime) {
            //后续的效果渲染
            //动画的总帧数
            float total = mDuration / 16;
            //拿到执行百分比 (index)/total
            float fraction = (index++) / total;
            //通过插值器去改变对应的执行百分比
            if (interpolator != null) {
                fraction = interpolator.getInterpolator(fraction);
            }
            //循环 repeat
            if (index >= total) {
                index = 0;
            }
            //交给mFloatPropertyValuesHolder,改变对应的属性值
            mFloatPropertyValuesHolder.setAnimatedValue(mTarget.get(), fraction);
            return false;
        }
    
        //开启动画
        public void start() {
            //交给mFloatPropertyValuesHolder改变对应的属性值
            mFloatPropertyValuesHolder.setupSetter();
            VSYNCManager.getInstance().add(this);
        }
    }
    
    

    最后我们来使用下MyObjectAnimator来看看动画效果

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            button = findViewById(R.id.bottom);
            MyObjectAnimator animator = MyObjectAnimator.ofFloat(button, "ScaleX", 1f, 2f, 3f, 1f);
            animator.setInterpolator(new LineInterpolator());
            animator.setDuration(3000);
            animator.start();
        }
    

    布局文件如下

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/bottom"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#008500"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </android.support.constraint.ConstraintLayout>
    

    效果如下图,对button进行横向缩放,和使用原生的ObjectAnimator实现的效果基本一致

    ObjectAnimator.gif

    相关文章

      网友评论

        本文标题:自定义属性动画框架

        本文链接:https://www.haomeiwen.com/subject/jkybgctx.html