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基礎:用約定簡化相親
相關文章
相關標籤/搜索