Android 动画简介
Android中动画分为帧动画,补间动画以及属性动画。
1. 帧动画
1.1 在XML文件中使用:
<animation-list>
必需
该元素必须是根元素。包含一个或多个 <item>
元素。
属性:
-
android:oneshot
布尔值。仅执行一次动画,则为“true”;如果要循环播放动画,则为“false”。
-
<item>
单帧动画。必须为
<animation-list>
元素的子项。属性:
-
android:drawable
可绘制资源。要用于此帧的可绘制对象。
-
android:duration
整型。显示此帧的持续时间,以毫秒为单位。
-
示例如下:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false" >
<item android:drawable="@drawable/pikachu_1" android:duration="200" />
<item android:drawable="@drawable/pikachu_2" android:duration="200" />
<item android:drawable="@drawable/pikachu_3" android:duration="200" />
</animation-list>
1.2 代码调用:
val image: ImageView = findViewById(R.id.image)
image.setBackgroundResource(R.drawable.pikachu)
val animation = image.background
if (animation is Animatable) {
animation.start()
}
animation.start()
与animation.stop()
控制动画的开始以及关闭
1.3 源码分析
动画开始是由animation.start()
开始的
/*
* 动画入口
* AnimationDrawable 的方法
*/
public void start() {
mAnimating = true;
if (!isRunning()) {
// 从第一帧开始
setFrame(0, false, mAnimationState.getChildCount() > 1
|| !mAnimationState.mOneShot);
// --> 分析1
}
}
/*
* 分析1
* AnimationDrawable 的方法
*/
private void setFrame(int frame, boolean unschedule, boolean animate) {
if (frame >= mAnimationState.getChildCount()) {
return;
}
mAnimating = animate;
mCurFrame = frame;
// 设置当前帧 Drawable
selectDrawable(frame);
if (unschedule || animate) {
// 触发 Callback 来停止更新
unscheduleSelf(this);
}
if (animate) {
// Unscheduling may have clobbered these values; restore them
mCurFrame = frame;
mRunning = true;
// 触发 Callback 来开始更新
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
// --> 分析2
}
}
/*
* 分析2
* Drawable 的方法
*/
public void scheduleSelf(@NonNull Runnable what, long when) {
// 获取 callback, callback 来自于 View.setBackground(Drawable background)
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
// --> 分析3
}
}
/*
* 分析3
* View 的方法
*/
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
if (verifyDrawable(who) && what != null) {
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null) {
// Choreographer 编排器负责协调动画、输入和绘制的时间。从显示子系统接收定时脉冲(如垂直同步),然后安 排呈现下一个显示帧的一部分工作。
// postCallbackDelayed 提交一个回调函数,在指定的延迟之后运行下一帧。回调函数执行一次就会被自动移 // 除。
mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, what, who,
Choreographer.subtractFrameDelay(delay));
// --> 分析4
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().postDelayed(what, delay);
}
}
}
/*
* 分析4
* Choreographer 的方法
*/
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
}
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
// -->分析5
}
/*
* 分析5
* Choreographer 的方法
*/
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
// 判断是否仍需要延迟执行
if (dueTime <= now) {
scheduleFrameLocked(now);
// --> 分析8
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
// --> 分析6
}
}
}
/*
* 分析6
* Choreographer 的内部Handler
*/
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0, new DisplayEventReceiver.VsyncEventData());
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
// 此处对 分析6 中所传递的消息进行处理
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
// --> 分析7
break;
}
}
}
/*
* 分析7
* Choreographer 的内部Handler
*/
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
// 可以看到此处仍调用了 分析8的方法
scheduleFrameLocked(now);
}
}
}
}
/*
* 分析8
* Choreographer 的方法
*/
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}
// 如果在Looper线程上运行,则立即调度垂直同步,否则尽快从UI线程发布消息调度垂直同步。
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
// 接下来在一系列消息传递以及方法回调后,最终将调用至 AnimationDrawable 的 run 方法
public void run() {
nextFrame(false);
}
private void nextFrame(boolean unschedule) {
int nextFrame = mCurFrame + 1;
final int numFrames = mAnimationState.getChildCount();
final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);
// 多次触发的动画在必要的时候循环。
if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
nextFrame = 0;
}
// 开始循环调用 setFrame 方法
setFrame(nextFrame, unschedule, !isLastFrame);
}
2. 补间动画
2.1 在XML文件中使用:
文件位置:res/anim/filename.xml
资源引用:
- 在 Kotlin / Java 中:
R.anim.filename
- 在 XML 中:
@[package:]anim/filename
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<alpha
android:fromAlpha="float"
android:toAlpha="float" />
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float" />
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float" />
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float" />
<set>
...
</set>
</set>
该文件必须具有一个根元素,可以是 <alpha>
、<scale>
、<translate>
、<rotate>
或包含一组(或多组)其他动画元素(包括嵌套的 <set>
元素)的 <set>
元素。
元素标签:
-
<set>
容纳其他动画元素(
<alpha>
、<scale>
、<translate>
、<rotate>
)或其他<set>
元素的容器。代表AnimationSet
。android:interpolator
插值器资源,要应用于动画的Interpolator
。该值是对指定插值器的资源的引用。android:shareInterpolator
布尔值。如果为“true”则表示要在所有子元素中共用同一插值器。
-
<alpha>
淡入或淡出动画。代表
AlphaAnimation
。android:fromAlpha
浮点数。起始不透明度偏移,0.0 表示透明,1.0 表示不透明。android:toAlpha
浮点数。结束不透明度偏移。
-
<scale>
大小调整动画。您可以通过指定
pivotX
和pivotY
,来指定图片向外(或向内)扩展的中心点。例如,如果这两个值为 0、0(左上角),则所有扩展均向右下方向进行。代表ScaleAnimation
。android:fromXScale
浮点数。起始 X 尺寸偏移,其中 1.0 表示不变。android:toXScale
浮点数。结束 X 尺寸偏移。android:fromYScale
浮点数。起始 Y 尺寸偏移。android:toYScale
浮点数。结束 Y 尺寸偏移。android:pivotX
浮点数。在对象缩放时要保持不变的 X 坐标。android:pivotY
浮点数。在对象缩放时要保持不变的 Y 坐标。
-
<translate>
竖直和/或水平移动。支持采用以下三种格式之一的以下属性:从 -100 到 100 的以“%”结尾的值,表示相对于自身的百分比;从 -100 到 100 的以“%p”结尾的值,表示相对于其父项的百分比;不带后缀的浮点值,表示绝对值。代表
TranslateAnimation
。android:fromXDelta
浮点数或百分比。起始 X 偏移。表示方式:相对于正常位置的像素数(例如"5"
),相对于元素宽度的百分比(例如"5%"
),或相对于父项宽度的百分比(例如"5%p"
)。android:toXDelta
浮点数或百分比。结束 X 偏移。android:fromYDelta
浮点数或百分比。起始 Y 偏移。android:toYDelta
浮点数或百分比。结束 Y 偏移。
-
<rotate>
旋转动画。代表
RotateAnimation
。android:fromDegrees
浮点数。起始角度位置,以度为单位。android:toDegrees
浮点数。结束角度位置,以度为单位。android:pivotX
浮点数或百分比。旋转中心的 X 坐标。表示方式:相对于对象左边缘的像素数(例如"5"
),相对于对象左边缘的百分比(例如"5%"
),或相对于父级容器左边缘的百分比(例如"5%p"
)。android:pivotY
浮点数或百分比。旋转中心的 Y 坐标。
2.2 代码调用
val image: ImageView = findViewById(R.id.image)
val animation: Animation = AnimationUtils.loadAnimation(this, R.anim.animation)
image.startAnimation(animation)
2.3 源码分析
// 利用配置文件生成动画对象
val animation: Animation = AnimationUtils.loadAnimation(this, R.anim.animation)
// 启动动画
image.startAnimation(animation)
// 生成动画对象的入口
public static Animation loadAnimation(Context context, @AnimRes int id)
throws NotFoundException {
XmlResourceParser parser = null;
try {
// 获取资源中对应的动画
parser = context.getResources().getAnimation(id);
//
return createAnimationFromXml(context, parser);
}
...
}
private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
throws XmlPullParserException, IOException {
// --> 分析1
return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
}
/*
* 分析1
* AnimationUtils 的方法
*/
private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
Animation anim = null;
int type;
int depth = parser.getDepth();
// 利用 XmlPullParser 对配置文件进行解析并最终返回动画对象
while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (name.equals("set")) {
anim = new AnimationSet(c, attrs);
createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
} else if (name.equals("alpha")) {
anim = new AlphaAnimation(c, attrs);
} else if (name.equals("scale")) {
anim = new ScaleAnimation(c, attrs);
} else if (name.equals("rotate")) {
anim = new RotateAnimation(c, attrs);
} else if (name.equals("translate")) {
anim = new TranslateAnimation(c, attrs);
} else if (name.equals("cliprect")) {
anim = new ClipRectAnimation(c, attrs);
} else {
throw new RuntimeException("Unknown animation name: " + parser.getName());
}
if (parent != null) {
parent.addAnimation(anim);
}
}
return anim;
}
// 启动动画的入口 View 的方法
public void startAnimation(Animation animation) {
// 当start time设置为START_ON_FIRST_FRAME时
// 动画将在第一次调用getTransformation(long, Transformation)时开始
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
// --> 分析2
// 标记此视图的父视图应清除其缓存,用于强制父视图重建其显示列表
invalidateParentCaches();
// 使绘图缓存失效重新绘制
invalidate(true);
}
/*
* 分析2
* Animation 的方法
*/
public void setAnimation(Animation animation) {
mCurrentAnimation = animation;
if (animation != null) {
// 如果屏幕是关闭的,仍将动画的开始时间设置为现在而不是我们绘制的下一帧。
// 若开始时间保持为START_ON_FIRST_FRAME 将导致动画在屏幕重新打开时开始
if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
&& animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
}
animation.reset();
}
}
3. 属性动画
3.1 在XML中使用
文件位置:res/animator/filename.xml
资源引用:
- 在 Kotlin / Java 代码中:
R.animator.filename
- 在 XML 中:
@[package:]animator/filename
<set
android:ordering=["together" | "sequentially"]>
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["restart" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<animator
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["restart" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<set>
...
</set>
</set>
该文件必须具有一个根元素,可以是 <set>
、<objectAnimator>
或 <valueAnimator>
。可以将动画元素(包括其他 <set>
元素)组合到 <set>
元素中。
元素:
-
<set>
容纳其他动画元素(
<objectAnimator>
、<valueAnimator>
或其他<set>
元素)的容器。代表AnimatorSet
。可以指定嵌套的<set>
标记来将动画进一步组合在一起。每个<set>
都可以定义自己的ordering
属性。android:ordering
关键字。指定集合中动画的播放顺序。
值 | 说明 |
---|---|
sequentially | 依序播放此集合中的动画 |
together (默认) | 同时播放此集合中的动画。 |
-
<objectAnimator>
在特定的一段时间内为对象的特定属性创建动画。代表
ObjectAnimator
。android:propertyName
字符串。必需。通过其名称引用来添加动画效果的对象属性。例如为 View 对象指定"alpha"
或"backgroundColor"
。但是,objectAnimator
元素不包含target
属性,因此无法在 XML 声明中设置要添加动画效果的对象。必须通过调用loadAnimator()
来扩充动画资源,然后调用setTarget()
来设置包含此属性的目标。android:valueTo
浮点数、整数或颜色。必需。动画属性的结束值。颜色以六位十六进制数字表示(例如,#333333)。android:valueFrom
浮点数、整数或颜色。动画属性的起始值。如果未指定,则动画将从属性的 get 方法获得的值开始。android:duration
整数。动画的时间,以毫秒为单位。默认为 300 毫秒。android:startOffset
整数。调用start()
后动画延迟的毫秒数。android:repeatCount
整数。动画的重复次数。设为"-1"
表示无限次重复。值"1"
表示动画在初次播放后重复播放一次。默认值为"0"
,不重复。android:repeatMode
整数。动画播放到结尾处的行为。android:repeatCount
必须设置为正整数或"-1"
,该属性才有效。设置为"reverse"
可让动画在每次迭代时反向播放,设置为"restart"
则可让动画每次从头开始循环播放。
-
<animator>
在指定的时间段内执行动画。 代表
ValueAnimator
。- 其属性与
<objectAnimator>
类似。
- 其属性与
3.2 代码调用
val set: AnimatorSet = AnimatorInflater.loadAnimator(context, R.animator.animator)
.apply {
setTarget(mObject)
start()
}
3.3 源码分析
val set: AnimatorSet = AnimatorInflater.loadAnimator(context, R.animator.animator)
.apply {
// 设置动画的目标对象
setTarget(mObject)
// 启动动画
start()
}
// 设置动画目标对象
public void setTarget(@Nullable Object target) {
final Object oldTarget = getTarget();
// 若不是原对象则取消动画并重新配置
if (oldTarget != target) {
if (isStarted()) {
cancel();
}
mTarget = target == null ? null : new WeakReference<Object>(target);
// New target should cause re-initialization prior to starting
mInitialized = false;
}
}
// 启动动画
public void start() {
start(false, true);
// --> 分析1
}
/*
* 分析1
* ObjectAnimator 的方法
*/
private void start(boolean inReverse, boolean selfPulse) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
...
// 初始化动画
initAnimation();
// --> 分析2
...
boolean isEmptySet = isEmptySet(this);
if (!isEmptySet) {
// 启动方法
startAnimation();
}
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationStart(this, inReverse);
}
}
if (isEmptySet) {
// 在AnimatorSet为空或者持续时间为0的情况下,将立即触发onAnimationEnd()
end();
}
}
/*
* 分析2
* ObjectAnimator 的方法
*/
private void initAnimation() {
if (mInterpolator != null) {
for (int i = 0; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
// 设置差值器
node.mAnimation.setInterpolator(mInterpolator);
}
}
// 更新动画持续时间
updateAnimatorsDuration();
//创建依赖视图
createDependencyGraph();
}
4. Lottie动画
4.1简介
lottie是一个使用极其简单的开源动画库,支持Android,iOS,React Native 以及 windows。
其存在以下几个优势:
- 与帧动画使用图片相比,Lottie使用JSON文件可以有效减少APP 打包之后的体积
- 可以通过URL加载动画,从而使得程序与动画解耦并且可以不用更新APP就可以更新动画
- 可以通过代码设置使得动画响应交互,并且可以控制动画的前进以及后退
4.2 使用
使用lottie十分简单,我们只需要在项目的build.gradle
中引入如下依赖
dependencies {
...
implementation "com.airbnb.android:lottie:$lottieVersion"
...
}
接下来我们既可以通过布局文件引入,也可以在代码中动态加载。
-
首先我们需要将json文件放入
res/raw [lottie_rawRes]
或者assets/[lottie_fileName]
-
若要在布局文件中引入:
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_rawRes="@raw/hello_world"
// 或者
app:lottie_fileName="hello_world.json"
// 无限循环
app:lottie_loop="true"
// 加载后自动播放
app:lottie_autoPlay="true" />
- 在代码中动态加载
val animationView = LottieAnimationView(context).apply {
setAnimation(R.raw.hello_world)
// 播放结束后下次循环依旧从头开始
repeatMode = LottieDrawable.RESTART
// 无限循环
repeatCount = LottieDrawable.INFINITE
}
animationView.playAnimation()
- 我们可以通过代码动态控制动画只播放一部分
// 动画从15帧播放至49帧
animationView.setMinFrame(15)
animationView.setMaxFrame(49)
// 动画从20%进度播放至80%进度
animationView.setMinProgress(0.2)
animationView.setMaxProgress(0.8)
- 设置监听器来处理交互
animationView.addAnimatorUpdateListener{
// TODO
}
animationView.addAnimatorPauseListener{
// TODO
}
// 将动画设置到40%进度的帧
animationView.setProgress(0.4)
这些就是lottie动画的简单使用,更为详细的介绍可以参考官方文档:http://airbnb.io/lottie/#/android
5. Demo
代码如下:
class MainActivity : AppCompatActivity(), OnClickListener {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.frameAnimation.setOnClickListener(this)
binding.tweenAnimation.setOnClickListener(this)
binding.propertyAnimation.setOnClickListener(this)
}
override fun onClick(view: View) {
when (view.id) {
R.id.frameAnimation -> {
binding.layout.removeAllViews()
val textView = TextView(this)
textView.setBackgroundResource(R.drawable.pikachu)
val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT
, LinearLayout.LayoutParams.MATCH_PARENT)
binding.layout.addView(textView, layoutParams)
val animation = textView.background
if (animation is Animatable) {
animation.start()
}
}
R.id.tweenAnimation -> {
val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT
, LinearLayout.LayoutParams.WRAP_CONTENT)
binding.layout.removeAllViews()
binding.layout.addView(Button(this).apply {
"translate".also { text = it }
setOnClickListener {
it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.translate))
}
}, layoutParams)
binding.layout.addView(Button(this).apply {
"alpha".also { text = it }
setOnClickListener {
it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.alpha))
}
}, layoutParams)
binding.layout.addView(Button(this).apply {
"rotate".also { text = it }
setOnClickListener {
it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.rotate))
}
}, layoutParams)
binding.layout.addView(Button(this).apply {
"scale".also { text = it }
setOnClickListener {
it.startAnimation(AnimationUtils.loadAnimation(it.context, R.anim.scale))
}
}, layoutParams)
}
R.id.propertyAnimation -> {
binding.layout.removeAllViews()
val button = Button(this).apply { text = context.getString(R.string.property_animation_text) }
binding.layout.addView(button, LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT
, LinearLayout.LayoutParams.WRAP_CONTENT))
button.setOnClickListener{
AnimatorSet().apply {
playTogether(ObjectAnimator.ofFloat(button, "translationX", 0f, 450f)
, ObjectAnimator.ofFloat(button, "translationY", 0f, 450f))
interpolator = BounceInterpolator()
duration = 800
start()
}
}
}
}
}
}
布局很简单:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/frameAnimation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAllCaps="false"
android:text="@string/frame_animation" />
<Button
android:id="@+id/tweenAnimation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAllCaps="false"
android:text="@string/tween_animation" />
<Button
android:id="@+id/propertyAnimation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAllCaps="false"
android:text="@string/property_animation" />
<LinearLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" />
</LinearLayout>
补间动画设置如下:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:toYDelta="400"
android:duration="2000" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1"
android:toAlpha="0.1"
android:duration="2000" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<rotate
android:fromDegrees="0"
android:toDegrees="90"
android:pivotX="50%"
android:pivotY="50%"
android:duration="2000" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:fromXScale="1.0"
android:toXScale="2.0"
android:fromYScale="1.0"
android:toYScale="2.0"
android:pivotX="0"
android:pivotY="0"
android:duration="2000" />
</set>
Demo演示效果如下:
6. 差值器与估值器
6.1 差值器(Interpolator)
在补间动画以及属性动画中,我们都是用到了差值器。比如在Demo中:
AnimatorSet().apply {
playTogether(ObjectAnimator.ofFloat(button, "translationX", 0f, 450f)
, ObjectAnimator.ofFloat(button, "translationY", 0f, 450f))
// 设置差值器为 BounceInterpolator
interpolator = BounceInterpolator()
duration = 800
start()
}
差值器是一个设置属性从初始值到结束值变化规律的接口
我们既可以在XML文件中通过android:interpolator
引入,也可以在代码中通过setInterpolator
动态设置。
系统已经为我们内置了9种差值器,基本可以满足大部分使用场景。
类/接口 | 说明 |
---|---|
AccelerateDecelerateInterpolator | 该插值器的变化率在开始和结束时缓慢但在中间会加快。 |
AccelerateInterpolator | 该插值器的变化率在开始时较为缓慢,然后会加快。 |
AnticipateInterpolator | 该插值器先反向变化,然后再急速正向变化。 |
AnticipateOvershootInterpolator | 该插值器先反向变化,再急速正向变化,然后超过定位值,最后返回到最终值。 |
BounceInterpolator | 该插值器的变化会跳过结尾处。 |
CycleInterpolator | 该插值器的动画会在指定数量的周期内重复。 |
DecelerateInterpolator | 该插值器的变化率开始很快,然后减速。 |
LinearInterpolator | 该插值器的变化率恒定不变。 |
OvershootInterpolator | 该插值器会急速正向变化,再超出最终值,然后返回。 |
TimeInterpolator | 该接口用于实现您自己的插值器。 |
差值器中的
override fun getInterpolation(input: Float): Float
方法的输入参数是动画运行的进度百分比,输出参数为属性的变化百分比。
我们给出几个不同的内置差值器的方法实现
AccelerateDecelerateInterpolator
【先加速后减速】
public class AccelerateDecelerateInterpolator extends BaseInterpolator
implements NativeInterpolator {
...
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}
...
}
LinearInterpolator
【线性】
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolator {
...
public float getInterpolation(float input) {
return input;
}
...
}
BounceInterpolator
【回弹】
public class BounceInterpolator extends BaseInterpolator implements NativeInterpolator {
...
private static float bounce(float t) {
return t * t * 8.0f;
}
public float getInterpolation(float t) {
t *= 1.1226f;
if (t < 0.3535f) return bounce(t);
else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
else return bounce(t - 1.0435f) + 0.95f;
}
...
}
根据getInterpolation
方法我们可以做出getInterpolation-input
的关系图,如下:
LinearInterpolator
只是简单地将输入输出AccelerateDecelerateInterpolator
通过平移以及松弛余弦图像得到了斜率先增加后降低并且值域在[0,1]的图像BounceInterpolator
通过分段函数得到了会在结尾处进行衰减式回弹
通过以上分析,我们可以知道如果这九种内置插值器无法满足需要,我们只需要实现
TimeInterpolator
接口并重写getInterpolation
方法即可。
6.2 估值器(TypeEvaluator)
估值器是一个设置属性从 初始值 变化到 结束值 具体数值的接口
假设我们使用线性插值器并且设置的属性是从20变化到100,那么当动画运行到一半时,插值器返回值为0.5;而估值器返回值为20 + (100 - 20) * 0.5 = 60
系统为我们内置了7种估值器
类 | 说明 |
---|---|
ArgbEvaluator | 这个估值器可用于在表示ARGB颜色的整数值之间进行类型插值 |
FloatArrayEvaluator | 这个估值器可用于在float[] 之间进行类型插值 |
FloatEvaluator | 这个估值器可用于在float 之间进行类型插值 |
IntArrayEvaluator | 这个估值器可用于在int[] 之间进行类型插值 |
IntEvaluator | 这个估值器可用于在int 之间进行类型插值 |
PointFEvaluator | 这个估值器可用于在PointF 之间进行类型插值 |
RectEvaluator | 这个估值器可用于在Rect 之间进行类型插值 |
估值器中的
Integer evaluate(float fraction, Integer startValue, Integer endValue)
方法的输入参数是属性变化的百分比,属性的开始值以及属性的结束值,输出参数为属性应设置的当前值。
我们给出几个不同的内置估值器的方法实现
IntEvaluator
【整型】
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
FloatArrayEvaluator
【浮点数组】
public class FloatArrayEvaluator implements TypeEvaluator<float[]> {
private float[] mArray;
public FloatArrayEvaluator() {
}
public FloatArrayEvaluator(float[] reuseArray) {
mArray = reuseArray;
}
@Override
public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
float[] array = mArray;
if (array == null) {
array = new float[startValue.length];
}
for (int i = 0; i < array.length; i++) {
float start = startValue[i];
float end = endValue[i];
array[i] = start + (fraction * (end - start));
}
return array;
}
}
可以总结出当前动画的值就是用结束值与初始值的差值乘以fraction系数,再加上初始值
如同demo中的ObjectAnimator.ofFloat(button, "translationX", 0f, 450f)
底层就是使用了FloatEvaluator对translationX的属性值进行计算
而如果我们用到了ValueAnimator.ofObject()
方法就需要使用到自定义估值器,我们只需要实现TypeEvaluator
接口并重写evaluate
方法即可