美文网首页Android开发经验谈KotlinAndroid开发
Kotlin进阶:动画代码太丑,忍不住用DSL重构,然后像说话一

Kotlin进阶:动画代码太丑,忍不住用DSL重构,然后像说话一

作者: 唐子玄 | 来源:发表于2019-07-15 14:03 被阅读11次

    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()都是带有一个参数的函数,这个参数是带接受者的 lambdaanimSet()代码如下:

    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实例,并将构建子动画的方法应用在此实例上。

    关于带接收者的lambdaapply()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作为参数,它定义了如何构建动画。ValueAnimObjectAnim分别对应于原生的ValueAnimatorObjectAnimator。它们有一个共同的基类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()
    }
    

    关于抽象属性更详细的介绍可以点击这里

    反转动画的算法对于ValueAnimObjectAnim有所不同,将反转算法作为抽象函数放在基类的好处时,在动画集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方式。

    被加入到AnimatorSetAnimator会被保存在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并将后续动画存入孩子列表。

    talk is cheap, show me the code

    推荐阅读

    1. Kotlin基础:白话文转文言文般的Kotlin常识

    2. Kotlin基础:望文生义的Kotlin集合操作

    3. Kotlin实战:用实战代码更深入地理解预定义扩展函数

    4. Kotlin实战:使用DSL构建结构化API去掉冗余的接口方法

    5. Kotlin基础:属性也可以是抽象的

    相关文章

      网友评论

        本文标题:Kotlin进阶:动画代码太丑,忍不住用DSL重构,然后像说话一

        本文链接:https://www.haomeiwen.com/subject/qalnkctx.html