Программирование
Круговое раскрытие как в WhatsApp с помощью Jetpack Compose
Несмотря на экономическую ситуацию и быстро меняющийся рынок.
В этой статье я покажу вам, как создать анимацию кругового раскрытия, которую использует WhatsApp, с помощью Jetpack Compose.
1. Использование AnimatedVisibility
Моей первой попыткой было использование AnimatedVisibility для достижения чего-то подобного.
xxxxxxxxxx
AnimatedVisibility(
visible = visible,
) {
BottomSheet()
}
Этот код эквивалентен:
xxxxxxxxxx
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically()
) {
BottomSheet()
}
Однако мне не удалось добиться кругового движения, которого я искал.
2. Использование GenericShape
Моя вторая попытка заключалась в том, чтобы использовать фигуру, к которой будет прикреплен мой composable. GenericShape позволяет мне написать любую форму, но нет ничего, что позволило бы мне нарисовать круг, поэтому я начал с прямоугольника.
xxxxxxxxxx
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 для рисования дуги вокруг прямоугольника.
xxxxxxxxxx
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.
xxxxxxxxxx
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.
xxxxxxxxxx
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.
xxxxxxxxxx
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, то, вероятно, получите то, что вас устраивает.
Исходники находятся здесь.
А оригинал статьи тут.
-
Новости2 недели назад
Видео и подкасты о мобильной разработке 2025.14
-
Видео и подкасты для разработчиков4 недели назад
Javascript для бэкенда – отличная идея: Node.js, NPM, Typescript
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.12
-
Разработка3 недели назад
«Давайте просто…»: системные идеи, которые звучат хорошо, но почти никогда не работают