Kotlin进阶:动画代码太丑,用DSL动画库拯救,像说话同样写代码哟!

最近在看《新说唱》,忽然就想到了这个带韵脚的标题。但愿你喜欢~。言归正传,Android构建动画的代码语法啰嗦,可读性差。若能构建一套可读性更强的接口就能提升动画的开发效率。本文尝试用 Kotlin 的 DSL 重写了整套构建动画的 API ,使得构建动画的代码量锐减,语义一目了然。另外,Android提供了反转动画的接口,但只有在 API level 26 以上才能使用,本文尝试突破这个限制。java

这是 Kotlin 系列的第六篇,文章列表详见末尾。node

感谢掘友“上课钟变成打卡钟_”在上一篇文章的留言,是你留言促成了这篇文章的诞生。git

原生动画代码

假设需求以下:“缩放 textView 的同时平移 button ,而后拉长 imageView,动画结束后 toast 提示”。用系统原生接口构建以下:github

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();
复制代码

啰嗦!并且乍一看不知道在作啥,只能一行一行的细看,待看完整段代码后,才能在脑海中构建出整个需求的样子。算法

但逐行看也很费劲,不信就试着从第一行开始读:api

建立一个横向缩放属性
建立一个纵向缩放属性
建立一个动画,这个动画施加在 textView 上,而且包含缩放和透明度属性
动画时长300毫秒
动画使用线性插值器
复制代码

原生 API 将“缩放 textView ”这短短的一句话拆分红一个个零散的逻辑单元,并以一种不符合天然语言的顺序排列,因此不得不读完全部单元,才能拼凑出整个语义。数组

若是有一种更符合天然语言的 API,就能更省力地构建动画,更快速地理解代码。bash

用 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 的代码有一种结构,这种结构让代码更符合天然语言。试着读一下:app

构建动画集,它包含{
    动画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

代码会持续更新,欢迎star,更欢迎提出问题。

推荐阅读

  1. Kotlin基础:白话文转文言文般的Kotlin常识
  2. Kotlin基础:望文生义的Kotlin集合操做
  3. Kotlin实战:用实战代码更深刻地理解预约义扩展函数
  4. Kotlin实战:使用DSL构建结构化API去掉冗余的接口方法
  5. Kotlin基础:属性也能够是抽象的
  6. Kotlin进阶:动画代码太丑,用DSL动画库拯救,像说话同样写代码哟!
  7. Kotlin基础:用约定简化相亲
相关文章
相关标签/搜索