一、动画开发中的痛点与Kotlin的解法
做Android动画开发,说实话,以前挺痛苦的。
我记得刚入行那会儿,写一个简单的平移动画,要new一个ObjectAnimator,设置duration,加listener,最后start()。代码又长又碎。更别提AnimatorListener那四个回调方法——你明明只想监听动画结束,却得把onAnimationStart、onAnimationRepeat、onAnimationCancel全给实现了。空方法写多了,自己都觉得尴尬。
后来Kotlin来了,扩展函数、高阶函数、Lambda这些特性,直接把动画开发体验拉高了一个档次。说白了,就是用更少的代码,做更多的事。而且代码读起来像在说话,而不是在念经。
这一章,我就带你看看,怎么用Kotlin的这些特性,把Android动画写得既优雅又高效。
二、ViewPropertyAnimator的扩展:让动画更“顺口”
ViewPropertyAnimator本身已经很好用了。.animate().translationX(100f).setDuration(300).start(),链式调用,一气呵成。但有个问题——它不支持同时设置多个属性时做一些自定义逻辑。比如,我想让View先平移再旋转,或者让动画带个弹性效果,原生API就有点力不从心了。
这时候,扩展函数就派上用场了。
2.1 给ViewPropertyAnimator加个“弹性结束”
我在项目中遇到过这样一个需求:列表项删除时,希望它先缩一下再弹回来,有点“舍不得走”的感觉。原生没有这个效果,怎么办?写个扩展函数。
fun ViewPropertyAnimator.withBounceEnd(duration: Long = 300) {
this.setDuration(duration)
.setInterpolator(BounceInterpolator())
}
然后调用的时候:
view.animate()
.scaleX(0.5f)
.scaleY(0.5f)
.withBounceEnd()
.start()
你看,代码读起来就是“让view动画,缩到一半,带个弹性结束”。是不是很自然?
2.2 组合动画的扩展封装
有时候我们需要同时做多个属性动画,比如平移+旋转+透明度。原生写法要分别创建三个Animator,然后塞进AnimatorSet。用扩展函数,可以这样:
fun View.animateTogether(
block: ViewPropertyAnimator.() -> Unit
) {
this.animate()
.apply(block)
.start()
}
调用:
view.animateTogether {
translationX(200f)
rotation(360f)
alpha(0.5f)
duration = 500
}
嗯,这里要注意,block是一个带接收者的Lambda,接收者就是ViewPropertyAnimator。这样写,内部可以直接调用translationX、rotation这些方法,不用每次都写view.animate()了。
三、AnimatorListener的Lambda简化:告别空方法
AnimatorListener有四个方法:onAnimationStart、onAnimationEnd、onAnimationRepeat、onAnimationCancel。大部分时候,我们只用其中一个。但Java时代,你必须实现全部。
Kotlin的Lambda和高阶函数,让这件事变得简单。
3.1 用扩展函数创建简化版Listener
fun Animator.doOnEnd(action: () -> Unit) {
this.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
action()
}
})
}
使用:
view.animate()
.translationX(200f)
.setDuration(300)
.doOnEnd {
// 动画结束后的逻辑
view.visibility = View.GONE
}
.start()
我曾经在一个列表动画中,需要每个item动画结束后才显示下一个。用doOnEnd配合递归调用,代码非常干净。要是用原来的方式,得写一堆匿名内部类,想想就头疼。
3.2 更通用的Lambda封装
如果你需要监听多个事件,可以这样:
fun Animator.onEvent(
onStart: (() -> Unit)? = null,
onEnd: (() -> Unit)? = null,
onRepeat: (() -> Unit)? = null,
onCancel: (() -> Unit)? = null
) {
this.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
onStart?.invoke()
}
override fun onAnimationEnd(animation: Animator) {
onEnd?.invoke()
}
override fun onAnimationRepeat(animation: Animator) {
onRepeat?.invoke()
}
override fun onAnimationCancel(animation: Animator) {
onCancel?.invoke()
}
})
}
调用时,只需要传你需要的方法:
animator.onEvent(
onStart = { Log.d("TAG", "动画开始") },
onEnd = { Log.d("TAG", "动画结束") }
)
说白了,就是用默认参数和Lambda,把四个方法压缩成一个。你想想看,是不是比原来清爽多了?
四、动画链式调用:让动画“排队”执行
多个动画按顺序执行,以前用AnimatorSet.playSequentially()。但如果你有5个、10个动画,代码会变得很臃肿。
用Kotlin的扩展函数和高阶函数,我们可以实现一个优雅的链式调用。
4.1 实现一个简单的动画队列
class AnimationQueue(private val view: View) {
private val animators = mutableListOf<Animator>()
fun then(block: ViewPropertyAnimator.() -> Unit): AnimationQueue {
val animator = view.animate().apply(block).setDuration(300).also {
it.start()
it.pause() // 先暂停,等前面的动画结束
}
animators.add(animator)
return this
}
fun start() {
if (animators.isEmpty()) return
var index = 0
fun playNext() {
if (index < animators.size) {
animators[index].apply {
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
index++
playNext()
}
})
resume() // 恢复执行
}
}
}
playNext()
}
}
fun View.animateQueue(block: AnimationQueue.() -> Unit) {
val queue = AnimationQueue(this)
queue.apply(block)
queue.start()
}
使用:
view.animateQueue {
then { translationX(100f) }
then { rotation(360f) }
then { alpha(0.5f) }
}
你看,代码读起来就是“先平移,再旋转,最后变透明”。是不是很像在描述一个故事?
五、属性动画的DSL封装:让动画配置像写配置文件
DSL(领域特定语言)是Kotlin的一大杀器。用DSL封装属性动画,可以让动画配置变得非常直观。
5.1 一个简单的动画DSL
class AnimDSL {
var duration: Long = 300
var interpolator: Interpolator = LinearInterpolator()
var repeatCount: Int = 0
var repeatMode: Int = ValueAnimator.RESTART
private val listeners = mutableListOf<Animator.AnimatorListener>()
fun onEnd(action: () -> Unit) {
listeners.add(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
action()
}
})
}
fun build(target: View, property: String, vararg values: Float): Animator {
val animator = ObjectAnimator.ofFloat(target, property, *values)
animator.duration = duration
animator.interpolator = interpolator
animator.repeatCount = repeatCount
animator.repeatMode = repeatMode
listeners.forEach { animator.addListener(it) }
return animator
}
}
fun View.animate(
property: String,
vararg values: Float,
config: AnimDSL.() -> Unit
): Animator {
val dsl = AnimDSL()
dsl.apply(config)
return dsl.build(this, property, *values)
}
使用:
view.animate("translationX", 0f, 200f) {
duration = 500
interpolator = OvershootInterpolator()
repeatCount = 1
repeatMode = ValueAnimator.REVERSE
onEnd {
Log.d("TAG", "动画结束")
}
}.start()
我个人习惯用这种方式来配置复杂的动画。尤其是当动画参数很多时,DSL的命名参数和代码块结构,让配置一目了然。你想想看,如果不用DSL,你得写多少行代码?
5.2 更复杂的DSL:支持多个属性同时配置
class MultiAnimDSL {
private val animConfigs = mutableListOf<Triple<String, FloatArray, AnimDSL.() -> Unit>>()
fun property(name: String, vararg values: Float, config: AnimDSL.() -> Unit = {}) {
animConfigs.add(Triple(name, values, config))
}
fun build(target: View): AnimatorSet {
val set = AnimatorSet()
val animators = animConfigs.map { (name, values, config) ->
target.animate(name, *values, config = config)
}
set.playTogether(animators)
return set
}
}
fun View.animateTogether(
block: MultiAnimDSL.() -> Unit
): AnimatorSet {
val dsl = MultiAnimDSL()
dsl.apply(block)
return dsl.build(this)
}
使用:
view.animateTogether {
property("translationX", 0f, 200f) {
duration = 400
interpolator = AccelerateDecelerateInterpolator()
}
property("rotation", 0f, 360f) {
duration = 600
interpolator = LinearInterpolator()
}
property("alpha", 1f, 0.5f) {
duration = 300
}
}.start()
你看,每个属性的动画配置都是独立的,互不干扰。代码结构清晰,维护起来也方便。
六、知识体系总览
下面这张图,帮你梳理这一章的核心内容:
七、总结与避坑
这一章我们聊了四个方向:
- ViewPropertyAnimator的扩展:给原生API加功能,让动画更“顺口”。
- AnimatorListener的Lambda简化:用高阶函数干掉空方法。
- 动画链式调用:让动画排队执行,代码像讲故事。
- 属性动画的DSL封装:用DSL让动画配置像写配置文件。
最后说几个我踩过的坑:
避坑指南:
- 我曾经在扩展函数里忘记return this,导致链式调用断掉。排查了半天才发现。记住,扩展函数如果要支持链式调用,一定要返回接收者对象。
- 我曾经在AnimatorListener的Lambda里直接引用了Activity的Context,导致内存泄漏。Lambda会持有外部引用,记得用弱引用或者在onDestroy里移除监听。
- 我曾经在DSL里用了大量嵌套Lambda,导致代码可读性反而下降。DSL虽好,但不要滥用。适度的抽象是优雅,过度的抽象是灾难。
好了,这一章就到这里。记住,工具是死的,思路是活的。Kotlin给了你扩展函数和高阶函数这两把利器,怎么用,看你的想象力了。
公众号:蓝海资料掘金营,微信deep3321