В этой статье я покажу вам, как создать анимацию кругового раскрытия, которую использует WhatsApp, с помощью Jetpack Compose.
1. Использование AnimatedVisibility
Моей первой попыткой было использование AnimatedVisibility для достижения чего-то подобного.
AnimatedVisibility( visible = visible, ) { BottomSheet() }
Этот код эквивалентен:
AnimatedVisibility( visible = visible, enter = expandVertically(), exit = shrinkVertically() ) { BottomSheet() }
Однако мне не удалось добиться кругового движения, которого я искал.
2. Использование GenericShape
Моя вторая попытка заключалась в том, чтобы использовать фигуру, к которой будет прикреплен мой composable. GenericShape позволяет мне написать любую форму, но нет ничего, что позволило бы мне нарисовать круг, поэтому я начал с прямоугольника.
GenericShape { size, direction -> val center = size.width / 2f this.addRect( Rect( left = center - center * it, top = center - center * it, right = center + center * it, bottom = size.height ) ) close() }
У меня получилось кое-что анимировать, но это все еще не круговое движение, которое я искал.
3. Использование addArc
Учитывая, что прямоугольник — это не то, что мне нужно, я попробовал использовать addArc для рисования дуги вокруг прямоугольника.
GenericShape { size, direction -> val centerV = size.height / 2f val centerH = size.width / 2f addArc( Rect( left = centerH - centerH * it * 2, top = size.height - size.height * it, right = centerH + centerH * it * 2, bottom = size.height + size.height * it ), 0f, -180f ) close() }
Результат был намного лучше, однако я не смог добиться полного раскрытия, поэтому часть компонента всегда оставалась обрезанной.
4. Использование cubicTo
Что если вместо обычной дуги использовать более «сложную»? Я попробовал использовать cubicTo, он использует кубическую кривую Безье. Она имеет 4 контрольные точки, что позволяет мне нарисовать что-то похожее на полукруг.
Если я реализую кубическую кривую Безье того же размера, что и композит, то верхние углы все равно будут обрезаны, поэтому мне нужен способ сделать так, чтобы кривая охватывала весь composable.
Для этого я сделал кривую в два раза больше, чем композит. Для оси x это делается путем умножения ширины на 2, для оси y я использую функцию lerp, которая переходит от height к -height.
GenericShape { size, direction -> val centerH = size.width / 2f moveTo( x = centerH - centerH * it * 2, y = size.height, ) val currentHeight = lerp(size.height, -size.height, it) cubicTo( x1 = centerH - centerH * it, y1 = currentHeight, x2 = centerH + centerH * it, y2 = currentHeight, x3 = centerH + centerH * it * 2, y3 = size.height, ) close() }
Результат получился довольно близким к тому, что я хотел, но кривая растет не пропорционально.
Я продолжал корректировать код, пока не пришел к чему-то, что было довольно близко к анимации, которую использует WhatsApp.
GenericShape { size, _ -> val centerH = size.width / 2f val multiplierW = 1.5f + size.height / size.width moveTo( x = centerH - centerH * progress * multiplierW, y = size.height, ) val currentWidth = (centerH * progress * multiplierW * 2.5f) cubicTo( x1 = centerH - centerH * progress * 1.5f, y1 = size.height - currentWidth * 0.5f, x2 = centerH + centerH * progress * 1.5f, y2 = size.height - currentWidth * 0.5f, x3 = centerH + centerH * progress * multiplierW, y3 = size.height, ) close() }
Бонус
Если вы снова посмотрите на анимацию WhatsApp, вы можете заметить, что предметы внутри карты также немного анимируются. Их масштаб в начале, вероятно, составляет 90%, затем он увеличивается до 110% и, наконец, уменьшается до 100%.
Чтобы реализовать это, я использовал animateFloatAsState.
var scale by remember { mutableStateOf(0.9f) } val animation = animateFloatAsState( targetValue = scale, animationSpec = FloatSpringSpec( dampingRatio = 0.3f, ) ) LaunchedEffect(Unit) { delay(20 + position.toLong() * 20) scale = 1f } Image( modifier = Modifier ... .graphicsLayer { scaleX = animation.value scaleY = animation.value } )
Это простая анимация, которая изменяется от 0.9 до 1 и использует пружинную анимацию. Я добавляю задержку 20 мс + еще одну задержку, основанную на позиции элемента, чтобы все не анимировалось одновременно. Если вы хотите, чтобы что-то появилось позже, просто задайте ему более высокую позицию.
Это не совсем похоже на то, что использует WhatsApp, но если вы можете продолжать настраивать задержку и значение dampingRatio, то, вероятно, получите то, что вас устраивает.
Исходники находятся здесь.
А оригинал статьи тут.