Реализация Shared Element Transition в Android-приложении
Shared Element Transition в Android — механизм, при котором View с одного экрана визуально «переезжает» на другой. Список новостей, карточка с изображением — пользователь нажимает, и изображение плавно расширяется до полноэкранного детального вида. Не исчезает и появляется, а перемещается с анимацией формы, размера и позиции.
Доступно через Fragment Transitions API (традиционный подход) и через Compose SharedTransitionLayout (современный).
Fragment Transitions: Activity и Fragment
Между Activity:
// Из ListActivity при клике на элемент:
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("productId", product.id)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
imageView, // view на текущем экране
"product_image_transition" // transitionName — совпадает на обоих экранах
)
startActivity(intent, options.toBundle())
// В DetailActivity:
ViewCompat.setTransitionName(detailImageView, "product_image_transition")
// postponeEnterTransition() + startPostponedEnterTransition() если изображение загружается асинхронно
Если изображение загружается через Glide или Coil — нужен postponeEnterTransition() до начала загрузки и startPostponedEnterTransition() в callback загрузки. Иначе анимация стартует до появления изображения, и transition работает с placeholder.
// В DetailActivity.onCreate():
postponeEnterTransition()
Glide.with(this)
.load(imageUrl)
.listener(object : RequestListener<Drawable> {
override fun onResourceReady(...): Boolean {
startPostponedEnterTransition()
return false
}
override fun onLoadFailed(...): Boolean {
startPostponedEnterTransition() // обязательно и при ошибке
return false
}
})
.into(detailImageView)
Между Fragment через Navigation Component:
// В ListFragment:
val extras = FragmentNavigatorExtras(
imageView to "product_image_transition"
)
findNavController().navigate(
R.id.action_list_to_detail,
bundleOf("productId" to product.id),
null,
extras
)
// В DetailFragment.onCreate():
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
postponeEnterTransition()
android.R.transition.move — стандартный transition, включает изменение bounds, translation и clip bounds. Для кастомного поведения — TransitionSet с ChangeBounds, ChangeImageTransform, ChangeClipBounds.
Возврат назад: Shared Element автоматически анимирует return transition при popBackStack() или кнопке назад. sharedElementReturnTransition можно настроить отдельно — например, другой easing для обратного пути.
Jetpack Compose: SharedTransitionLayout
Compose 1.7+ (stable) принёс SharedTransitionLayout и SharedTransitionScope:
SharedTransitionLayout {
NavHost(navController, startDestination = "list") {
composable("list") {
AnimatedVisibility(visible = true) {
ProductList(
onProductClick = { product ->
navController.navigate("detail/${product.id}")
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@AnimatedVisibility
)
}
}
composable("detail/{id}") { backStackEntry ->
AnimatedVisibility(visible = true) {
ProductDetail(
productId = backStackEntry.arguments?.getString("id"),
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@AnimatedVisibility
)
}
}
}
}
// В ProductList:
@Composable
fun ProductCard(
product: Product,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
) {
with(sharedTransitionScope) {
AsyncImage(
model = product.imageUrl,
modifier = Modifier.sharedElement(
rememberSharedContentState(key = "product-image-${product.id}"),
animatedVisibilityScope
)
)
}
}
Ключ (key) должен совпадать на обоих экранах. sharedElement обрабатывает геометрический переход, sharedBounds — когда контейнер тоже должен плавно изменить размер.
Типичные ошибки
transitionName задан только на одном экране — transition не работает молча. Всегда проверяй оба направления.
RecyclerView с множеством элементов: setTransitionName нужно вызывать в onBindViewHolder с уникальным именем для каждого item (например, "product_image_${product.id}"). Если использовать одинаковое имя — Android не знает, какой именно view анимировать.
Shared Element поверх системной навигации (edge-to-edge): если view находится близко к краю и content за navigation bar — WindowInsetsCompat может смещать layout, и финальная позиция transition отличается от реальной. Решается через правильное применение insets через ViewCompat.setOnApplyWindowInsetsListener.
Сроки
Shared Element Transition между двумя экранами (изображение + заголовок): 1 день. С поддержкой асинхронной загрузки изображений, кастомным transition set и обратной анимацией: 1–2 дня. Стоимость рассчитывается индивидуально.







