Jeptpack Compose 官網(wǎng)教程學習筆記(五)動畫

動畫

主要學習內(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 = "開始動畫")
        }
    }
}

準備工作

官網(wǎng)示例下載

因為之后的代碼都是基于其中的項目進行的酌予,所以還是推薦下載抛虫。同時也可以看一下Google人員對于的Compose的代碼編寫風格

因為代碼過多且需要修改資源文件建椰,此處就不將代碼寫出來了

因為涉及動畫效果棉姐,運行效果不好進行展示伞矩。請一定要自己進行編碼乃坤,在虛擬機或真機(推薦)上運行查看效果

其實是懶的制作gif (/ω\)沟蔑,筆記中所有的效果圖為官網(wǎng)教程中的效果圖

該項目包含多個模塊:

  • start 是本 Codelab 的起始狀態(tài)
  • finished 是完成本 Codelab 后應用的最終狀態(tài)

我們可以選擇Import Project方式進行學習瘦材,也可以通過拷貝代碼到自己項目中的方式

我使用的是拷貝代碼的方式食棕,可能之后跟Import Project方式有些區(qū)別請諒解

start項目中宣蠕,在每個我們需要修改的代碼段前都帶有 //TODO 注釋抢蚀,方便我們查找修改位置和修改需求

Android Studio中皿曲,可以通過左下角的 TODO 工具窗口屋休,然后瀏覽文件中的每個 TODO注釋

TODO 工具窗口

簡單值動畫

我們先從 Compose 中最簡單的動畫 API 著手

運行項目,點擊頂部的“Home”和“Work”按鈕叠艳,嘗試切換標簽頁附较。這樣操作不會真正切換標簽頁內(nèi)容拒课,不過可以看到早像,內(nèi)容的背景顏色會發(fā)生變化

而我們要實現(xiàn)的效果就是讓背景顏色的變化呈現(xiàn)動畫效果扎酷,即增加過渡效果

Home標簽
Work標簽

我們點擊 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)動畫效果

animateColorAsState效果

可見性動畫

當我們滾動應用內(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)
    )
}
AnimatedVisibility效果

當指定的 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)
        )
    }
}

想要自定義動畫亡蓉,就需要我們指定enterexit 參數(shù)的值

enter 參數(shù)是 EnterTransition 的實例砍濒,要實現(xiàn)組件移動效果爸邢,我們可以使用 slideInVertically 函數(shù)創(chuàng)建 EnterTransition

此函數(shù)可使用 initialOffsetYanimationSpec 參數(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 是包括 EnterTransitionExitTransition在內(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ù)為animationSpectargetOffsetY射富,其中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)
            )
        }
    }
}
AnimatedVisibility效果

內(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 修飾符柴灯,為其大小變化添加動畫效果

animateContentSize效果

當然我們也可以用可見性動畫實現(xiàn),不過具體效果上有些差異

可見性動畫:文本是從底部向上展開

內(nèi)容大小變化動畫:文本從頂部向下展開

多值動畫

現(xiàn)在我們已經(jīng)熟悉一些基本的動畫 API拖陆,接下來我們來了解一下 Transition API弛槐。借助該 API,我們可以制作更復雜的動畫

在本示例中依啰,我們將完成HomeTabBar上顯示的矩形的移動動畫乎串,不同于之間的簡單值動畫,這次將同時變化多個值實現(xiàn)移動和顏色變化

image.png

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 指定的目標值

每個動畫值都可以使用 Transitionanimate* 擴展函數(shù)進行聲明钝侠。在本示例中,我們使用 animateDpanimateColor酸舍。它們會接受一個 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)指定的值

Transition多值動畫

此外啃勉,我們可以指定 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
}
Spring彈性效果

重復動畫

點擊當前氣溫旁邊的刷新圖標按鈕叮阅。應用開始加載最新天氣信息(當然只是模擬)。在加載完成之前泣特,會看到加載指示器浩姥,即一個灰色圓圈和一個條形。我們來為該指示器的 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 值添加動畫效果丢氢,所以使用 animatedFloatinitialValue 參數(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)在岛抄,您可以看到加載指示器會顯示動畫效果

rememberInfiniteTransition效果

手勢動畫

最后一部分中别惦,我們將學習如何基于觸控輸入運行動畫

要實現(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ù) initialValueinitialVelocity 計算浮點衰減動畫的目標值

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就沒有介紹清晰

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末慈参,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子稻艰,更是在濱河造成了極大的恐慌懂牧,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尊勿,死亡現(xiàn)場離奇詭異僧凤,居然都是意外死亡,警方通過查閱死者的電腦和手機元扔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門躯保,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人澎语,你說我怎么就攤上這事途事⊙榘茫” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵尸变,是天一觀的道長义图。 經(jīng)常有香客問我,道長召烂,這世上最難降的妖魔是什么碱工? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮奏夫,結(jié)果婚禮上怕篷,老公的妹妹穿的比我還像新娘。我一直安慰自己酗昼,他們只是感情好廊谓,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著麻削,像睡著了一般蒸痹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碟婆,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天电抚,我揣著相機與錄音,去河邊找鬼竖共。 笑死,一個胖子當著我的面吹牛俺祠,可吹牛的內(nèi)容都是我干的公给。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蜘渣,長吁一口氣:“原來是場噩夢啊……” “哼淌铐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蔫缸,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤腿准,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拾碌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吐葱,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年校翔,在試婚紗的時候發(fā)現(xiàn)自己被綠了弟跑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡防症,死狀恐怖孟辑,靈堂內(nèi)的尸體忽然破棺而出哎甲,到底是詐尸還是另有隱情,我是刑警寧澤饲嗽,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布炭玫,位于F島的核電站,受9級特大地震影響貌虾,放射性物質(zhì)發(fā)生泄漏吞加。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一酝惧、第九天 我趴在偏房一處隱蔽的房頂上張望榴鼎。 院中可真熱鬧,春花似錦晚唇、人聲如沸巫财。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽平项。三九已至,卻和暖如春悍及,著一層夾襖步出監(jiān)牢的瞬間闽瓢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工心赶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扣讼,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓缨叫,卻偏偏與公主長得像椭符,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子耻姥,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容