Jetpack Compose 卡叠时序问题
Jetpack Compose Timing Issue with Stack of Cards
实际上我什至不确定这是否是时间问题,但让我们先从代码开始。
我从 MainActivity
开始,准备一个简单的数据结构,其中包含从 A
到 Z
的字母
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
)
}
}
}
实际效果如下:
几个关键点,让我们考虑从A
到Z
的所有字母的初始状态:
当我开始拖动带有字母“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。关于您的代码的一些评论:
- 你把
screenDensity
传给durationMillis
很奇怪。就毫秒而言,它的值非常小,这使您的动画几乎是即时的,并且从逻辑上看有点奇怪。
- 如果您不需要来自
forEachIndexed
的索引,只需使用 forEach
而不是指定 _
占位符。
- 像您在此处所做的那样使用
Box
,当您只有一个 child 并且没有为 Box
指定任何修饰符时,它没有任何效果。
实际上我什至不确定这是否是时间问题,但让我们先从代码开始。
我从 MainActivity
开始,准备一个简单的数据结构,其中包含从 A
到 Z
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
)
}
}
}
实际效果如下:
几个关键点,让我们考虑从A
到Z
的所有字母的初始状态:
当我开始拖动带有字母“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。关于您的代码的一些评论:
- 你把
screenDensity
传给durationMillis
很奇怪。就毫秒而言,它的值非常小,这使您的动画几乎是即时的,并且从逻辑上看有点奇怪。 - 如果您不需要来自
forEachIndexed
的索引,只需使用forEach
而不是指定_
占位符。 - 像您在此处所做的那样使用
Box
,当您只有一个 child 并且没有为Box
指定任何修饰符时,它没有任何效果。