Irbisa · Шпаргалка

Анимации и жесты в Compose

animate*AsState, Transition, AnimatedContent, Animatable, pointerInput, drag, nestedScroll, Canvas

Middle-разработчик · 24 вопроса

  1. 01

    Как анимировать изменение размера composable, когда его содержимое растёт, например у раскрывающейся карточки?

    Добавьте контейнеру Modifier.animateContentSize(): когда его измеренный размер меняется, он плавно анимируется от старого к новому вместо скачка.

    @Composablefun ExpandableCard(title: String, details: String) {  var expanded by remember { mutableStateOf(false) }  Card(onClick = { expanded = !expanded }) {    Column(      Modifier…
  2. 02

    Как анимировать появление и исчезновение composable и как настроить анимацию входа и выхода?

    Оберните его в AnimatedVisibility(visible = ...): он компонует содержимое, когда visible равно true, проигрывает EnterTransition при появлении и ExitTransition при исчезновении, а по окончании выхода убирает содержимое из композиции.

    @Composablefun Details(details: String) {  var visible by remember { mutableStateOf(false) }  Column {    Button(onClick = { visible = !visible }) { Text("Toggle") }    AnimatedVisibility(…
  3. 03

    Как сделать бесконечно повторяющуюся анимацию, например пульсирующую точку или шиммер загрузки?

    Создайте rememberInfiniteTransition() и анимируйте на нём значения через animateFloat или animateColor со спецификацией infiniteRepeatable; анимация стартует, когда попадает в композицию, и останавливается, когда из неё уходит.

    @Composablefun PulsingDot(modifier: Modifier = Modifier) {  val transition = rememberInfiniteTransition(label = "pulse")  val scale by transition.animateFloat(    initialValue = 0.8f,    targetValue = 1.2f,…
  4. 04

    Счётчик ниже анимируется, но и уходящий, и появляющийся текст показывают новое число. Что не так?

    @Composablefun Total(count: Int) {  AnimatedContent(    targetState = count,    transitionSpec = {      slideInVertically { it } togetherWith slideOutVertically { -it }    },    label = "total",  ) {    Text("Total: $count")  }}

    Лямбда контента игнорирует свой параметр и читает захваченный count, поэтому уходящая копия тоже рекомпозируется с новым значением.

  5. 05

    Когда для обработки нажатий использовать clickable, combinedClickable или pointerInput { detectTapGestures }?

    Для обычного нажатия подходит clickable, а если нужны ещё долгое нажатие или двойной тап, то combinedClickable; к detectTapGestures переходите, только когда тап не является «кликом», например когда важна позиция нажатия на графике.

    // a post: tap opens, double tap likes, long press shows the menuBox(  Modifier    .clip(MaterialTheme.shapes.medium)   // clip first, so the ripple is rounded    .combinedClickable(      onClick = onOpen,…
  6. 06

    Как рисовать произвольную графику в Compose и чем отличаются Canvas, drawBehind и drawWithContent?

    Все три дают DrawScope с drawCircle, drawLine, drawArc, drawPath и прочими: Canvas служит самостоятельным пустым элементом, drawBehind рисует под существующим composable, а drawWithContent позволяет рисовать поверх его детей.

    @Composablefun ProgressRing(progress: () -> Float, modifier: Modifier = Modifier) {  val track = MaterialTheme.colorScheme.surfaceVariant  val fill = MaterialTheme.colorScheme.primary  Canvas(modifier.size(64.dp)) {   // progress() is read inside the draw lambda: redraw only    val stroke = Stroke(width = 6.dp.toPx(), cap = StrokeCap.Round)…
  7. 07

    Чем отличаются animate*AsState, updateTransition и Animatable и когда что выбирать?

    animate*AsState анимирует одно значение к цели; updateTransition ведёт несколько значений от одного состояния, чтобы они оставались синхронными; Animatable даёт императивное значение на корутинах, когда анимацией должен управлять жест или событие.

  8. 08

    Версии A и B анимируют одни и те же два Animatable. Сколько длится каждая и в каком порядке меняются значения?

    val alpha = remember { Animatable(0f) }val offsetY = remember { Animatable(40f) } // version ALaunchedEffect(Unit) {  alpha.animateTo(1f, tween(300))  offsetY.animateTo(0f, tween(300))} // version BLaunchedEffect(Unit) {  launch { alpha.animateTo(1f, tween(300)) }  launch { offsetY.animateTo(0f, tween(300)) }}

    A выполняет их друг за другом (сначала alpha, затем offsetY) и длится 600 мс, потому что animateTo является suspend-функцией, которая возвращается только по окончании анимации.

  9. 09

    Найдите баг: при каждом изменении progress эта полоска падает до нуля и заполняется заново, вместо того чтобы продолжить с текущей ширины.

    @Composablefun ProgressBar(progress: Float, modifier: Modifier = Modifier) {  val animated = Animatable(0f)  LaunchedEffect(progress) {    animated.animateTo(progress, animationSpec = tween(500))  }  Box(    modifier      .fillMaxWidth()      .height(8.dp)      .drawBehind {        drawRect(Color(0xFF3D7BFF), size = size.copy(width = size.width * animated.value))      }  )}

    Animatable(0f) создаётся в теле composable без remember, поэтому каждая рекомпозиция строит новый экземпляр со значением 0; перезапущенный LaunchedEffect анимирует уже его с нуля, а прежнее значение теряется.

  10. 10

    Когда выбирать spring, tween или keyframes как спецификацию анимации и зачем нужен visibilityThreshold?

    spring подходит для движения, которое могут прервать или которое следует за жестом: он переносит текущую скорость в новую цель; tween берут, когда дизайнер задал точную длительность и кривую; keyframes нужен для многошагового движения, например с перелётом.

    // exact curve from the design specval alpha by animateFloatAsState(  targetValue = if (visible) 1f else 0f,  animationSpec = tween(300, easing = CubicBezierEasing(0.2f, 0f, 0f, 1f)),  label = "alpha",)…
  11. 11

    Как анимировать добавление, удаление и перестановку элементов в LazyColumn?

    Добавьте Modifier.animateItem() на корень каждого элемента и задайте стабильные key; тогда новые элементы проявляются, удалённые исчезают, а переставленные плавно едут на новое место.

    @Composablefun TaskList(tasks: List<Task>, onToggle: (String) -> Unit) {  LazyColumn {    items(tasks, key = { it.id }) { task ->      // stable, unique key      TaskRow(        task = task,…
  12. 12

    Как работают shared element transitions в Compose и как подключить их к Navigation Compose?

    Оберните оба экрана в один SharedTransitionLayout, пометьте совпадающие элементы Modifier.sharedElement с одинаковым ключом и передайте каждому AnimatedVisibilityScope перехода, который меняет экраны; в NavHost это receiver лямбды composable { }.

    @Serializable object Covers@Serializable data class Detail(val id: String) @OptIn(ExperimentalSharedTransitionApi::class)@Composablefun App(nav: NavHostController) {…
  13. 13

    Как Compose учитывает системную настройку «Удалить анимации» и что всё равно нужно обработать самому?

    Compose масштабирует анимации по системному коэффициенту длительности анимаций, поэтому при 0 анимация, запущенная из композиции, сразу перескакивает в конец; самим нужно обработать зацикленное движение и анимации, которые управляются извне композиции.

  14. 14

    Что делает Modifier.graphicsLayer и почему через него лучше всего анимировать прозрачность, масштаб и поворот?

    graphicsLayer рисует composable в отдельный слой рендеринга со свойствами трансформации (alpha, scaleX/Y, rotationZ, translationX/Y, shadowElevation, clip); их изменение перерисовывает слой без рекомпозиции и повторного измерения контента.

    @Composablefun FlipCard(flipped: Boolean, front: @Composable () -> Unit, back: @Composable () -> Unit) {  val rotation by animateFloatAsState(if (flipped) 180f else 0f, label = "flip")  val showBack by remember { derivedStateOf { rotation > 90f } }  Box(    Modifier.graphicsLayer {              // rotation is read here, in the draw phase…
  15. 15

    Как написать собственный жест через Modifier.pointerInput и за что отвечают его ключи?

    pointerInput(key) { } запускает suspend-блок, в котором вы ждёте события указателя (через хелперы вроде detectDragGestures или вручную через awaitEachGesture и awaitFirstDown); блок перезапускается только при смене ключа.

    @Composablefun VolumeStrip(onChange: (Float) -> Unit, modifier: Modifier = Modifier) {  val change by rememberUpdatedState(onChange)   // always the latest callback  Box(    modifier      .size(48.dp, 200.dp)…
  16. 16

    Найдите баг: при перетаскивании этот чип убегает вперёд пальца, и разрыв растёт тем больше, чем дальше тянуть.

    @Composablefun DraggableChip(label: String) {  var offsetX by remember { mutableFloatStateOf(0f) }  Text(    text = label,    modifier = Modifier      .offset(x = offsetX.dp)      .pointerInput(Unit) {        detectHorizontalDragGestures { change, dragAmount ->          change.consume()          offsetX += dragAmount        }      }  )}

    Смещения жеста приходят в пикселях, а offsetX.dp считает накопленную сумму пикселей в dp, поэтому чип сдвигается в density раз дальше пальца (в 2,75 раза на типичном телефоне с 440 dpi); правильно это выглядит только на экране с плотностью 1,0.

  17. 17

    Ручка перетаскивания на detectDragGestures внутри строки LazyColumn блокирует прокрутку списка. Как Compose решает, какой жест побеждает?

    Жест принадлежит первому узлу, который вызвал consume() для изменения указателя.

  18. 18

    Как реализовать pinch-to-zoom и перемещение картинки в Compose?

    Используйте Modifier.transformable с rememberTransformableState или detectTransformGestures внутри pointerInput; оба на каждое событие отдают изменения масштаба, сдвига и поворота, которые вы накапливаете в стейте и применяете через graphicsLayer.

    @Composablefun ZoomableImage(painter: Painter, modifier: Modifier = Modifier) {  var scale by remember { mutableFloatStateOf(1f) }  var offset by remember { mutableStateOf(Offset.Zero) }  val state = rememberTransformableState { zoomChange, panChange, _ ->    scale = (scale * zoomChange).coerceIn(1f, 5f)…
  19. 19

    Как сделать строку со свайпом для удаления или любой перетаскиваемый элемент, который защёлкивается в фиксированных позициях?

    Для стандартного случая берите SwipeToDismissBox из Material 3; для своих точек фиксации используйте AnchoredDraggableState с DraggableAnchors и Modifier.anchoredDraggable, а сдвигать контент по его смещению нужно самим.

    enum class Swipe { Settled, Dismissed }@Composablefun SwipeToDeleteRow(item: Item, onDelete: () -> Unit) {  val state = remember { AnchoredDraggableState(initialValue = Swipe.Settled) }  LaunchedEffect(state.settledValue) {            // act when the animation lands    if (state.settledValue == Swipe.Dismissed) onDelete()…
  20. 20

    Ваша отрисовка создаёт Path и Brush на каждом кадре внутри drawBehind. Что меняет drawWithCache?

    drawWithCache делит отрисовку на две части: внешний блок выполняется только при смене размера или прочитанного в нём стейта, а возвращаемый им блок onDrawBehind или onDrawWithContent выполняется на каждом кадре, поэтому дорогие объекты создаются один раз и переиспользуются.

    @Composablefun Gauge(progress: Float, modifier: Modifier = Modifier) {  val sweep by animateFloatAsState(progress * 270f, label = "sweep")  Box(    modifier      .size(160.dp)…
  21. 21

    Как сделать, чтобы перетаскиваемый элемент после отпускания продолжал двигаться со скоростью пальца, и что для этого нужно Animatable во время и после жеста?

    Во время перетаскивания делайте snapTo в позицию пальца, чтобы ничего не анимировалось; при отпускании передайте измеренную скорость в animateDecay, чтобы элемент катился по инерции, или в animateTo(target, initialVelocity = v), чтобы пружиной вернуться в точку покоя, не потеряв бросок.

    @Composablefun FlingCard(modifier: Modifier = Modifier) {  val offsetX = remember { Animatable(0f) }  val scope = rememberCoroutineScope()  val decay = rememberSplineBasedDecay<Float>()…
  22. 22

    Как работает вложенная прокрутка (nested scroll) в Compose и как сделать сворачивающийся заголовок через NestedScrollConnection?

    Дельта прокрутки сначала идёт в onPreScroll родителей, начиная с внешнего, затем в прокручиваемый дочерний элемент, а остаток возвращается через onPostScroll; сворачивающийся заголовок сжимается в pre-scroll и разворачивается в post-scroll, когда список уже наверху.

    class CollapsingHeader(private val maxPx: Float) : NestedScrollConnection {  var offset by mutableFloatStateOf(0f)          // -maxPx..0f    private set  // finger up: the header shrinks BEFORE the list scrolls  override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {    if (available.y >= 0f) return Offset.Zero…
  23. 23

    Как анимировать изменение раскладки, при котором элементы переезжают на новые позиции, например переключение списка в сетку из двух колонок?

    Оставьте один лейаут, у которого меняются параметры, оберните его в LookaheadScope и повесьте на каждый элемент Modifier.animateBounds(lookaheadScope): lookahead-проход вычисляет, куда едет каждый элемент, и тот анимирует размер и позицию к этой цели.

    @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalLayoutApi::class)@Composablefun Gallery(items: List<Item>, grid: Boolean) {  LookaheadScope {    FlowRow(      Modifier.animateContentSize(),                        // the container size changes too…
  24. 24

    Как размыть composable или применить собственный шейдер и что показывать на версиях Android, где это недоступно?

    Modifier.blur работает только на Android 12 (API 31) и новее, а AGSL RuntimeShader работает только на Android 13 (API 33); ниже размытие молча ничего не делает, поэтому сначала спроектируйте запасной вариант (градиент или полупрозрачную подложку), а эффект добавляйте поверх.

    @Composablefun CoverImage(modifier: Modifier = Modifier) {  // fallback decided in one place  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {   // API 31+    Box(modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded)) { Cover() }  } else {…