Jetpack Compose 卡叠时序问题

Jetpack Compose Timing Issue with Stack of Cards

实际上我什至不确定这是否是时间问题,但让我们先从代码开始。

我从 MainActivity 开始,准备一个简单的数据结构,其中包含从 AZ

的字母
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val model = mutableStateListOf<Char>()
            model.addAll(('A'..'Z').toList())

            val swipeComplete = {
                model.removeFirst()
            }

            CardStack(elements = model, onSwipeComplete = { swipeComplete() })
        }
    }
}

这里我调用CardStack,看起来像下面这样

@Composable
fun CardStack(elements: List<Char>, onSwipeComplete: () -> Unit) {
    elements.take(2).reversed().forEachIndexed { _, character ->
        Box {
            SwipeCard(
                character.toString(),
                onSwipeComplete = onSwipeComplete
            )
        }
    }
}

刷卡的时候,我也想看下面的卡。因此,我只拿出最上面的两张牌并展示它们。然后是 SwipeCard 本身。

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeCard(text: String, onSwipeComplete: () -> Unit) {
    val color by remember {
        val random = Random()
        mutableStateOf(Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)))
    }

    val screenWidth = LocalConfiguration.current.screenWidthDp.dp.value
    val screenDensity = LocalConfiguration.current.densityDpi

    var offsetXTarget by remember { mutableStateOf(0f) }
    var offsetYTarget by remember { mutableStateOf(0f) }

    val swipeThreshold = abs(screenWidth * screenDensity / 100)

    var dragInProgress by remember {
        mutableStateOf(false)
    }

    val offsetX by animateFloatAsState(
        targetValue = offsetXTarget,
        animationSpec = tween(
            durationMillis = screenDensity / 3,
            easing = LinearEasing
        ),
        finishedListener = {
            if (!dragInProgress) {
                onSwipeComplete()
            }
        }
    )
    val offsetY by animateFloatAsState(
        targetValue = offsetYTarget,
        animationSpec = tween(
            durationMillis = screenDensity / 3,
            easing = LinearEasing
        )
    )

    val rotationZ = (offsetX / 60).coerceIn(-40f, 40f) * -1

    Card(
        shape = RoundedCornerShape(20.dp),
        elevation = 0.dp,
        backgroundColor = color,
        modifier = Modifier
            .fillMaxSize()
            .padding(50.dp)
            .graphicsLayer(
                translationX = offsetX,
                translationY = offsetY,
                rotationZ = rotationZ
            )
            .pointerInput(Unit) {
                detectDragGestures(
                    onDrag = { change, dragAmount ->
                        dragInProgress = true
                        change.consumeAllChanges()
                        offsetXTarget += dragAmount.x
                        offsetYTarget += dragAmount.y
                    },
                    onDragEnd = {
                        if (abs(offsetX) < swipeThreshold / 20) {
                            offsetXTarget = 0f
                            offsetYTarget = 0f
                        } else {
                            offsetXTarget = swipeThreshold
                            offsetYTarget = swipeThreshold
                            if (offsetX < 0) {
                                offsetXTarget *= -1
                            }
                        }
                        dragInProgress = false
                    }
                )
            }

    ) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            Text(
                text = text,
                style = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 52.sp
                ),
                color = Color.White
            )
        }
    }
}

实际效果如下:

几个关键点,让我们考虑从AZ的所有字母的初始状态:

当我开始拖动带有字母“A”的卡片时,我可以看到下面带有字母“B”的卡片。

当拖动动作结束时,字母“A”的卡片将移动到左侧或右侧,具体取决于用户选择的一侧。

动画完成后,将调用 onSwipeComplete 以删除最顶层的元素,即我的数据模型中的字母“A”。

从数据模型中删除最顶层的元素后,我希望卡片堆可以用字母“B”和“C”重新组合。

问题是当卡片“A”动画消失时,字母“B”突然画在这张动画卡片上,而“B”所在的位置现在是“C”。

似乎数据模型已经更新,而第一张带有字母“A”的卡片仍在动画中。

这让我只剩下一张字母“C”的卡片。 “C”下面没有其他卡片。

对我来说,时间似乎有问题,但我无法弄清楚到底是什么。

以下是要添加的所有导入:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.swipecard.ui.theme.SwipeCardTheme
import java.util.*
import kotlin.math.abs

这还需要以下依赖项

implementation "androidx.compose.runtime:runtime:1.0.1"
implementation "androidx.compose.runtime:runtime-livedata:1.0.1"

当您从数组中删除一个项目时,从 Compose 的角度来看,它看起来就像您删除了最后一个视图并更改了其他视图的数据。视图 A 的视图被内容 B 重用,并且因为该视图具有 non-zero 偏移值,所以它在屏幕上不可见,所以您只能看到视图 C。

使用key,您可以告诉 Compose 哪个视图与哪些数据相关联,以便正确地重复使用它们:

@Composable
fun CardStack(elements: List<Char>, onSwipeComplete: () -> Unit) {
    elements.take(2).reversed().forEach { character ->
        key(character) {
            SwipeCard(
                character.toString(),
                onSwipeComplete = onSwipeComplete
            )
        }
    }
}

p.s。关于您的代码的一些评论:

  1. 你把screenDensity传给durationMillis很奇怪。就毫秒而言,它的值非常小,这使您的动画几乎是即时的,并且从逻辑上看有点奇怪。
  2. 如果您不需要来自 forEachIndexed 的索引,只需使用 forEach 而不是指定 _ 占位符。
  3. 像您在此处所做的那样使用 Box,当您只有一个 child 并且没有为 Box 指定任何修饰符时,它没有任何效果。