最近在看《新說唱》,忽然就想到了這個帶韻腳的標題。但願你喜歡~。言歸正傳,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
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,就能夠作的更好!
直接上代碼:
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 的實現方案。
代碼中animSet()
、objectAnim()
、anim()
都是帶有一個參數的函數,這個參數是帶接受者的 lambda
。animSet()
代碼以下:
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
實例,並將構建子動畫的方法應用在此實例上。
關於帶接收者的lambda
和apply()
、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
做爲參數,它定義瞭如何構建動畫。ValueAnim
和ObjectAnim
分別對應於原生的ValueAnimator
和ObjectAnimator
。它們有一個共同的基類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()
}
複製代碼
關於抽象屬性更詳細的介紹能夠點擊這裏
反轉動畫的算法對於ValueAnim
和ObjectAnim
有所不一樣,將反轉算法做爲抽象函數放在基類的好處時,在動畫集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 更詳細的解釋能夠點擊這裏。
整套 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
方式。
被加入到AnimatorSet
的Animator
會被保存在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
並將後續動畫存入孩子列表。
代碼會持續更新,歡迎star,更歡迎提出問題。