Как сделать, чтоб анимация на LazyGrid для item производилась только первый раз?
Должно быть не сильно сложно, но по какой то причине рекомпозиция проходит 2 раза, что мешает добавить условие для выполнения анимиции только первый раз.
В общем хочу сделать так как в этом примере, но я немного изменил его, но суть такая же - https://yasinkacmaz.medium.com/simple-item-animation-with-jetpack-composes-lazygrid-78316992af22
Нужно, чтоб элементы грида появлялись бабл эфектом
Вот такой код
private val dataSet: List<String> = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
private val data: List<String> = List(5) { dataSet }.flatten()
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Test_delete_itTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Gallery(
paddingValues = innerPadding,
uiConfig = { data }
)
}
}
}
}
}
@Composable
private fun Gallery(
paddingValues: PaddingValues,
uiConfig: () -> List<String>
) {
val config: List<String> = uiConfig()
val columns = 2
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(config.size) { idx ->
val item: String = config[idx]
val (scale, alpha) = scaleAndAlpha(idx, columns)
MyItem(
modifier = Modifier.graphicsLayer(alpha = alpha, scaleX = scale, scaleY = scale),
text = item
)
}
}
)
}
}
@Composable
private fun MyItem(
modifier: Modifier = Modifier,
text: String
) {
Card(
modifier = modifier.height(150.dp),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(8.dp),
colors = CardDefaults.cardColors(
containerColor = Color.Blue,
)
) {
Box(
modifier = Modifier
.weight(1f)
.height(150.dp)
.clip(RoundedCornerShape(16.dp))
) {
Text(
text = text,
color = Color.White
)
}
}
}
@Immutable
private enum class State { PLACING, PLACED }
@Immutable
data class ScaleAndAlphaArgs(
val fromScale: Float,
val toScale: Float,
val fromAlpha: Float,
val toAlpha: Float
)
@OptIn(ExperimentalTransitionApi::class)
@Composable
fun scaleAndAlpha(
args: ScaleAndAlphaArgs,
animation: FiniteAnimationSpec<Float>
): Pair<Float, Float> {
val transitionState = remember { MutableTransitionState(State.PLACING).apply { targetState = State.PLACED } }
val transition = rememberTransition(transitionState, label = "")
val alpha by transition.animateFloat(transitionSpec = { animation }, label = "") {
if (it == State.PLACING) args.fromAlpha else args.toAlpha
}
val scale by transition.animateFloat(transitionSpec = { animation }, label = "") {
if (it == State.PLACING) args.fromScale else args.toScale
}
return alpha to scale
}
val scaleAndAlpha: @Composable (idx: Int, columns: Int) -> Pair<Float, Float> = { idx, columns ->
scaleAndAlpha(
args = ScaleAndAlphaArgs(2f, 1f, 0f, 1f),
animation = tween(300, delayMillis = (idx / columns) * 100)
)
}
Можно попробовать изменить Gallery
на вот такую, чтоб отслеживать если элемент уже был показан пользователю
@Composable
private fun Gallery(
paddingValues: PaddingValues,
uiConfig: () -> List<String>
) {
val config: List<String> = uiConfig()
val columns = 2
// Remember a set of already animated indices
val animatedIndices = remember { mutableSetOf<Int>() }
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(config.size) { idx ->
val item: String = config[idx]
// Determine if the item should animate
val shouldAnimate = !animatedIndices.contains(idx)
// If it should animate, mark it as animated
if (shouldAnimate) {
animatedIndices.add(idx)
}
val (scale, alpha) = if (shouldAnimate) {
scaleAndAlpha(idx, columns)
} else {
1f to 1f // No animation
}
MyItem(
modifier = Modifier.graphicsLayer(alpha = alpha, scaleX = scale, scaleY = scale),
text = item
)
}
}
)
}
}
Но проблема в том, что рекомпозиция вызывается дважды для каждого элемента вот тут - items(config.size) { idx ->
, из за этого элемент появляется на экране как будто нет анимации
Что я пропускаю тут?
Ответы (1 шт):
На первой рекомпозиции items лямбды, когда shouldAnimate равно true, вызывается scaleAndAlpha. Это функция Compose, которая рекомпилируется на каждом кадре анимации и возвращает текущие значения scale и alpha при каждой рекомпозиции. Чтобы MyItem обновлялся соответствующим образом, вся лямбда items рекомпилируется при изменении scale и alpha.
Вторая рекомпозиция нежелательна, потому что теперь shouldAnimate установлено в false, и анимация, которая только что началась, полностью пропускается.
Простое решение – вынести scaleAndAlpha и MyItem в отдельный composable элемент, чтобы его рекомпозиции не зависели от shouldAnimate.
@Composable
private fun MyAnimatedItem(
shouldAnimate: Boolean,
idx: Int,
columns: Int,
item: String,
) {
val (scale, alpha) = if (shouldAnimate) {
scaleAndAlpha(idx, columns)
} else {
1f to 1f // No animation
}
MyItem(
modifier = Modifier.graphicsLayer(
alpha = alpha,
scaleX = scale,
scaleY = scale,
),
text = item,
)
}
Simply called like this (in addition I simplified it to use `itemsIndexed` instead of `items`):
itemsIndexed(config) { idx, item ->
// Determine if the item should animate
val shouldAnimate = !animatedIndices.contains(idx)
// If it should animate, mark it as animated
if (shouldAnimate) {
animatedIndices.add(idx)
}
MyAnimatedItem(shouldAnimate, idx, columns, item)
}
Теперь рекомпозиции, которые происходят из-за анимации, ограничены MyAnimatedItem
, лямбда itemsIndexed
не затрагивается и рекомпилируется только тогда, когда элемент прокручивается за пределы области видимости и возвращается обратно. И только тогда shouldAnimate
устанавливается в false
, как и предполагалось.