Настройте переход общего элемента

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

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

Чтобы изменить параметры анимации, используемые для изменения размера и положения, можно указать другой параметр 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 сначала измеряется макет дочернего элемента с учетом ограничений предварительного просмотра (или целевых значений). Затем стабильный макет дочернего элемента масштабируется, чтобы соответствовать общим границам. ScaleToBounds можно рассматривать как «графический масштаб» между состояниями.

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

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

Разница между двумя режимами изменения размера видна на следующих примерах:

ScaleToBounds

RemeasureToBounds

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

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

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

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

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" сохраняет свое конечное состояние в начале анимации.

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

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

Он будет отображаться поверх других несовпадающих элементов пользовательского интерфейса. После завершения перехода элемент будет перемещен из области наложения в собственную 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
)

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

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

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

Без использования Modifier.renderInSharedTransitionScopeOverlay()

С помощью Modifier.renderInSharedTransitionScopeOverlay()

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

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

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