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

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

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

Чтобы изменить спецификацию анимации, используемую для изменения размера и положения, можно указать другой параметр 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 , так как это позволяет избежать перекомпоновки и переформатирования текста на разные строки. 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

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