通过本篇文章,你将会了解
- 安卓属性动画的基本架构
- 插值器和估值器在动画中的作用
- 手撸属性动画
设想一下,如果你是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()来实现动画。
动画架构分析
根据上面的架构图,我们将动画任务拆成若干个关键帧,每个关键帧在不同的时间点执行自己的动画,最终将整个动画完成,但每两个关键帧之间是有时间间隔的,我们要实现一个补帧的操作来过渡两个关键帧动画,使动画看起来衔接平滑自然。
这里可能大家会有一个疑问:为什么要将动画分解成不同的关键帧?原因是动画完成是需要时间开销的。如果不给出关键帧动画,动画的过程将无法控制,而且在不同的时间点,控件的状态也不一样。
代码设计架构图
撸代码
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
网友评论