Создадим захватывающую анимацию кругового вытеснения (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()
}
)
)

Вывод:

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Kappdev: How to Create a Clock Wipe Animation in Jetpack Compose

Предыдущая статьяРеволюция в условном рендеринге React 
Следующая статьяКак дуэт Angular-Wiz поменяет правила игры