Android Compose 屬性動畫使用探索詳解
前言
Jetpack Compose
(簡稱 Compose )是 Google 官方推出的基于 Kotlin 語言的 Android 新一代 UI 開發(fā)框架,其采用聲明式的 UI 編程特性使得 Android 應(yīng)用界面的編寫和維護變得更加簡單。
本專欄將詳細介紹在使用 Compose 進行 UI 開發(fā)中如何實現(xiàn)炫酷的動畫效果。動畫效果在 App 使用中至關(guān)重要,它使得 App 的交互更加自然流暢,用戶使用體驗更加良好。
在傳統(tǒng)的 Android 開發(fā)中有古老的 View 動畫和目前流行的屬性動畫,如今 View 動畫幾乎已被廣大開發(fā)者所拋棄,屬性動畫因其可以作用于任何對象的靈活和強大特性而被開發(fā)者所擁抱。既然屬性動畫這么強大,那么它是否能用在 Compose 開發(fā)中呢?如果能那跟傳統(tǒng) UI 開發(fā)中使用又有什么區(qū)別呢?本篇就帶領(lǐng)你來探索一下在 Compose 中屬性動畫的使用。
使用探索
在傳統(tǒng) Android 開發(fā)中,屬性動畫使用得最多的是 ObjectAnimator
和 ValueAnimator
,接下來就探索一下在 Compose 中如何使用它們來實現(xiàn)動畫效果。
ObjectAnimator 使用探索
首先看一下在傳統(tǒng) Android 開發(fā)中如何使用屬性動畫,比如使用屬性動畫實現(xiàn)豎直方向向下移動的動畫:
val animator = ObjectAnimator.ofFloat(view, "translationY", 10f, 100f) animator.start()
通過 ObjectAnimator
作用于 View 的 translationY
屬性,不斷改變 translationY 的值從而實現(xiàn)動畫效果,一個很簡單的屬性動畫,這里就不貼運行效果了。
那在 Compose 中能否使用 ObjectAnimator 呢?
下面使用 Compose 在界面上顯示一個 100dp*100dp 的藍色正方形方塊,代碼如下:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Box(Modifier.padding(start = 10.dp, top = 10.dp) .size(100.dp) .background(Color.Blue) ) } } }
運行效果如下:
現(xiàn)在要同樣實現(xiàn)一個豎直方向移動的動畫效果,讓方塊從上往下移動。在上面的屬性動畫實現(xiàn)中 ObjectAnimator
是作用于 View 組件上的,按照這個思路在這里 ObjectAnimator 就應(yīng)該作用于 Box 上,但實際上我們這里壓根拿不到 Box 的實例,因為這里的 Box 實際是一個函數(shù)且沒有返回值,看一下 Box 的源碼:
@Composable fun Box(modifier: Modifier) { Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier) }
既然作用于 Box 上不行,那能不能作用于 State 上呢,Compose 是數(shù)據(jù)驅(qū)動 UI 刷新,通過數(shù)據(jù)狀態(tài)改變重組 UI 實現(xiàn)界面的刷新,把上面的 top 提取為一個 State 再通過 ObjectAnimator 去改變是否可行呢?改造代碼實驗一下:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val topPadding:MutableState<Int> = mutableStateOf(10) val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100) animator.duration = 1000 setContent { Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp) .size(100.dp) .background(Color.Blue) // 添加點擊事件 .clickable { // 啟動動畫 animator.start() } ) } } }
改造如下:
- 將之前 top 的固定值提取成了一個 State 變量 topPadding,當 topPadding 的值發(fā)生改變時會重組界面從而讓界面刷新
- 聲明了 ObjectAnimator 的 animator 變量,作用于 topPadding 的 value 屬性上,并設(shè)置動畫值從 10 到 100,動畫時長 1000ms
- 給 Box 添加點擊監(jiān)聽事件啟動動畫
實際上寫完這段代碼,編輯器就已經(jīng)有報錯提示了,提示如下:
說沒有找到帶 Int 參數(shù)的 setValue
方法,那來看看 MutableState
是否有 setValue 方法:
interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit }
可以發(fā)現(xiàn) MutableState 中是有一個 var 修飾的 value 變量的,說明是有 setValue 方法的,但是錯誤提示是找不到帶 Int 參數(shù)的 setValue 方法,實際上 MutableState 的 setValue 的定義應(yīng)該是這樣的:
fun setValue(value:T){ this.value = value }
這里參數(shù)類型是泛型 T
,而 ObjectAnimator 找的是明確的 Int 類型參數(shù)的方法,所以找不到。那怎么辦呢?是不是就意味著在 Compose 中無法使用 ObjectAnimator 了呢?
直接使用確實是不行,那我們能不能對其進行封裝,不是找不到對應(yīng)的 setValue 方法嘛,那我封裝一下提供一個 setValue 方法不就行了。定義一個 IntState
類,再提供一個 mutableIntStateOf
方法:
class IntState(private val state: MutableState<Int>){ var value : Int = state.value get() = state.value set(value) { field = value state.value = value } } fun mutableIntStateOf(value: Int, policy: SnapshotMutationPolicy<Int> = structuralEqualityPolicy()) : IntState{ val state = mutableStateOf(value, policy) return IntState(state) }
IntState
構(gòu)造方法傳入一個 MutableState 類型的 state 參數(shù),然后提供一個 value 變量,get 方法返回 state.value ,set 方法將傳入值設(shè)置給 state.value,這樣 IntState
就有了一個明確的 setValue(value:Int) 的方法。
為了便于使用,封裝一個 mutableIntStateOf
方法,實現(xiàn)里先采用 Compose 提供的 mutableStateOf 方法獲取一個 MutableState ,然后用其構(gòu)建一個 IntState 進行返回。
再改造一下上面動畫實現(xiàn)代碼將 mutableStateOf
替換成 mutableIntStateOf
:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 替換為 mutableIntStateOf val topPadding = mutableIntStateOf(10) // 創(chuàng)建 ObjectAnimator 目標為 topPadding,作用屬性為 value,值從 10 變化到 100 val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100) // 設(shè)置動畫時長 1s animator.duration = 1000 setContent { Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp) .size(100.dp) .background(Color.Blue) // 添加點擊事件 .clickable { // 啟動動畫 animator.start() } ) } } }
現(xiàn)在不報錯了,運行一下看看是否有動畫效果:
效果符合預(yù)期,說明這種辦法是可行,也說明 ObjectAnimator 在 Compose 中也是可以使用的,只是不能像傳統(tǒng) Android 開發(fā)那樣直接作用于 View 組件上,而是需要進行二次封裝后使用。
ValueAnimator 使用探索
ObjectAnimator 使用探索完了,那么 ValueAnimator
能否使用呢?Compose 以聲明式的方式通過數(shù)據(jù)驅(qū)動界面刷新,而ValueAnimator
主要用于數(shù)據(jù)的改變,好像很契合的樣子,使用 ValueAnimator 不斷改變 State 的值理論上就可以實現(xiàn)動畫效果。還是上面的例子,改造成使用 ValueAnimator
來實現(xiàn):
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 使用 mutableStateOf 創(chuàng)建 topPadding 的 State var topPadding by mutableStateOf(10) // 創(chuàng)建 ValueAnimator 從 10 變化到 100 val animator = ValueAnimator.ofInt(10, 100) // 動畫時長 1s animator.duration = 1000 // 設(shè)置監(jiān)聽,當動畫改變時動態(tài)修改 topPadding 的值 animator.addUpdateListener { topPadding = it.animatedValue as Int } setContent { Box(Modifier.padding(start = 10.dp, top = topPadding.dp) .size(100.dp) .background(Color.Blue) .clickable { animator.start() } ) } } }
是否有效果呢?運行一下看看效果:
跟上面使用 ObjectAnimator 實現(xiàn)的效果一致,說明 ValueAnimator 在 Compose 中實現(xiàn)動畫是可行的,只是需要手動去監(jiān)聽 ValueAnimator 值的變化然后去動態(tài)更新 State 的值,稍微麻煩了一點,實際上我們也可以對其進行封裝簡化其使用。
通過上面的代碼發(fā)現(xiàn),如果要在 Compose 中使用 ValueAnimator 來實現(xiàn)動畫,對動畫數(shù)值的改變進行監(jiān)聽并動態(tài)更新 State 的值是必不可少的一步,那么我們就可以將其提取進行封裝。
/** * @param state 動畫作用的目標 State * @param values 動畫的變化值,可變參數(shù) */ fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{ // 創(chuàng)建 ValueAnimator ,參數(shù)為傳入的 values val animator = ValueAnimator.ofInt(*values) // 添加監(jiān)聽 animator.addUpdateListener { // 更新 state 的 value 值 state.value = it.animatedValue as Int } return animator }
然后將上面的創(chuàng)建動畫替換成使用 animatorOfInt 創(chuàng)建:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val topPadding = mutableStateOf(10) // 使用封裝的 animatorOfInt 方法創(chuàng)建動畫 val animator = animatorOfInt(topPadding, 10, 100) animator.duration = 1000 setContent { Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp) .size(100.dp) .background(Color.Blue) .clickable { animator.start() } ) } } }
使用是不是要簡單很多,不需要手動去處理動畫值變化的監(jiān)聽了,有點使用 ObjectAnimator 的感覺,只是不需要指定目標屬性。運行效果跟上面一致就不貼圖了。
Compose 函數(shù)中使用屬性動畫
前面在 Compose 中使用的動畫都是創(chuàng)建在 Compose 函數(shù)外面的,如果我們想把這個組件封裝成一個獨立的 Compose 組件就需要將動畫的創(chuàng)建放到 Compose 函數(shù)里面,比如將上面的效果封裝成一個 AnimationBox
組件:
@Composable fun AnimationBox(){ val topPadding = mutableStateOf(10) val animator = animatorOfInt(topPadding, 10, 100) animator.duration = 1000 Box(modifier = Modifier.padding(start = 10.dp, top = topPadding.value.dp) .size(100.dp) .background(Color.Blue) .clickable { animator.start() }) }
首先 mutableStateOf 會報錯:
意思是在組合過程中創(chuàng)建 state 需要使用 remember
,原因是當 state 里的值發(fā)生變化時 Compose 會進行重組導(dǎo)致函數(shù)重新執(zhí)行,如果 mutableStateOf 不加 remember
則會每次重組都重新創(chuàng)建 state,導(dǎo)致 UI 上使用的值每次都是初始值而得不到刷新。
既然報錯那就給他加上 remember
:
@Composable fun AnimationBox(){ val topPadding = remember { mutableStateOf(10) } ... }
然后在使用的地方直接使用 AnimationBox() 即可:
setContent { AnimationBox() }
運行后發(fā)現(xiàn)效果跟之前一樣,那是不是就可以了呢?
實際上上面的代碼是還存在問題的,前面說在 Compose 重組時會重新執(zhí)行 Compose 組件的代碼,也就是在界面刷新時會多次重復(fù)創(chuàng)建動畫對象,我們在 animatorOfInt 函數(shù)里添加一個日志再看看運行時的日志輸出:
fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{ println("-------call animatorOfInt--------") ... }
輸出結(jié)果:
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
日志確實輸出了多次,意味著動畫確實創(chuàng)建了多次,那怎么解決呢?
前面說了 remember
可以解決重組時重復(fù)創(chuàng)建的問題,所以只需在創(chuàng)建動畫上套上 remember
即可,如下:
val animator = remember { animatorOfInt(topPadding, 10, 100) }
修改后再看日志,發(fā)現(xiàn)就只在第一次進行了創(chuàng)建,動畫執(zhí)行過程中并沒有再次創(chuàng)建。
為了方便使用,可以再封裝一個 rememberAnimatorOfInt
方法:
@Composable fun rememberAnimatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{ return remember { animatorOfInt(state, *values) } }
在 animatorOfInt 上套了一個 remember,這樣使用時就可以直接使用 rememberAnimatorOfInt 方法:
val animator = rememberAnimatorOfInt(topPadding, 10, 100)
remember 是 Compose 提供的在 Compose 函數(shù)中緩存狀態(tài)的方法,解決在 Compose 重組時重復(fù)創(chuàng)建的問題,關(guān)于 remember 更多使用大家可以自行查詢相關(guān)資料,本專欄主要講解動畫的使用就不過多贅述。
實戰(zhàn)
前面介紹了屬性動畫在 Compose 中的運用,那在實際開發(fā)中到底好不好用呢?接下來我們通過一個實例來看看。
先看一下最終實現(xiàn)的效果:
一個上傳按鈕的動畫效果,動畫主要分為三階段:
- 上傳開始
- 按鈕從圓角矩形變成圓形
- 按鈕顏色從藍色變成中間白色,邊框灰色
- 文字逐漸消失
- 上傳進度
- 邊框根據(jù)進度變?yōu)樗{色
- 上傳完成
- 按鈕從圓形變成圓角矩形
- 按鈕顏色變成紅色
- 文字逐漸顯示,且文字變?yōu)?“Success”
上傳開始動畫
先把按鈕的初始狀態(tài)使用 Compose 實現(xiàn):
@Composable fun UploadButton() { Box( modifier = Modifier .padding(start = 10.dp, top = 10.dp) .width(180.dp), contentAlignment = Alignment.Center ) { Box( modifier = Modifier .clip(RoundedCornerShape(24.dp)) .background(Color.Blue) .size(180.dp, 48.dp), contentAlignment = Alignment.Center, ) { Text("Upload", color = Color.White) } } }
運行效果如下:
下面就為這按鈕添加動畫,前面講了動畫主要作用于 State 上,所以需要先將使用到的數(shù)據(jù)提取成對應(yīng)的狀態(tài):
@Composable fun UploadButton() { val originWidth = 180.dp val circleSize = 48.dp var text by remember { mutableStateOf("Upload") } val textAlpha = remember { mutableStateOf(1.0f) } val backgroundColor = remember { mutableStateOf(Color.Blue) } val boxWidth = remember { mutableStateOf(originWidth) } Box( modifier = Modifier .padding(start = 10.dp, top = 10.dp) .width(originWidth), contentAlignment = Alignment.Center ) { Box( modifier = Modifier .clip(RoundedCornerShape(height/2)) .background(backgroundColor.value) .size(boxWidth.value, height), contentAlignment = Alignment.Center, ) { Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value)) } } }
創(chuàng)建開始上傳的動畫:
@Composable fun UploadButton() { ... val uploadStartAnimator = remember { // 創(chuàng)建 AnimatorSet val animatorSet = AnimatorSet() // 按鈕寬度變化動畫 val widthAnimator = animatorOfDp(boxWidth, arrayOf(originWidth, circleSize)) // 文字消失動畫 val textAnimator = animatorOfFloat(textAlpha, 1f, 0.0f) // 按鈕顏色動畫 val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Gray)) // 動畫添加到 AnimatorSet animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator) animatorSet } Box(...) { Box( modifier = Modifier ... .clickable { // 點擊執(zhí)行動畫 uploadStartAnimator.start() }, ... ) } }
分別創(chuàng)建按鈕寬度、按鈕顏色和文字 alpha 值變化的動畫,因需同時執(zhí)行多個動畫,這里使用 AnimatorSet 進行同時執(zhí)行,然后在按鈕上添加點擊事件進行動畫執(zhí)行。
上面的 animatorOfDp
、animatorOfFloat
、animatorOfColor
都是自定義封裝的函數(shù),封裝方法與上面介紹的 animatorOfInt
基本相同,源碼可通過文章最后附的源碼地址進行查看。
運行效果如下:
好像還差點,中間應(yīng)該是白色的,在 Box 下再添加一個白色圓形的 Box,默認 alpha 是 0,上傳開始時 alpha 從 0 變成 1 :
@Composable fun UploadButton() { ... val progressAlpha = remember { mutableStateOf(0.0f) } val uploadStartAnimator = remember { ... // 中間白色透明度變化動畫 val centerAlphaAnimator = animatorOfFloat(progressAlpha, 0.0f, 1f) animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator, centerAlphaAnimator) animatorSet } Box(...) { Box(...) { // 白色圓形 Box( modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp)) .alpha(progressAlpha.value).background(Color.White) ) Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value)) } } }
運行效果如下:
上傳進度動畫
這里通過自定義 clip 的一個弧形的 shape 來實現(xiàn)進度,自定義代碼如下:
class ArcShape(private val progress: Int) : Shape { override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { val path = Path().apply { moveTo(size.width / 2f, size.height / 2f) arcTo(Rect(0f, 0f, size.width, size.height), -90f, progress / 100f * 360f, false) close() } return Outline.Generic(path) } }
傳入一個進度值(0-100),然后根據(jù)進度值算出一個繪制的弧度,使用這個自定義的 ArcShape 代碼如下:
Box(Modifier.size(48.dp).clip(ArcShape(30)).background(Color.Blue))
效果:
所以只需動態(tài)改變 ArcShape 的 progress 參數(shù)的值就能實現(xiàn)上傳進度效果,修改代碼如下:
@Composable fun PreviewUploadButton() { ... val progress = remember { mutableStateOf(0) } //上傳進度動畫 val progressAnimator = remember { val animator = animatorOfInt(progress, 0, 100) animator.duration = 1000 animator } val uploadStartAnimator = remember { ... // 添加動畫監(jiān)聽,完成后執(zhí)行進度動畫 animatorSet.addListener(onEnd = { progressAnimator.start() }) animatorSet } Box(...) { Box(...) { // 進度 Box Box( modifier = Modifier.size(height).clip(ArcShape(progress.value)) .alpha(progressAlpha.value).background(Color.Blue) ) ... } } }
運行效果:
上傳完成動畫
最后是上傳完成動畫就很簡單了,基本就是開始動畫的反向,只是按鈕顏色從藍色變成了紅色,動畫在上傳進度動畫完成時執(zhí)行:
@Composable fun PreviewUploadButton() { ... val endAnimatorSet = remember { val animatorSet = AnimatorSet() val widthAnimator = animatorOfDp(boxWidth, arrayOf(circleSize, originWidth)) val centerAnimator = animatorOfFloat(progressAlpha, 1f, 0f) val textAnimator = animatorOfFloat(textAlpha, 0f, 1f) val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Red)) animatorSet.playTogether(widthAnimator, centerAnimator, textAnimator, colorAnimator) animatorSet.addListener(onStart = { text = "Success" }) animatorSet } val progressAnimator = remember { val animator = animatorOfInt(progress, 0, 100) animator.duration = 1000 animator.addListener(onEnd = { endAnimatorSet.start() }) animator } ... }
最終效果:
最后
通過本篇文章的探索可以發(fā)現(xiàn)屬性動畫在 Compose 中確實是可以使用的,雖然跟傳統(tǒng) UI 開發(fā)中使用屬性動畫有所區(qū)別,但確實能用,而且通過一個簡單的實戰(zhàn)示例發(fā)現(xiàn)好像還挺好用的。好了,我已經(jīng)學(xué)會 Compose 的動畫開發(fā)了,什么?Compose 還單獨提供了一套動畫 API ?
屬性動畫這不是挺好使的么,這不是多此一舉么,難道 Compose 的動畫 API 比屬性動畫還好用、還強大?如果感興趣請關(guān)注本專欄,從下一篇開始帶你真正走進 Compose 的動畫世界。
源碼地址:ComposeAnimationDemo
以上就是Android Compose 屬性動畫使用探索詳解的詳細內(nèi)容,更多關(guān)于Android Compose 屬性動畫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義控件ViewGroup實現(xiàn)標簽云
這篇文章主要為大家詳細介紹了Android自定義控件ViewGroup實現(xiàn)標簽云,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05Flutter框架解決盒約束widget和assets里加載資產(chǎn)技術(shù)
這篇文章主要為大家介紹了Flutter框架解決盒約束widget和assets里加載資產(chǎn)技術(shù)運用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12android中Bitmap用法(顯示,保存,縮放,旋轉(zhuǎn))實例分析
這篇文章主要介紹了android中Bitmap用法,以實例形式較為詳細的分析了android中Bitmap操作圖片的顯示、保存、縮放、旋轉(zhuǎn)等相關(guān)技巧,需要的朋友可以參考下2015-09-09解決Android 6.0獲取wifi Mac地址為02:00:00:00:00:00問題
這篇文章主要介紹了Android 6.0獲取wifi Mac地址為02:00:00:00:00:00的解決方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-11-11