Android构建动画的代码语法啰嗦,可读性差。如果能推翻原生 API,构建一套可读性更强的接口就能提高动画的开发效率。本文尝试用 Kotlin 的 DSL 重写了整套构建动画的 API ,使得构建动画的代码量锐减,语义一目了然。另外,Android提供了反转动画的接口,但只有在 API level 26 以上才能使用,本文尝试突破这个限制。
这是 Kotlin 系列的第六篇,文章列表详见末尾。
感谢掘友“上课钟变成打卡钟_”在上一篇文章的留言,是你留言促成了这篇文章的诞生。
原生动画代码
假设需求如下:“缩放 textView 的同时平移 button ,然后拉长 imageView,动画结束后 toast 提示”。用系统原生接口构建如下:
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f);
ObjectAnimator tvAnimator = ObjectAnimator.ofPropertyValuesHolder(textView, scaleX, scaleY);
tvAnimator.setDuration(300);
tvAnimator.setInterpolator(new LinearInterpolator());
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, 100f);
ObjectAnimator btnAnimator = ObjectAnimator.ofPropertyValuesHolder(button, translationX);
btnAnimator.setDuration(300);
btnAnimator.setInterpolator(new LinearInterpolator());
ValueAnimator rightAnimator = ValueAnimator.ofInt(ivRight, screenWidth);
rightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int right = ((int) animation.getAnimatedValue());
imageView.setRight(right);
}
});
rightAnimator.setDuration(400);
rightAnimator.setInterpolator(new LinearInterpolator());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tvAnimator).with(btnAnimator);
animatorSet.play(tvAnimator).before(rightAnimator);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
Toast.makeText(activity,"animation end" ,Toast.LENGTH_SHORT).show();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
animatorSet.start();
啰嗦!而且乍一看不知道在做啥,只能一行一行的细看,待看完整段代码后,才能在脑海中构建出整个需求的样子。
但逐行看也很费经,不信就试着从第一行开始读:
创建一个横向缩放属性
创建一个纵向缩放属性
创建一个动画,这个动画施加在 textView 上,并且包含缩放和透明度属性
动画时长300毫秒
动画使用线性插值器
原生 API 将“缩放 textView ”这短短的一句话拆分成一个个零散的逻辑单元,并以一种不符合自然语言的顺序排列,所以不得不读完所有单元,才能拼凑出整个语义。
如果有一种更符合自然语言的 API,就能更省力地构建动画,更快速地理解代码。
用 Kotlin 预定义扩展函数简化代码
AnimatorSet().apply {
ObjectAnimator.ofPropertyValuesHolder(
textView,
PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f),
PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f)
).apply {
duration = 300L
interpolator = LinearInterpolator()
}.let {
play(it).with(
ObjectAnimator.ofPropertyValuesHolder(
button,
PropertyValuesHolder.ofFloat("translationX", 0f, 100f)
).apply {
duration = 300L
interpolator = LinearInterpolator()
}
)
play(it).before(
ValueAnimator.ofInt(ivRight,screenWidth).apply {
addUpdateListener { animation -> imageView.right= animation.animatedValue as Int }
duration = 400L
interpolator = LinearInterpolator()
}
)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
start()
}
使用apply()
和let()
避免了重复对象名,缩减了代码量。更重要的是 Kotlin 的代码有一种结构,这种结构让代码更符合自然语言。试着读一下:
构建动画集,它包含{
动画1
将动画1和动画2一起播放
将动画3在动画1之后播放
。。。
}
虽然在语义上已经比较清晰,但结构还是显得啰嗦,此起彼伏的缩进看着有点乱。
用 DSL 进一步简化代码
如果使用自定义的 DSL,就可以做的更好!
直接上代码:
animSet {
objectAnim {
target = textView
scaleX = floatArrayOf(1.0f,1.3f)
scaleY = scaleX
duration = 300L
interpolator = LinearInterpolator()
} with objectAnim {
target = button
translationX = floatArrayOf(0f,100f)
duration = 300
interpolator = LinearInterpolator()
} before anim {
values = intArrayOf(ivRight,screenWidth)
action = { value -> imageView.right = value as Int }
duration = 400
interpolator = LinearInterpolator()
}
onEnd = Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
start()
}
一目了然的语义和清晰的结构,就好像是一篇英语文章。
这里运用了多个 Kotlin 语言特性,包括扩展函数、带接收者的 lambda、顶层函数、抽象属性、属性访问器、中缀表示法、函数类型变量、apply()、also()、let()。
逐个讲解 Kotlin 语法知识点后,再分析整套 DSL 的实现方案。
带接收者的 lambda
代码中animSet()
、objectAnim()
、anim()
都是带有一个参数的函数,这个参数是带接受者的 lambda
。animSet()
代码如下:
fun animSet(creation: AnimSet.() -> Unit) = AnimSet().apply { creation() }.also { it.build() }
它是一个顶层函数,定义在类体外,即它不隶属于任何类。这样定义的目的是可以在任何地方调用animSet()
来构造动画集。
它的参数类型是一个带接收者的 lambda AnimSet.() -> Unit
,接收者是AnimSet
类,它表示动画集(类似AnimatorSet
)。这样定义的好处是,可以在传入animSet()
的 lambda 中访问AnimSet
中的非私有成员,若把构建单个动画的方法objectAnim()
和anim()
定义在AnimSet()
中,就可以像写 HTML 一样使用结构化的语法构建动画。所以参数creation
描述的是在动画集中构建动画的过程。
animSet()
在函数体中,创建了一个动画集AnimSet
实例,并将构建子动画的方法应用在此实例上。
关于带接收者的lambda
和apply()
、also()
、let()
更详细的讲解可以点击这里。
构建动画的方法定义如下:
class AnimSet {
//'构建ValueAnim'
fun anim(animCreation: ValueAnim.() -> Unit): Anim = ValueAnim().apply(animCreation).also { anims.add(it) }
//'构建ObjectAnim'
fun objectAnim(animCreation: ObjectAnim.() -> Unit): Anim = ObjectAnim().apply(animCreation).also { it.setPropertyValueHolder() }.also { anims.add(it) }
}
这两个函数和构建动画集的函数非常相似,都使用了带接收者的lambda
作为参数,它定义了如何构建动画。ValueAnim
和ObjectAnim
分别对应于原生的ValueAnimator
和ObjectAnimator
。它们有一个共同的基类Anim
对应于原生的Animator
:
abstract class Anim {
//'原生动画实例'
abstract var animator: ValueAnimator
//'动画时长'
var duration
get() = 300L
set(value) {
animator.duration = value
}
//'插值器'
var interpolator
get() = LinearInterpolator() as Interpolator
set(value) {
animator.interpolator = value
}
//'动画与动画之间的连机器'
var builder:AnimatorSet.Builder? = null
//'反转动画'
abstract fun reverseValues()
}
抽象属性
动画基类Anim
是抽象类,因为animator
属性和reverseValues()
方法是抽象的。
animator
属性对于ValueAnim
来说是ValueAnimator
实例,对于ObjectAnim
来说是ObjectAnimator
实例:
class ObjectAnim : Anim() {
override var animator: ValueAnimator = ObjectAnimator()
}
class ValueAnim : Anim() {
override var animator: ValueAnimator = ValueAnimator()
}
关于抽象属性更详细的介绍可以点击这里
反转动画的算法对于ValueAnim
和ObjectAnim
有所不同,将反转算法作为抽象函数放在基类的好处时,在动画集AnimSet
中可以无需关心算法细节而是直接调用reverseValues()
实现反转动画:
class AnimSet {
//'动画集中包含的所有子动画'
private val anims by lazy { mutableListOf<Anim>() }
fun reverse() {
if (animatorSet.isRunning) return
//'遍历所有动画并让其反转'
anims.takeIf { !isReverse }?.forEach { anim -> anim.reverseValues() }
animatorSet.start()
isReverse = true
}
}
反转动画的算法会在下面分析,先来看下一个用到的 Kotlin 特性。
属性访问器
var duration
get() = 300L
set(value) {
animator.duration = value
}
在类属性的下面实现set()
和get()
方法,这样的语法叫属性访问器。当定义了访问器的属性被赋值时,set()
函数会执行,属性被读取时,get()
函数会执行,所以访问器定义了属性值的读写算法。
访问器在这里的好处是提供了默认值并隐藏了赋值细节,如果在构建动画时没有提供 duration ,则默认为300ms,为Anim
实例设置 duration 时,其实就是调用了原生的ValueAnimator.setDuration()
方法,属性访问器隐藏了这一细节,使得可以使用如下这样简洁的语法构建动画:
anim{
values = intArrayOf(ivRight,screenWidth)
action = { value -> imageView.right = value as Int }
duration = 400 //'为动画设置时长'
interpolator = LinearInterpolator()
}
函数类型
构建单个动画进行了4个属性赋值操作。其中action
属性表示“如何将动画值的序列应用到 View 上”:
class ValueAnim : Anim() {
override var animator: ValueAnimator = ValueAnimator()
var action: ((Any) -> Unit)? = null
set(value) {
field = value
animator.addUpdateListener { valueAnimator ->
valueAnimator.animatedValue.let { value?.invoke(it) }
}
}
}
Kotlin 中可以将函数保存在一个变量中,这种变量的类型叫做函数类型
,action
的类型就是函数类型
,用((Any) -> Unit)?
描述,意思是这个函数接收一个Any
类型的参数但什么也不返回。
这个属性也用到了访问器,当action
被赋值时就会为原生动画设置AnimatorUpdateListener
,并将属性值变化的序列作为参数传递给存放在action
中的 lambda,这样在构建动画时,就可以用一个简单的 lambda 定义做什么样的动画,比如下面就是在做向右平移动画:
anim{
values = floatArrayOf(0f,100f)
action = { value -> imageView.translationX = value as Float }
duration = 400
interpolator = LinearInterpolator()
}
其中的values
属性表示动画值序列:
class ValueAnim : Anim() {
var values: Any? = null
set(value) {
field = value
value?.let {
//'构建ValueAnimator对象'
when (it) {
is FloatArray -> animator.setFloatValues(*it)
is IntArray -> animator.setIntValues(*it)
else -> throw IllegalArgumentException("unsupported value type")
}
}
}
}
values
属性也使用了访问器,将根据类型调用ValueAnimator.setXXXValue()
细节隐藏。
中缀表示法
Kotlin 中,当函数调用只有一个参数时,可以省略包括参数的()
,以让代码更简洁,更符合自然语言,这种表示法叫中缀表示法。上述代码中用于连接多个动画的before()
函数就使用了中缀表示法:
infix fun Anim.before(anim: Anim): Anim {
animatorSet.play(animator).before(anim.animator).let { this.builder = it }
return anim
}
中缀表示的方法必须以关键词infix
开头,且函数只能有一个参数。同时这也是一个Anim
类的扩展函数。这个函数的调用者、参数、返回值都是一个Anim
实例。所以可以像a1 with a2 with a3
这样将多个Anim
连接起来。(连接动画的原理会在下面分析。)
实现方案
将从“如何构建Object动画”、“如何反转动画”、“如何连接动画”这三个方面来分析整套 DSL 的实现方法,关于 DSL 更详细的解释可以点击这里。
构建ObjectAnim
整套 DSL 并不是实现一个全新的动画框架。而是将原生动画提供的接口通过 DSL 封装成结构化的 API 以减少代码量并增加可读性。
ObjectAnim
中定义了属性用于存放动画值序列:
class ObjectAnim : Anim() {
//'构建空ObjectAnimator对象'
override var animator: ValueAnimator = ObjectAnimator()
//'各个属性值序列'
var translationX: FloatArray? = null
var translationY: FloatArray? = null
var scaleX: FloatArray? = null
var scaleY: FloatArray? = null
var alpha: FloatArray? = null
//'用数组存放非空的属性值序列'
private val valuesHolder = mutableListOf<PropertyValuesHolder>()
当调用如下代码时,属性被赋值:
objectAnim {
target = textView
scaleX = floatArrayOf(1.0f,1.3f)
scaleY = scaleX
duration = 300L
interpolator = LinearInterpolator()
}
因为并不知道,每个动画会为哪些属性赋值,所以不能调用ObjectAnimator.ofPropertyValuesHolder(textView, scaleX, scaleY);
来构建ObjectAnimator
对象。而只能用一个数组存放所有被赋值的属性,并且通过遍历数组调用ObjectAnimator.setValues()
异步构建ObjectAnimator
对象:
class AnimSet {
fun objectAnim(action: ObjectAnim.() -> Unit): Anim = ObjectAnim().apply(action).also { it.setPropertyValueHolder() }.also { anims.add(it) }
}
class ObjectAnim : Anim() {
fun setPropertyValueHolder() {
//'遍历所有属性序列,如果非空则构建PropertyValuesHolder并将其加入到集合中'
translationX?.let { PropertyValuesHolder.ofFloat(TRANSLATION_X, *it) }?.let { valuesHolder.add(it) }
translationY?.let { PropertyValuesHolder.ofFloat(TRANSLATION_Y, *it) }?.let { valuesHolder.add(it) }
scaleX?.let { PropertyValuesHolder.ofFloat(SCALE_X, *it) }?.let { valuesHolder.add(it) }
scaleY?.let { PropertyValuesHolder.ofFloat(SCALE_Y, *it) }?.let { valuesHolder.add(it) }
alpha?.let { PropertyValuesHolder.ofFloat(ALPHA, *it) }?.let { valuesHolder.add(it) }
animator.setValues(*valuesHolder.toTypedArray())
}
}
反转动画
反转动画的思路是:“将动画值序列倒序并重新播放动画”。动画基类AnimSet
中定义了反转算法的抽象方法:
abstract class Anim {
abstract fun reverseValues()
}
ValueAnimator
重写如下:
class ValueAnim : Anim() {
override var animator: ValueAnimator = ValueAnimator()
//'属性值序列,它是ValueAnim必须的属性'
var values: Any? = null
set(value) {
field = value
value?.let {
//'根据类型将属性值序列设置给ValueAnimator'
when (it) {
is FloatArray -> animator.setFloatValues(*it)
is IntArray -> animator.setIntValues(*it)
else -> throw IllegalArgumentException(’unsupported value type’)
}
}
}
override fun reverseValues() {
values?.let {
//'将属性值序列原地翻转并重新应用到ValueAnimator上'
when (it) {
is FloatArray -> {
it.reverse()
animator.setFloatValues(*it)
}
is IntArray -> {
it.reverse()
animator.setIntValues(*it)
}
else -> throw IllegalArgumentException("unsupported type of value")
}
}
}
}
AnimSet
提供反转动画对的外接口:
class AnimSet {
//'动画集所有子动画'
private val anims by lazy { mutableListOf<Anim>() }
//'反转动画中所有子动画'
fun reverse() {
if (animatorSet.isRunning) return
//'逐个调用Anim.reverseValues()'
anims.takeIf { !isReverse }?.forEach { anim -> anim.reverseValues() }
animatorSet.start()
isReverse = true
}
}
ObjectAnim
的反转算法略有不同:
class ObjectAnim : Anim() {
//'属性序列'
var translationX: FloatArray? = null
var translationY: FloatArray? = null
var scaleX: FloatArray? = null
var scaleY: FloatArray? = null
var alpha: FloatArray? = null
//'属性序列集合'
private val valuesHolder = mutableListOf<PropertyValuesHolder>()
//'遍历属性序列集合并翻转对应属性序列'
override fun reverseValues() {
valuesHolder.forEach { valuesHolder ->
when (valuesHolder.propertyName) {
TRANSLATION_X -> translationX?.let {
it.reverse()
valuesHolder.setFloatValues(*it)
}
TRANSLATION_Y -> translationY?.let {
it.reverse()
valuesHolder.setFloatValues(*it)
}
SCALE_X -> scaleX?.let {
it.reverse()
valuesHolder.setFloatValues(*it)
}
SCALE_Y -> scaleY?.let {
it.reverse()
valuesHolder.setFloatValues(*it)
}
ALPHA -> alpha?.let {
it.reverse()
valuesHolder.setFloatValues(*it)
}
}
}
}
}
连接动画
DSL 中的连接方案抛弃了AnimatorSet.playTogether()
和playSequentially()
,而是采用更加灵活的AnimtorSet.Builder
方式。
被加入到AnimatorSet
的Animator
会被保存在Node
这个结构中:
public final class AnimatorSet extends Animator {
private static class Node implements Cloneable {
Animator mAnimation;
//孩子列表
ArrayList<Node> mChildNodes = null;
//兄弟列表
ArrayList<Node> mSiblings;
//父亲列表
ArrayList<Node> mParents;
}
}
Animator
之间的播放顺序关系通过三个列表维护。兄弟列表中的动画会和自己同时播放,孩子列表会晚于自己播放,父亲列表会早于自己播放。
为了向这三个列表填值,系统定义了Builder
类:
public final class AnimatorSet extends Animator {
public class Builder {
private Node mCurrentNode;
//'为当前动画构建新结点'
Builder(Animator anim) {
mDependencyDirty = true;
mCurrentNode = getNodeForAnimation(anim);
}
//'向当前动画的兄弟列表中添加动画'
public Builder with(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addSibling(node);
return this;
}
//'向当前动画的孩子列表中添加动画'
public Builder before(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
}
}
//'只能通过这个方法构建Builder'
public Builder play(Animator anim) {
if (anim != null) {
return new Builder(anim);
}
return null;
}
}
同时播放a1,a2,a3动画,只需要这样调用 java API:
AnimatorSet set = new AnimatorSet();
set.play(a1).with(a2).with(a3);
此时结点间只有一个层级,即a1在外层,a2和a3存放在a1的兄弟列表中。
将上述 java 代码转换成 Kotlin 的中缀表示法如下:
class AnimSet {
private val animatorSet = AnimatorSet()
infix fun Anim.with(anim: Anim): Anim {
//'当前动画没有Builder,则调用play()构建Builder,否则直接调用with()'
if (builder == null) builder = animatorSet.play(animator).with(anim.animator)
else builder?.with(anim.animator)
return anim
}
}
abstract class Anim {
//'动画对应的Builder'
var builder:AnimatorSet.Builder? = null
}
因为同时播放的动画只有一个层级,所以调用链中,只需要第一个动画调用一次play()
即可。为Anim
增加了builder
属性以判断当前动画是否调用过play()
来创建结点。
相比之下,顺序播放的代码层级就变多了,如果要先播放a1,再播放a2,最后播放a3,java api 如下:
AnimatorSet set = new AnimatorSet();
set.play(a1).before(a2);
set.play(a2).before(a3);
这个结构有点像树,后续结点是之前结点的孩子。对应的中缀表达式定义如下:
class AnimSet {
infix fun Anim.before(anim: Anim): Anim {
animatorSet.play(animator).before(anim.animator).let { this.builder = it }
return anim
}
}
每次都为当前动画调用play()
创建Builder
并将后续动画存入孩子列表。
网友评论