理解属性动画从仿写开始
属性动画的本质:
改变某一个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);
- 首先会把我们传入的View保存起来
- 然后会初始化一个Holder对象,这个Holder对象是用来干啥的呢?我们传入了一个translationX值,Holder对象中提供一个方法,把translationX拼接成一个set方法,setTranslationX,然后通过反射找到View中的此方法。当数值计算出来之后,执行获取的这个方法。
- KeyframeSet是一个关键帧的集合,最后我们传入0-200就是关键帧
- 插值器,用来计算某一时间中动画长度播放的百分比
- 估值器,根据插值器计算的百分比,来计算某个时间,动画所要更新的值。然后交给Holder执行动画
- 属性动画会监听系统发出的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();
执行效果: