Создадим захватывающую анимацию кругового вытеснения (clock wipe). Мы будем использовать Jetpack Compose, гибкая настройка которого позволяет применить этот анимационный эффект к любому представлению.

Фигура кругового вытеснения
Идея заключается в том, чтобы создать пользовательскую фигуру (shape), представляющую сектор, похожий на кусок пирога, который можно использовать для обрезки composable-представления.
Итак, начнем с определения и реализации класса ClockWipeShape
:
class ClockWipeShape(
private val progress: Float,
private val startAngle: Float = -90f,
private val isClockwise: Boolean = true
) : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
// Расчет необходимых параметров для дуги
val targetAngle = if (isClockwise) -360f else 360f
val arcFraction = 1f - progress.coerceIn(0f, 1f)
val sweepAngle = targetAngle * arcFraction
val diagonal = sqrt(size.width.pow(2) + size.height.pow(2))
val arcRect = Rect(size.center, diagonal / 2)
// Создание пути для эффекта кругового вытеснения
val path = Path().apply {
if (arcFraction != 1f) {
// Отрисовка дуги, представляющей вытеснение
arcTo(arcRect, startAngle, sweepAngle, forceMoveTo = true)
// Отрисовка линии к центру для завершения сектора
lineTo(size.center.x, size.center.y)
close() // Закрытие пути для формирование фигуры, похожей на пирог
} else {
// Отрисовка всего прямоугольника (при условии полной видимости)
addRect(size.toRect())
}
}
// Возврат окончательного контура
return Outline.Generic(path)
}
}

Модификатор кругового вытеснения (clockWipe)
Теперь, создав ClockWipeShape
, можно обрезать любое представление с помощью этой фигуры.
Чтобы сделать этот процесс более удобным, создадим пользовательский Modifier
(модификатор):
fun Modifier.clockWipe(
progress: Float,
startAngle: Float = -90f,
isClockwise: Boolean = true
) = this.clip(ClockWipeShape(progress, startAngle, isClockwise))
Модификатор анимации кругового вытеснения (clockWipeAnimation)
Чтобы обрезать представление с помощью модификатора clockWipe
, придется вручную управлять прогрессом (progress
).
Такой подход может быть полезен, если вы хотите сами управлять анимацией, например, с помощью ползунка.
Однако рассмотрим автоматизированный вариант воспроизведения анимации:
@Composable
fun Modifier.clockWipeAnimation(
// Управляет тем, является ли содержимое видимым (true) или скрытым с помощью вытесенения (false).
isVisible: Boolean,
startAngle: Float = -90f,
isClockwise: Boolean = true,
animationSpec: AnimationSpec<Float> = tween(1000),
onFinish: ((isVisible: Boolean) -> Unit)? = null
): Modifier {
// Анимируйте прогресс в зависимости от видимости: 0f - когда он виден, 1f - когда скрыт
val progress by animateFloatAsState(
targetValue = if (isVisible) 0f else 1f,
animationSpec = animationSpec,
label = "ClockWipeProgress",
finishedListener = { animatedValue ->
// Инициирование обратного вызова onFinish при завершении анимации
onFinish?.invoke(animatedValue == 0f)
}
)
// Применение эффекта кругового вытеснения
return this.clockWipe(progress, startAngle, isClockwise)
}
Анимация успешно создана. Полный код можете найти на GitHub Gist. Теперь изучим возможности использования.
Видео о написании кода смотрите здесь.
Случаи использования
В этом разделе рассмотрим два случая: ручной (с использованием clockWipe
) и автоматизированный (с использованием clockWipeAnimation
).
Ручной
var progress by remember { mutableFloatStateOf(0f) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(50.dp)
) {
Image(
painter = painterResource(R.drawable.cat),
contentDescription = "CatImage",
modifier = Modifier.clockWipe(progress)
)
Slider(
value = progress,
onValueChange = { progress = it },
modifier = Modifier.fillMaxWidth(0.8f)
)
}
Вывод:

Автоматизированный
val context = LocalContext.current
var isVisible by remember { mutableStateOf(true) }
Image(
painter = painterResource(R.drawable.cat),
contentDescription = "CatImage",
modifier = Modifier
.clickable {
isVisible = !isVisible // Переключение видимости по клику
}
.clockWipeAnimation(
isVisible = isVisible,
onFinish = { isContentVisible ->
// Показывать toast-сообщение в зависимости от видимости после завершения анимации
val message = if (isContentVisible) "The Content is Visible" else "The Content is Hidden"
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
)
)
Вывод:

Читайте также:
- Реализация «бесконечного» пейджера в Jetpack Compose
- Как создать загрузчик с вращающимися кругами в Jetpack Compose
- Поддержка новых форм-факторов с помощью новой библиотеки Jetpack WindowManager
Читайте нас в Telegram, VK и Дзен
Перевод статьи Kappdev: How to Create a Clock Wipe Animation in Jetpack Compose