Устранение эффекта мерцания при вращательной анимации элементов списка

Я работаю со списком из трех элементов, скажем [A, B, C], и пытаюсь создать с их помощью анимацию вращения. Последовательность этой анимации будет такой: AB, BC, CA, AB и т. д., повторяясь бесконечно. В списке также может быть n объектов, и я хочу, чтобы он вращался бесконечно.

Однако я столкнулся с проблемой обеспечения плавного перехода между элементами. Кажется, наблюдается заметный эффект «мигания», нарушающий плавность анимации. Мне нужен совет, как устранить эту проблему, или предложения по альтернативным методам достижения того же эффекта.

Желаемый эффект, к которому я стремлюсь, показан ниже. Но при наблюдении вы можете заметить, что анимация не является непрерывной и возникает определенный эффект «мигания».

Устранение эффекта мерцания при вращательной анимации элементов списка

@Composable
fun FollowEventCardHome() {
    val data = "{...}" 
    val eventData = Gson().fromJson(data, EventData::class.java)
    var currentEvent by remember { mutableStateOf(eventData.events?.getOrNull(0)) }
    var nextEvent by remember { mutableStateOf(eventData.events?.getOrNull(1)) }
    var currentIndex by remember { mutableIntStateOf(0) }
    var isAnimate by remember { mutableStateOf(true) }

    val configuration = LocalConfiguration.current
    val cardWidth = configuration.screenWidthDp.dp - 55.dp

    if (isAnimate) {
        StartEndInfo(
            nextEvent = nextEvent,
            currentEvent = currentEvent,
            isAnimate = true,
            isAnimationFinished = {
                isAnimate = false
                if (eventData.events?.size!! > 1) {
                    if (currentIndex == eventData.events?.size!! - 1) {
                        currentIndex = 0
                    } else {
                        currentIndex++
                    }
                }
                currentEvent = eventData.events?.getOrNull(currentIndex)
                nextEvent = if (currentIndex < eventData.events?.size!!) {
                    eventData.events?.getOrNull(0)
                } else {
                    eventData.events?.getOrNull(currentIndex + 1)
                }
            }
        )
    }

    if (!isAnimate){
        StartEndInfo(
            nextEvent = nextEvent,
            currentEvent = currentEvent,
            isAnimate = true,
            isAnimationFinished = {
                isAnimate = true
                if (eventData.events?.size!! > 1) {
                    if (currentIndex == eventData.events?.size!! - 1) {
                        currentIndex = 0
                    } else {
                        currentIndex++
                    }
                }
                currentEvent = eventData.events?.getOrNull(currentIndex)
                nextEvent = if (currentIndex < eventData.events?.size!!) {
                    eventData.events?.getOrNull(0)
                } else {
                    eventData.events?.getOrNull(currentIndex + 1)
                }
            }
        )
    }
}

@Composable
fun StartEndInfo(
    nextEvent: Event? = null,
    currentEvent: Event? = null,
    isAnimate: Boolean,
    isAnimationFinished: () -> Unit
) {
    Row(
        modifier = Modifier
            .padding(
                start = 16.dp,
                bottom = 16.dp
            )
            .fillMaxWidth()
    ) {
        Column(modifier = Modifier.fillMaxSize()) {
            StartEndWithProgress(
                nextEvent = nextEvent,
                currentEvent = currentEvent,
                isAnimate = isAnimate,
                isAnimationFinished = isAnimationFinished
            )
        }
    }
}

@Composable
fun StartEndWithProgress(
    nextEvent: Event?,
    currentEvent: Event?,
    isAnimate: Boolean,
    isAnimationFinished: () -> Unit
) {
    val scalePreviousEvent = remember { Animatable(1f) }
    val scaleCurrentEvent = remember { Animatable(0f ) }

    LaunchedEffect(isAnimate) {
        scalePreviousEvent.animateTo(
            targetValue = 0f,
            animationSpec = tween(durationMillis = 4000)
        )
    }

    LaunchedEffect(key1 = isAnimate) {
        scaleCurrentEvent.animateTo(
            targetValue = 1f,
            animationSpec = tween(durationMillis = 4000)
        )
    }

    if (isAnimate && scalePreviousEvent.value == 0f ||  !isAnimate && scalePreviousEvent.value == 1f){
        isAnimationFinished()
    }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 30.dp)
            .padding(top = 150.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box {
            currentEvent?.start?.locationCode.let {
                Text(
                    text = currentEvent?.start?.locationCode ?: "",
                    fontSize = 20.sp,
                    modifier = Modifier.graphicsLayer {
                        scaleX = 1f
                        scaleY = if (isAnimate) {
                            scaleCurrentEvent.value
                        } else 1f
                        transformOrigin = TransformOrigin(0.5f, 1f)
                    }
                )
                Text(
                    text = nextEvent?.start?.locationCode ?: "",
                    fontSize = 20.sp,
                    modifier = Modifier.graphicsLayer {
                        scaleX = 1f
                        scaleY = if (isAnimate) {
                            scalePreviousEvent.value
                        } else 0f
                        transformOrigin = TransformOrigin(0.5f, 0f)
                    }
                )
            }
        }
        Box {
            currentEvent?.end?.locationCode.let {
                Text(
                    text = currentEvent?.end?.locationCode ?: "",
                    fontSize = 20.sp,
                    modifier = Modifier.graphicsLayer {
                        scaleX = 1f
                        scaleY = if (isAnimate) { scaleCurrentEvent.value } else 1f
                        transformOrigin = TransformOrigin(0.5f, 1f)
                    }
                )
            }
            Text(
                text = nextEvent?.end?.locationCode ?: "",
                fontSize = 20.sp,
                modifier = Modifier.graphicsLayer {
                    scaleX = 1f
                    scaleY = if (isAnimate) {
                        scalePreviousEvent.value
                    } else 0f
                    transformOrigin = TransformOrigin(0.5f, 0f)
                }
            )
        }
    }
}

Пожалуйста, отредактируйте свой вопрос и сократите код до минимально воспроизводимого примера. Там много лишнего кода, который не компилируется из-за отсутствия зависимостей.

Leviathan 03.07.2024 15:36
3
1
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Не меняя вашего текущего подхода к использованию Animatable с graphicsLayer, я просто выделил соответствующие части в специальный компонуемый объект и немного его упростил:

@Composable
fun <T> AnimateRotatingList(
    list: List<T>,
    modifier: Modifier = Modifier,
    animationSpec: AnimationSpec<Float> = tween(durationMillis = 4000),
    itemContent: @Composable (item: T) -> Unit,
) {
    Box(modifier = modifier) {
        if (list.isEmpty()) return
        if (list.size == 1) {
            itemContent(list.first())
            return
        }

        var currentIndex by remember(list) { mutableIntStateOf(0) }
        val current = remember(list, currentIndex) { list[currentIndex] }
        val next = remember(list, currentIndex) { list[(currentIndex + 1) % list.size] }

        val scale = remember(list) { Animatable(1f) }

        LaunchedEffect(scale, animationSpec) {
            while (true) {
                scale.animateTo(
                    targetValue = 0f,
                    animationSpec = animationSpec,
                )
                scale.snapTo(1f)
                currentIndex = (currentIndex + 1) % list.size
            }
        }

        Box(modifier = Modifier.graphicsLayer {
            scaleY = scale.value
            transformOrigin = TransformOrigin(0.5f, 0f)
        }) {
            itemContent(current)
        }

        Box(modifier = Modifier.graphicsLayer {
            scaleY = 1 - scale.value
            transformOrigin = TransformOrigin(0.5f, 1f)
        }) {
            itemContent(next)
        }
    }
}

Я не знаю, где именно была ваша ошибка, но эта подчищенная версия теперь работает как положено:

@Composable
fun FollowEventCardHome() {
    val eventData: EventData = TODO()

    AnimateRotatingList(
        list = eventData.events.orEmpty(),
        modifier = Modifier
            .padding(top = 150.dp)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 30.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                text = it.start.locationCode,
                fontSize = 20.sp,
            )
            Text(
                text = it.end.locationCode,
                fontSize = 20.sp,
            )
        }
    }
}

Другие вопросы по теме