最近在看《新說唱》,突然就想到了這個帶韻腳的標題。希望你喜歡~。言歸正傳,Android構建動畫的程式碼語法囉嗦,可讀性差。若能構建一套可讀性更強的介面就能提高動畫的開發效率。本文嘗試用 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()
都是帶有一個引數的函式,這個引數是帶接受者的 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 更詳細的解釋可以點選這裡。
構建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
方式。
被加入到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
並將後續動畫存入孩子列表。
talk is cheap, show me the code
程式碼會持續更新,歡迎star,更歡迎提出問題。