動畫
主要學習內(nèi)容
- 如何使用幾個基礎動畫 API
- 何時使用哪個 API
動畫原理
相比于 Compose 中的動畫闺鲸,對于 View 體系中的動畫我們更了解一些翠拣,比如 View 動畫體系中的ObjectAnimator
误墓,其是基于動畫過程的計算出的數(shù)值調(diào)用對應屬性的setter
方法益缎,在View的setter
方法中會調(diào)用invalidate(true)
進行重繪
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"alpha",1,0,1); animator.setDuration(2000); animator.start();
ObjectAnimator
內(nèi)部就會通過反射機制去尋找setAlpha
方法欣范,將動畫過程中計算出的數(shù)值傳遞給setAlpha
方法public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ... public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { ensureTransformationInfo(); if (mTransformationInfo.mAlpha != alpha) { setAlphaInternal(alpha); if (onSetAlpha((int) (alpha * 255))) { mPrivateFlags |= PFLAG_ALPHA_SET; invalidateParentCaches(); invalidate(true); } else { mPrivateFlags &= ~PFLAG_ALPHA_SET; invalidateViewProperty(true, false); mRenderNode.setAlpha(getFinalAlpha()); } } } ... }
在 Compose 中動畫原理也差不了多少恼琼,都是計算出動畫過程中的數(shù)值晴竞,然后去修改屬性值并通知系統(tǒng)重繪該區(qū)域
不過 Compose 中使用重組通知系統(tǒng)重新繪畫噩死,而重組通常發(fā)生在可組合函數(shù)使用的狀態(tài)值發(fā)生變化的時候已维。所以在 Compose 中將動畫中以狀態(tài)方式記錄需要變化的值就可以做到通知系統(tǒng)重新繪制組件垛耳,然后在協(xié)程中計算出要變化的值并改變狀態(tài)值就可以實現(xiàn)動畫效果
所以你可以發(fā)現(xiàn)Compose中動畫變化的值都是State
基于上述的原理實現(xiàn)的最基礎的動畫效果堂鲜,當然 Compose 中動畫實現(xiàn)會更復雜泡嘴、安全和高效
class TestActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Surface { TestAnimation() } } } @Composable fun TestAnimation() { var width by remember { mutableStateOf(80.dp) } Button(onClick = { lifecycleScope.launch { var i = 0 while (i<20) { delay(50) width+=3.dp i++ } } }, modifier = Modifier.width(width)) { Text(text = "開始動畫") } } }
準備工作
因為之后的代碼都是基于其中的項目進行的酌予,所以還是推薦下載抛虫。同時也可以看一下Google人員對于的Compose的代碼編寫風格
因為代碼過多且需要修改資源文件建椰,此處就不將代碼寫出來了
因為涉及動畫效果棉姐,運行效果不好進行展示伞矩。請一定要自己進行編碼乃坤,在虛擬機或真機(推薦)上運行查看效果
其實是懶的制作gif (/ω\)沟蔑,筆記中所有的效果圖為官網(wǎng)教程中的效果圖
該項目包含多個模塊:
-
start
是本 Codelab 的起始狀態(tài) -
finished
是完成本 Codelab 后應用的最終狀態(tài)
我們可以選擇Import Project
方式進行學習瘦材,也可以通過拷貝代碼到自己項目中的方式
我使用的是拷貝代碼的方式食棕,可能之后跟
Import Project
方式有些區(qū)別請諒解
在start
項目中宣蠕,在每個我們需要修改的代碼段前都帶有 //TODO
注釋抢蚀,方便我們查找修改位置和修改需求
在Android Studio
中皿曲,可以通過左下角的 TODO
工具窗口屋休,然后瀏覽文件中的每個 TODO
注釋
簡單值動畫
我們先從 Compose 中最簡單的動畫 API 著手
運行項目,點擊頂部的“Home”和“Work”按鈕叠艳,嘗試切換標簽頁附较。這樣操作不會真正切換標簽頁內(nèi)容拒课,不過可以看到早像,內(nèi)容的背景顏色會發(fā)生變化
而我們要實現(xiàn)的效果就是讓背景顏色的變化呈現(xiàn)動畫效果扎酷,即增加過渡效果
我們點擊 TODO 工具窗口中的 TODO 1
val backgroundColor = if (tabPage == TabPage.Home) Purple100 else Green300
背景顏色可以在紫色和綠色之間切換法挨,具體取決于backgroundColor
凡纳。我們需要為這個值的變化添加動畫效果
如需為諸如此類的簡單值變化添加動畫效果荐糜,我們可以使用 animate*AsState
API暴氏。只需使用 animate*AsState
可組合項的相應變體(在本例中為 animateColorAsState
)封裝更改值答渔,即可創(chuàng)建動畫值沼撕。返回的值是 State<T>
對象务豺,因此我們可以使用包含 by
聲明的本地委托屬性笼沥,以將該值視為普通變量
val backgroundColor by animateColorAsState(targetValue = if (tabPage == TabPage.Home) Purple100 else Green300)
重新運行應用并嘗試切換標簽頁”记常現(xiàn)在顏色變化會呈現(xiàn)動畫效果
可見性動畫
當我們滾動應用內(nèi)容,會發(fā)現(xiàn)懸浮操作按鈕按照滾動方向而展開和縮小
找到 TODO 2-1 可以看到其背后的機制营勤。它位于 HomeFloatingActionButton
可組合項中葛作。使用 if
語句顯示或隱藏表示“EDIT”的文本
if (extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
添加可見性動畫非常簡單赂蠢,只需將 if
替換為 AnimatedVisibility
可組合項即可
AnimatedVisibility(extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
當指定的 Boolean
值發(fā)生變化時虱岂,AnimatedVisibility
會運行其動畫第岖。默認情況下蔑滓,AnimatedVisibility
會以淡入和展開的方式顯示元素键袱,以淡出和縮小的方式隱藏元素
@Composable
fun RowScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandHorizontally(),
exit: ExitTransition = fadeOut() + shrinkHorizontally(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) { ... }
當然我們也可以自定義動畫方式
點擊FloatingActionButton
后褐健,我們會看到條內(nèi)容為“Edit feature is not supported”的消息铝量,即EditMessage
可組合項
在EditMessage
中使用AnimatedVisibility
為其出現(xiàn)和消失添加動畫效果慢叨,我們通過自定義動畫效果拍谐,讓其元素出現(xiàn)時從頂部移出轩拨,消失時移入至頂部
找到 TODO 2-2 并查看 EditMessage
可組合項中的代碼
AnimatedVisibility(
visible = shown
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
想要自定義動畫亡蓉,就需要我們指定enter
和 exit
參數(shù)的值
enter
參數(shù)是 EnterTransition
的實例砍濒,要實現(xiàn)組件移動效果爸邢,我們可以使用 slideInVertically
函數(shù)創(chuàng)建 EnterTransition
此函數(shù)可使用 initialOffsetY
和 animationSpec
參數(shù)進一步自定義
-
initialOffsetY
是返回動畫開始元素y坐標
位置的 lambda杠河。lambda 會收到一個表示元素高度的參數(shù)券敌,因此我們只需返回其負值即可陪白。使用slideInVertically
時咱士,滑入后的目標偏移量始終為0
(像素)≡觯可使用 lambda 函數(shù)將initialOffsetY
指定為絕對值
@Stable
fun slideInVertically(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffsetY: (fullHeight: Int) -> Int = { -it / 2 },
): EnterTransition =
slideIn(
initialOffset = { IntOffset(x = 0, y = initialOffsetY(it.height)) },
animationSpec = animationSpec
)
slideInVertically
會將元素y方向上的偏移量值會從initialOffsetY
變?yōu)?所以要實現(xiàn)整個元素從頂部移出效果序厉,我們需要讓 lambda 函數(shù)返回
-元素高度
-
animationSpec
是包括EnterTransition
和ExitTransition
在內(nèi)的許多動畫 API 的通用參數(shù)。我們可以傳遞各種AnimationSpec
類型中的一種毕箍,以指定動畫值應如何隨時間變化在本示例中弛房,我們使用基于時長的簡單
AnimationSpec
。它可以使用tween
函數(shù)創(chuàng)建文捶。時長為 150 毫秒,加/減速選項為LinearOutSlowInEasing
@Stable fun <T> tween( durationMillis: Int = DefaultDurationMillis, delayMillis: Int = 0, easing: Easing = FastOutSlowInEasing ): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
easing
參數(shù)可以理解為是動畫執(zhí)行時的速度控制器媒咳,可以類比為View體系中的Interpolator
類(插值器)
同樣粹排,我們可以對 exit
參數(shù)使用 slideOutVertically
函數(shù)。slideOutVertically
假定初始偏移量為 0涩澡,因此只需指定 targetOffsetY
顽耳。我們對 animationSpec
參數(shù)使用相同的 tween
函數(shù),但時長為 250 毫秒妙同,加/減速選項為 FastOutLinearInEasing
slideOutVertically
中參數(shù)為animationSpec
和targetOffsetY
射富,其中targetOffsetY
是返回動畫結(jié)束時元素y坐標
位置的 lambda,即元素y坐標
會從0開始變化為targetOffsetY
最后代碼:
@Composable
private fun EditMessage(shown: Boolean) {
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
animationSpec = tween(
durationMillis = 150,
easing = LinearOutSlowInEasing
),
initialOffsetY = { fullHeight -> -fullHeight }
),
exit = slideOutVertically(
animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing),
targetOffsetY = { fullHeight -> -fullHeight }
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
}
內(nèi)容大小變化動畫
在實例應用中點擊TopicRow
會展開并顯示該主題的正文部分粥帚。當正文顯示或隱藏時胰耗,包含文本的組件會展開或縮小
查看 TopicRow
可組合項中 TODO 3 的代碼
@Composable
private fun TopicRow(topic: String, expanded: Boolean, onClick: () -> Unit) {
TopicRowSpacer(visible = expanded)
Surface(
...
) {
// TODO 3: Animate the size change of the content.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.animateContentSize()
) {
...
//也可以用AnimatedVisiibility替換if,實現(xiàn)動畫效果
if (expanded) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.lorem_ipsum),
textAlign = TextAlign.Justify
)
}
}
}
TopicRowSpacer(visible = expanded)
}
注釋處的Column
可組合項會在內(nèi)容發(fā)生變化時更改其大小芒涡。我們可以添加 animateContentSize
修飾符柴灯,為其大小變化添加動畫效果
當然我們也可以用可見性動畫實現(xiàn),不過具體效果上有些差異
可見性動畫:文本是從底部向上展開
內(nèi)容大小變化動畫:文本從頂部向下展開
多值動畫
現(xiàn)在我們已經(jīng)熟悉一些基本的動畫 API拖陆,接下來我們來了解一下 Transition
API弛槐。借助該 API,我們可以制作更復雜的動畫
在本示例中依啰,我們將完成HomeTabBar
上顯示的矩形的移動動畫乎串,不同于之間的簡單值動畫,這次將同時變化多個值實現(xiàn)移動和顏色變化
在 HomeTabIndicator
可組合項中找到 TODO 4,查看標簽頁指示器的實現(xiàn)方式
@Composable
private fun HomeTabIndicator(
tabPositions: List<TabPosition>,
tabPage: TabPage
) {
// TODO 4: Animate these value changes.
val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) Purple700 else Green800
Box(
Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.BottomStart)
.offset(x = indicatorLeft)
.width(indicatorRight - indicatorLeft)
.padding(4.dp)
.fillMaxSize()
.border(
BorderStroke(2.dp, color),
RoundedCornerShape(4.dp)
)
)
}
其中叹誉,indicatorLeft
表示標簽頁行中指示器左側(cè)邊緣的水平位置鸯两。indicatorRight
表示指示器右側(cè)邊緣的水平位置。顏色也在紫色和綠色之間變化
如需同時為多個值添加動畫效果长豁,可使用 Transition
钧唐。Transition
可使用 updateTransition
函數(shù)創(chuàng)建。將當前所選標簽頁的索引作為 targetState
參數(shù)傳遞
@Composable fun <T> updateTransition( targetState: T, label: String? = null ): Transition<T> { ... }
當
targetState
發(fā)生變化時匠襟,Transition
會將其所有子動畫運行到為新targetState
指定的目標值
每個動畫值都可以使用 Transition
的 animate*
擴展函數(shù)進行聲明钝侠。在本示例中,我們使用 animateDp
和 animateColor
酸舍。它們會接受一個 lambda 塊帅韧,我們可以為每個狀態(tài)指定目標值
val transition = updateTransition(tabPage)
val indicatorLeft by transition.animateDp { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor { page ->
if (page == TabPage.Home) Purple700 else Green800
}
點擊標簽頁會更改 tabPage
狀態(tài)的值,這時與 transition
關聯(lián)的所有動畫值會開始以動畫方式切換至為目標狀態(tài)指定的值
此外啃勉,我們可以指定 transitionSpec
參數(shù)來自定義動畫行為
@Composable
inline fun <S> Transition<S>.animateDp(
noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Dp> = {
spring(visibilityThreshold = Dp.VisibilityThreshold)
},
label: String = "DpAnimation",
targetValueByState: @Composable (state: S) -> Dp
): State<Dp>
例如忽舟,我們可以讓靠近目標頁面的一邊比另一邊移動得更快來實現(xiàn)指示器的彈性效果』床可以在 transitionSpec
lambda 中使用 isTransitioningTo
infix 函數(shù)來確定狀態(tài)變化的方向
val transition = updateTransition(
tabPage,
label = "Tab indicator"
)
val indicatorLeft by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
spring(stiffness = Spring.StiffnessVeryLow)
} else {
spring(stiffness = Spring.StiffnessMedium)
}
},
label = "Indicator left"
) { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
spring(stiffness = Spring.StiffnessMedium)
} else {
spring(stiffness = Spring.StiffnessVeryLow)
}
},
label = "Indicator right"
) { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(
label = "Border color"
) { page ->
if (page == TabPage.Home) Purple700 else Green800
}
重復動畫
點擊當前氣溫旁邊的刷新圖標按鈕叮阅。應用開始加載最新天氣信息(當然只是模擬)。在加載完成之前泣特,會看到加載指示器浩姥,即一個灰色圓圈和一個條形。我們來為該指示器的 Alpha 值添加動畫效果群扶,以便更清楚地呈現(xiàn)該進程正在進行
在 LoadingRow
可組合項中找到 TODO 5
@Composable
private fun LoadingRow() {
// TODO 5: Animate this value between 0f and 1f, then back to 0f repeatedly.
val alpha = 1f
Row(
modifier = Modifier
.heightIn(min = 64.dp)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.LightGray.copy(alpha = alpha))
)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(Color.LightGray.copy(alpha = alpha))
)
}
}
我們希望Alpha值設為在 0f 和 1f 之間以動畫效果方式重復呈現(xiàn)及刻,為此,可以使用 InfiniteTransition
此 API 與 Transition
API 類似竞阐。兩者都是為多個值添加動畫效果缴饭,但 Transition
會根據(jù)狀態(tài)變化為值添加動畫效果,而 InfiniteTransition
則無限期地為值添加動畫效果
如需創(chuàng)建 InfiniteTransition
骆莹,請使用 rememberInfiniteTransition
函數(shù)颗搂。然后,可以使用 InfiniteTransition
的一個 animate*
擴展函數(shù)聲明每個動畫值變化
在本例中幕垦,我們要為 Alpha 值添加動畫效果丢氢,所以使用 animatedFloat
。initialValue
參數(shù)應為 0f
先改,而 targetValue
應為 1f
疚察。我們還可以為此動畫指定 AnimationSpec
,但此 API 僅接受 InfiniteRepeatableSpec
仇奶。我們可以使用 infiniteRepeatable
函數(shù)創(chuàng)建InfiniteRepeatableSpec
InfiniteRepeatableSpec
會封裝任何基于時長的AnimationSpec
貌嫡,使其可重復
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000),
repeatMode = RepeatMode.Reverse
)
)
運行應用,然后嘗試點擊刷新按鈕。現(xiàn)在岛抄,您可以看到加載指示器會顯示動畫效果
手勢動畫
最后一部分中别惦,我們將學習如何基于觸控輸入運行動畫
要實現(xiàn)的效果類似于狀態(tài)欄中通知的滑動事件,會根據(jù)滑動速度決定元素回到原位或被移除
在這種情況下夫椭,需要考慮幾個獨特的因素掸掸。首先,任何正在播放的動畫都可能會被觸摸事件攔截蹭秋。其次扰付,動畫值可能不是唯一的可信來源。換句話說感凤,我們可能需要將動畫值與來自觸摸事件的值同步
在Modifier.swipeToDismiss
修飾符中找到 TODO 6-1
我們通過創(chuàng)建一個修飾符悯周,以使觸摸時元素可滑動。當元素被快速滑動到屏幕邊緣時陪竿,我們將調(diào)用 onDismissed
回調(diào),以便移除該元素
Animatable
是我們目前看到的最低級別的 API屠橄。它有一些對手勢場景非常有用的功能族跛,所以我們可以創(chuàng)建一個 Animatable
實例,并使用它表示可滑動元素的水平偏移量
val offsetX = remember { Animatable(0f) } //新增這行代碼
pointerInput {
// 用于計算動畫的穩(wěn)定位置
val decay = splineBasedDecay<Float>(this)
// 在協(xié)程中使用掛起函數(shù)來處理觸摸事件和動畫
coroutineScope {
while (true) {
// ...
TODO 6-2 是我們剛剛收到向下輕觸事件的位置锐墙。如果動畫當前正在運行礁哄,我們應將其攔截∠保可以通過對 Animatable
調(diào)用 stop
來實現(xiàn)此目的
當然如果動畫未運行桐绒,系統(tǒng)會忽略該函數(shù)調(diào)用
// 等待手指按下事件
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // 新增這行代碼
// 準備拖動事件,并且記錄下移動的速度
val velocityTracker = VelocityTracker()
// 等待拖動事件
awaitPointerEventScope {
在 TODO 6-3 位置之拨,我們不斷接收到拖動事件茉继。必須將觸摸事件的位置同步到動畫值中。為此蚀乔,我們可以對 Animatable
使用 snapTo
horizontalDrag(pointerId) { change ->
// 新增下列三行代碼
// 獲取到觸摸事件位置
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
offsetX.snapTo(horizontalDragOffset)
}
// 記錄拖動的速度
velocityTracker.addPosition(change.uptimeMillis, change.position)
// 消費掉這個手勢事件烁竭,不向下傳遞事件
change.consumePositionChange()
}
TODO 6-4 是元素剛剛被松開和快速滑動的位置。我們需要計算快速滑動操作的最終位置吉挣,以便確定是要將元素滑回原始位置派撕,還是滑開元素并調(diào)用回調(diào)
// 拖動結(jié)束,計算拖動的速度
val velocity = velocityTracker.calculateVelocity().x
// 新增這行代碼
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
calculateTargetValue
會根據(jù)initialValue
和initialVelocity
計算浮點衰減動畫的目標值
在 TODO 6-5 位置睬魂,我們將開始播放動畫终吼。但在此之前,我們需要為 Animatable
設置值的上下界限氯哮,使其在到達界限時立即停止际跪。借助 pointerInput
修飾符,我們可以通過 size
屬性訪問元素的大小,因此我們可以使用它獲取界限
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
最終垫卤,我們可以在 TODO 6-6 位置開始播放動畫威彰。我們首先來比較之前計算的快速滑動操作的最終位置以及元素的大小。如果最終位置低于該大小穴肘,則表示快速滑動的速度不夠歇盼。可使用 animateTo
將值的動畫效果設置回 0f评抚。否則豹缀,我們可以使用 animateDecay
來開始播放快速滑動動畫。當動畫結(jié)束(很可能是到達我們之前設置的界限)時慨代,我們可以調(diào)用回調(diào)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// 拖動的速度不夠邢笙,滑動回原位
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// 拖動的速度足夠,將元素滑出屏幕邊緣
offsetX.animateDecay(velocity, decay)
// 執(zhí)行回調(diào)
onDismissed()
}
}
最后侍匙,我們來到 TODO 6-7氮惯。我們已設置所有動畫和手勢,因此想暗,請記得對元素應用偏移
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
最終代碼:
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }//新增這行代碼
pointerInput(Unit) {
// 用于計算動畫的穩(wěn)定位置
val decay = splineBasedDecay<Float>(this)
// 在協(xié)程中使用掛起函數(shù)來處理觸摸事件和動畫
coroutineScope {
while (true) {
// 等待手指按下事件
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop()//新增這行代碼
// 準備拖動事件妇汗,并且記錄下移動的速度
val velocityTracker = VelocityTracker()
// 等待拖動事件
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// 新增下列三行代碼
// 獲取到觸摸事件位置
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
offsetX.snapTo(horizontalDragOffset)
}
// 記錄拖動的速度
velocityTracker.addPosition(change.uptimeMillis, change.position)
// 消費掉這個手勢事件,不向下傳遞事件
change.consumePositionChange()
}
}
// 拖動結(jié)束说莫,計算拖動的速度
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)//新增這行代碼
//新增這行代碼
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// 拖動的速度不夠杨箭,滑動回原位
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// 拖動的速度足夠,將元素滑出屏幕邊緣
offsetX.animateDecay(velocity, decay)
// 執(zhí)行回調(diào)
onDismissed()
}
}
}
}
}
}.offset {
IntOffset(offsetX.value.roundToInt(), 0)
}
}
手勢動畫這塊储狭,官網(wǎng)教程沒有鋪墊一點手勢相關的部分就開始于動畫效果配合互婿,對于初學者不是特別友好,而且動畫效果也沒有介紹完全辽狈,比如這里使用的
Animatable
就沒有介紹清晰