Как настроить переход между общими элементами

Чтобы настроить анимацию перехода общего элемента, можно использовать несколько параметров, которые позволяют изменить способ перехода общих элементов.

Спецификация анимации

Чтобы изменить спецификацию анимации, используемую для движения размера и положения, можно указать другой параметр boundsTransform в Modifier.sharedElement(). Это позволяет определить начальную Rect и целевую позиции Rect.

Например, чтобы текст из предыдущего примера двигался по дуге, укажите для параметра boundsTransform спецификацию keyframes:

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

Вы можете использовать любой AnimationSpec. В этом примере используется спецификация keyframes.

Рисунок 1. Пример с разными параметрами boundsTransform

Режим изменения размера

При анимации между двумя общими границами можно задать для параметра resizeMode значение RemeasureToBounds или ScaleToBounds. Этот параметр определяет, как общий элемент переходит из одного состояния в другое. ScaleToBounds сначала измеряет дочерний макет с помощью ограничений lookahead (или целевых ограничений). Затем стабильная разметка дочернего элемента масштабируется, чтобы поместиться в общих границах. ScaleToBounds можно рассматривать как "графическую шкалу" между состояниями.

В отличие от этого, RemeasureToBounds повторно измеряет и перекомпоновывает дочерний макет sharedBounds с анимированными фиксированными ограничениями на основе целевого размера. Повторное измерение запускается при изменении размера границ, которое может происходить каждый кадр.

Для композиций Text рекомендуется использовать ScaleToBounds, поскольку это позволяет избежать перекомпоновки и переноса текста на другие строки. RemeasureToBounds рекомендуется использовать для границ с разными соотношениями сторон, а также если вы хотите обеспечить плавный переход между двумя общими элементами.

Разница между двумя режимами изменения размера показана в примерах ниже.

ScaleToBounds

RemeasureToBounds

Динамическое включение и отключение общих элементов

По умолчанию sharedElement() и sharedBounds() настроены на анимацию изменений макета при обнаружении соответствующего ключа в целевом состоянии. Однако вы можете динамически отключать эту анимацию в зависимости от определенных условий, например направления навигации или текущего состояния интерфейса.

Чтобы управлять переходом общего элемента, можно настроить параметр SharedContentConfig, передаваемый в rememberSharedContentState(). Свойство isEnabled определяет, активен ли общий элемент.

В примере ниже показано, как задать конфигурацию, в которой общий переход будет включен только при переходе между определенными экранами (например, только с экрана А на экран Б), а для остальных экранов будет отключен.

SharedTransitionLayout {
    val transition = updateTransition(currentState)
    transition.AnimatedContent { targetState ->
        // Create the configuration that depends on state changing.
        fun animationConfig() : SharedTransitionScope.SharedContentConfig {
            return object : SharedTransitionScope.SharedContentConfig {
                override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
                    // For this example, we only enable the transition in one direction
                    // from A -> B and not the other way around.
                    get() =
                        transition.currentState == "A" && transition.targetState == "B"
            }
        }
        when (targetState) {
            "A" -> Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = "shared_box",
                            config = animationConfig()
                        ),
                        animatedVisibilityScope = this
                    )
                    // ...
            ) {
                // Your content
            }
            "B" -> {
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = "shared_box",
                                config = animationConfig()
                            ),
                            animatedVisibilityScope = this
                        )
                        // ...
                ) {
                    // Your content
                }
            }
        }
    }
}

По умолчанию, если общий элемент отключен во время анимации, она все равно завершается, чтобы предотвратить случайное удаление выполняющихся анимаций. Если вам нужно удалить элемент во время анимации, вы можете переопределить shouldKeepEnabledForOngoingAnimation в интерфейсе SharedContentConfig, чтобы вернуть значение false.

Перейти к финальной разметке

По умолчанию при переходе между двумя макетами размер макета анимируется между начальным и конечным состоянием. Это может быть нежелательно при анимации контента, например текста.

В примере ниже показано, как текст описания "Lorem Ipsum" появляется на экране двумя разными способами. В первом примере текст перекомпоновывается по мере того, как контейнер увеличивается в размере. Во втором примере текст не переносится при увеличении. Добавление Modifier.skipToLookaheadSize() предотвращает перекомпоновку по мере роста.

Нет Modifier.skipToLookaheadSize() – обратите внимание на перенос текста Lorem Ipsum

Modifier.skipToLookaheadSize() – обратите внимание, что текст "Lorem Ipsum" сохраняет свое конечное состояние в начале анимации.

Клипы и наложения

Чтобы общие элементы могли использоваться в разных composable-функциях, отрисовка composable-функции выполняется в виде наложения слоя, когда начинается переход к соответствующему элементу в целевом компоненте. В результате он выйдет за границы родительского элемента и его преобразования слоя (например, альфа-канал и масштаб).

Он будет отображаться поверх других элементов интерфейса, которые не являются общими. После этого элемент будет перемещен из оверлея в отдельный DrawScope.

Чтобы обрезать общий элемент по форме, используйте стандартную функцию Modifier.clip(). Разместите его после sharedElement():

Image(
    painter = painterResource(id = R.drawable.cupcake),
    contentDescription = "Cupcake",
    modifier = Modifier
        .size(100.dp)
        .sharedElement(
            rememberSharedContentState(key = "image"),
            animatedVisibilityScope = this@AnimatedContent
        )
        .clip(RoundedCornerShape(16.dp)),
    contentScale = ContentScale.Crop
)

Если вам нужно, чтобы общий элемент никогда не отображался за пределами родительского контейнера, задайте для sharedElement() значение clipInOverlayDuringTransition. По умолчанию для вложенных общих границ clipInOverlayDuringTransition использует путь отсечения из родительского элемента sharedBounds().

Чтобы во время перехода между общими элементами определенные элементы интерфейса, например нижняя панель или плавающая кнопка действия, всегда оставались поверх других, используйте Modifier.renderInSharedTransitionScopeOverlay(). По умолчанию этот модификатор сохраняет контент в оверлее, пока активен общий переход.

Например, в Jetsnack элемент BottomAppBar должен быть расположен поверх общего элемента до тех пор, пока экран не станет видимым. Добавление модификатора к composable-функции позволяет ей оставаться на том же уровне.

Без Modifier.renderInSharedTransitionScopeOverlay()

Билеты: Modifier.renderInSharedTransitionScopeOverlay()

Возможно, вы захотите, чтобы неиспользуемая composable-функция исчезала с экрана, а не оставалась поверх других composable-функций до перехода. В таких случаях используйте renderInSharedTransitionScopeOverlay().animateEnterExit(), чтобы анимировать исчезновение элемента Composable во время перехода общего элемента:

JetsnackBottomBar(
    modifier = Modifier
        .renderInSharedTransitionScopeOverlay(
            zIndexInOverlay = 1f,
        )
        .animateEnterExit(
            enter = fadeIn() + slideInVertically {
                it
            },
            exit = fadeOut() + slideOutVertically {
                it
            }
        )
)

Рисунок 2. Нижняя панель приложения, которая появляется и исчезает при переходе между анимациями.

В редких случаях, когда вы не хотите, чтобы общий элемент отображался в оверлее, вы можете задать для параметра renderInOverlayDuringTransition в элементе sharedElement() значение false.

Уведомлять соседние макеты об изменении размера общего элемента

По умолчанию sharedBounds() и sharedElement() не уведомляют родительский контейнер об изменении размера при переходе к другому макету.

Чтобы изменения размера передавались в родительский контейнер при переходе, измените параметр placeholderSize на PlaceholderSize.AnimatedSize. При этом размер объекта будет меняться. Все остальные элементы макета реагируют на изменение.

PlaceholderSize.ContentSize (по умолчанию)

PlaceholderSize.AnimatedSize

(Обратите внимание, как другие элементы списка смещаются вниз, когда один из них увеличивается.)