Реализация кастомных анимаций Page Transition в мобильном приложении
Стандартный slide-from-right на iOS и fade на Android уже не воспринимаются как что-то особенное — это минимальный уровень. Проблемы начинаются, когда навигация должна передавать смысл: карточка разворачивается в полноэкранный детейл, фото из сетки «перелетает» в галерею, экран настроек выезжает снизу с затемнением родительского контента. Каждый из этих переходов требует отдельного подхода.
Shared Element Transitions — самое сложное и самое ценное
Переход, когда элемент с одного экрана «перелетает» на другой — не перерисовывается, а именно перелетает — это Shared Element Transition (iOS называет это Hero Animation).
На iOS реализуем через кастомный UIViewControllerAnimatedTransitioning. Ключевой момент: элемент-источник нужно «снять» в containerView через snapshotView(afterScreenUpdates: false), скрыть оригинал, анимировать снапшот по нужной траектории и в конце скрыть снапшот, показав destination-элемент. Если пропустить afterScreenUpdates: false — получаем белый прямоугольник вместо снапшота на первых кадрах.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let snapshot = sourceView.snapshotView(afterScreenUpdates: false) else { return }
snapshot.frame = sourceFrame
containerView.addSubview(destinationVC.view)
containerView.addSubview(snapshot)
sourceView.isHidden = true
UIView.animate(withDuration: duration, delay: 0,
usingSpringWithDamping: 0.85, initialSpringVelocity: 0.3) {
snapshot.frame = destinationFrame
} completion: { _ in
snapshot.removeFromSuperview()
self.sourceView.isHidden = false
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
На Android — SharedElementCallback + ActivityOptions.makeSceneTransitionAnimation или Jetpack Compose SharedTransitionLayout с Modifier.sharedElement. Compose-подход значительно проще и не требует XML-разметки transition-сцен.
В Flutter — Hero виджет для простых случаев, flutter_animate или кастомный PageRouteBuilder для сложных. Hero ограничен: он не работает корректно с ListView внутри NestedScrollView и иногда клипает элемент на переходе — тогда спасает flightShuttleBuilder.
Кастомные переходы без shared elements
Для экранов без общих элементов реализуем кастомные PageRoute:
-
Expand из точки — экран «вырастает» из нажатой кнопки через
ClipOval→ClipRRect→ полный экран. Используется для action-кнопок (FAB). -
Depth transition — текущий экран уменьшается и уходит вглубь, новый надвигается. Достигается через
transform: Matrix4.translationValues+scale. -
Stagger reveal — элементы нового экрана появляются последовательно с задержкой через
AnimationStaggered. Хорошо работает для списков и dashboard-экранов.
Интерактивные (прерываемые) переходы
Переход, который можно прервать свайпом назад на середине — это UIPercentDrivenInteractiveTransition на iOS и BackHandler + AnimationController в Flutter. Важно: анимация должна «отпружинивать» обратно при незавершённом жесте, а не просто обрываться. Для этого completionCurve должна быть .easeOut, а не .linear.
Процесс работы
Начинаем с Figma-прототипа или описания переходов. Определяем тип: shared element, expand, custom route. Реализуем под целевую платформу. Тестируем прерываемость жестом (если нужно) и поведение в accessibility-режиме Reduce Motion — там анимации должны быть заменены на простые fade.
Срок: 1–3 дня на переход, зависит от сложности и числа экранов.







