一、动画开发中的痛点与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动画,缩到一半,带个弹性结束”。是不是很自然?

小技巧:扩展函数可以链式调用,是因为它返回了ViewPropertyAnimator本身。记得在函数最后return this。

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扩展 • 弹性结束:withBounceEnd() • 组合动画:animateTogether { } • 链式调用,代码像说话 AnimatorListener Lambda简化 • doOnEnd { } 只监听结束 • onEvent() 可选参数监听 • 告别空方法,代码更干净 动画链式调用 • AnimationQueue 队列管理 • then { } 顺序执行 • 先平移 → 再旋转 → 最后透明 属性动画DSL封装 • AnimDSL 配置动画参数 • animateTogether { } 多属性 • 像写配置文件一样写动画 Kotlin 核心思想:用更少的代码,写更清晰的动画

七、总结与避坑

这一章我们聊了四个方向:

  • ViewPropertyAnimator的扩展:给原生API加功能,让动画更“顺口”。
  • AnimatorListener的Lambda简化:用高阶函数干掉空方法。
  • 动画链式调用:让动画排队执行,代码像讲故事。
  • 属性动画的DSL封装:用DSL让动画配置像写配置文件。

最后说几个我踩过的坑:

避坑指南:

  • 我曾经在扩展函数里忘记return this,导致链式调用断掉。排查了半天才发现。记住,扩展函数如果要支持链式调用,一定要返回接收者对象。
  • 我曾经在AnimatorListener的Lambda里直接引用了Activity的Context,导致内存泄漏。Lambda会持有外部引用,记得用弱引用或者在onDestroy里移除监听。
  • 我曾经在DSL里用了大量嵌套Lambda,导致代码可读性反而下降。DSL虽好,但不要滥用。适度的抽象是优雅,过度的抽象是灾难。

好了,这一章就到这里。记住,工具是死的,思路是活的。Kotlin给了你扩展函数和高阶函数这两把利器,怎么用,看你的想象力了。


公众号:蓝海资料掘金营,微信deep3321