You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Compose中如何实现指定的垂直滑动动画效果?

解决Compose中CircleWordsSwipe的滑动动画问题

我想要实现一个垂直滑动交互:向上滑动元素使其滑出屏幕,随后从屏幕中心重新出现新的元素。我尝试用AnchoredDraggable但没能让元素回归初始状态,改用AnimatedContent和pointerInput的方案虽然能运行,但效果不完善。现有代码如下:

fun CircleWordsSwipe(
       modifier: Modifier = Modifier,
       word: String = "word",
       onGuessed: () -> Unit = { print("+") },
       onNotGuessed: () -> Unit = { print("-") },
    ) {
    var offsetY by remember { mutableStateOf(0f) }
    var dismissedState by remember { mutableStateOf(true) }
    var lastState by remember { mutableStateOf(true) }

    AnimatedContent(
        targetState = offsetY,
        transitionSpec = {
            scaleIn(
                animationSpec = tween(550, 150),
                initialScale = if (offsetY == 0f) 1f else 01f) togetherWith
                    slideOutVertically(animationSpec = tween(250),
                        targetOffsetY = { offsetY.roundToInt() }) using
                    SizeTransform { initialSize, targetSize ->
                        keyframes {
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 100
                        }
                    }
        },
        modifier = Modifier
            .offset {
                IntOffset(
                    x = 0, y = offsetY.roundToInt()
                )
            }.pointerInput(dismissedState) {

                detectVerticalDragGestures(
                    onDragEnd = {
                        offsetY = 0f
                    }
                ) { change, dragAmount ->
                    change.consume()

                    if (offsetY >= 100f && dismissedState) {
                        onNotGuessed()
                        offsetY = 0f
                        lastState = false
                        dismissedState = false
                    } else if (offsetY <= -100f && dismissedState) {
                        onGuessed()
                        offsetY = 0f
                        lastState = true
                        dismissedState = false
                    } else {
                        if (dismissedState)
                            offsetY += dragAmount
                    }
                }
                detectTapGestures(
                    onPress = {  },
                    onLongPress = {  },
                    onTap = {  },
                )
            },
        ) { Content() 
    }
     LaunchedEffect(!dismissedState) {
        offsetY = 0f
        dismissedState = true
    }
}

问题分析与修复方案

原代码的核心问题在于用offsetY作为AnimatedContent的目标状态,无法正确触发"元素滑出后重新出现"的完整动画流程。我们需要调整状态管理逻辑,用独立状态标记内容刷新时机,同时优化动画过渡和拖拽处理逻辑。

以下是修复后的完整代码:

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun CircleWordsSwipe(
    modifier: Modifier = Modifier,
    word: String = "word",
    onGuessed: () -> Unit = { println("+") },
    onNotGuessed: () -> Unit = { println("-") },
) {
    // 触发内容刷新的状态标记,每次滑动完成后切换值
    var refreshTrigger by remember { mutableStateOf(false) }
    // 拖拽时的临时垂直偏移量
    var dragOffsetY by remember { mutableStateOf(0f) }
    // 标记是否正在执行退出动画
    var isExiting by remember { mutableStateOf(false) }

    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        AnimatedContent(
            targetState = refreshTrigger,
            transitionSpec = {
                // 根据滑动方向设置退出动画路径
                val exitSlide = if (dragOffsetY < 0) {
                    slideOutVertically(tween(250)) { -it }
                } else {
                    slideOutVertically(tween(250)) { it }
                }
                (exitSlide + fadeOut(tween(250))) togetherWith
                        // 进入动画:延迟150ms从屏幕中心淡入
                        slideInVertically(tween(550, delayMillis = 150)) { 0 } + fadeIn(tween(550, delayMillis = 150))
            }
        ) {
            WordContent(
                word = word,
                offsetY = dragOffsetY,
                onDrag = { delta ->
                    if (!isExiting) dragOffsetY += delta
                },
                onDragEnd = {
                    when {
                        dragOffsetY <= -100f -> {
                            onGuessed()
                            isExiting = true
                        }
                        dragOffsetY >= 100f -> {
                            onNotGuessed()
                            isExiting = true
                        }
                        else -> {
                            // 拖拽距离不足,重置偏移量
                            dragOffsetY = 0f
                        }
                    }
                }
            )
        }
    }

    // 退出动画完成后重置状态,触发新内容进入
    LaunchedEffect(isExiting) {
        if (isExiting) {
            kotlinx.coroutines.delay(250) // 等待退出动画结束
            dragOffsetY = 0f
            refreshTrigger = !refreshTrigger
            isExiting = false
        }
    }
}

@Composable
private fun WordContent(
    word: String,
    offsetY: Float,
    onDrag: (Float) -> Unit,
    onDragEnd: () -> Unit
) {
    Box(
        modifier = Modifier
            .size(120.dp)
            .offset { IntOffset(0, offsetY.roundToInt()) }
            .pointerInput(Unit) {
                detectVerticalDragGestures(
                    onDragEnd = onDragEnd
                ) { change, dragAmount ->
                    change.consume()
                    onDrag(dragAmount)
                }
            },
        contentAlignment = Alignment.Center
    ) {
        // 替换为你实际的圆形单词组件
        Text(text = word, fontSize = 24.sp)
    }
}

关键改动说明

  1. 状态拆分:

    • 用refreshTrigger控制AnimatedContent的内容刷新,确保每次滑动后触发新元素的进入动画
    • 分离dragOffsetY(拖拽临时偏移)和isExiting(退出动画标记),避免状态逻辑混淆
  2. 动画逻辑优化:

    • 退出动画根据滑动方向动态设置滑出路径,匹配用户操作意图
    • 进入动画延迟150ms后从屏幕中心淡入,完全符合示例效果
  3. 拖拽逻辑简化:

    • 将拖拽处理封装到WordContent组件,主组件逻辑更清晰
    • 仅当拖拽距离超过阈值时触发回调和动画,否则重置偏移量
  4. 时序控制:

    • 通过LaunchedEffect等待退出动画完成后再重置状态,保证动画流程的连贯性

内容的提问来源于stack exchange,提问作者Riffaells

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 23:48:14