理解属性动画从仿写开始

属性动画的本质:

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

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

1
2
3
4
5
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为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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时刻的值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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:根据当前百分比调用估值器获取响应的值,这里的估值器直接使用系统的估值器里面实现很简单源码如下
1
2
3
4
5
6
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,下面来看我们的入口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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方法的。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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信号,这里通过一个线程来模拟发从信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 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中

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

执行效果:

源码地址

# UI

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×