В этой статье описан процесс создания захватывающего импульсного эффекта с использованием Jetpack Compose. Реализация такой анимации — отличный способ привлечь внимание пользователя.

Определение модификатора импульсного эффекта
Начнем с определения функции-модификатора pulseEffect
:
@Composable
fun Modifier.pulseEffect(
targetScale: Float = 1.5f,
initialScale: Float = 1f,
brush: Brush = SolidColor(Color.Black.copy(0.32f)),
shape: Shape = CircleShape,
animationSpec: DurationBasedAnimationSpec<Float> = tween(1200)
): Modifier {
// Реализация...
}
Параметры:
-
targetScale
— определяет масштаб, до которого будет расти импульсный эффект;
-
initialScale
— определяет начальный масштаб импульсного эффекта; -
brush
— кисть, используемая для заливки импульсного эффекта;
-
shape
— форма эффекта импульса; -
animationSpec
— спецификация анимации, управляющая эффектом.
Реализация импульсного эффекта
В этом разделе рассмотрим реализацию функции-модификатора pulseEffect
.
Анимация
Сначала определим бесконечный анимированный переход:
val pulseTransition = rememberInfiniteTransition("PulseTransition")
Используя этот переход, создадим две анимации:
● pulseScale
— анимацию масштаба (scale) эффекта: от initialScale
до targetScale
и непрерывное повторение:
val pulseScale by pulseTransition.animateFloat(
initialValue = initialScale,
targetValue = targetScale,
animationSpec = infiniteRepeatable(animationSpec),
label = "PulseScale"
)

● pulseAlpha
— анимацию прозрачности (alpha) эффекта, изменяя ее от 1f
до 0f
и повторяя непрерывно:
val pulseAlpha by pulseTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(animationSpec),
label = "PulseAlpha"
)

Отрисовка эффекта
Наконец, возвращаем модификатор this
и используем drawBehind
для отрисовки импульсного эффекта:
return this.drawBehind {
// Преобразование формы в Outline
val outline = shape.createOutline(size, layoutDirection, this)
// Применение масштаба
scale(pulseScale) {
// Отрисовка контура формы
drawOutline(outline, color, pulseAlpha)
}
}
Двойной импульсный эффект
Хотя модификатор pulseEffect
сам по себе поражает воображение, объединение двух импульсных эффектов в одну анимацию создает еще более захватывающее визуальное впечатление.
Для его создания воспользуемся дополнительным модификатором, который упрощает задачу и делает код чище:
@Composable
fun Modifier.doublePulseEffect(
targetScale: Float = 1.5f,
initialScale: Float = 1f,
brush: Brush = SolidColor(Color.Black.copy(0.32f)),
shape: Shape = CircleShape,
duration: Int = 1200,
): Modifier {
return this
.pulseEffect(
targetScale, initialScale, brush, shape,
animationSpec = tween(duration, easing = FastOutSlowInEasing)
)
.pulseEffect(
targetScale, initialScale, brush, shape,
animationSpec = tween(
durationMillis = (duration * 0.7f).toInt(),
delayMillis = (duration * 0.3f).toInt(),
easing = LinearEasing
)
)
}
Анимация успешно создана. Полный код (включая дополнительные перегрузки Color) можете найти на GitHub Gist. Осталось рассмотреть применение импульсного эффекта.
Случаи использования
В этом разделе рассмотрим три простых примера использования импульсного эффекта.
Этот эффект может демонстрировать неожиданное поведение при применении к интерактивным компонентам, например кнопкам (Buttons).
Проблема возникает из-за того, что эти компоненты используют модификатор minimumInteractiveComponentSize
. Если размер кнопки меньше 48.dp
, эффект все равно будет отрисован по размеру 48.dp
, что приведет к несоответствию размеров.
Решением является переопределение размера компонента путем применения пользовательского модификатора размера.
Использование по умолчанию
FilledIconButton(
onClick = { /*TODO*/ },
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = Color.Black,
contentColor = Color.White
),
modifier = Modifier.doublePulseEffect().size(42.dp)
) {
Icon(Icons.Rounded.Mic, null)
}

Градиент
FilledIconButton(
onClick = { /*TODO*/ },
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = Color.Black,
contentColor = Color.White
),
modifier = Modifier
.doublePulseEffect(
targetScale = 2f,
brush = Brush.radialGradient(
0.6f to Color.Yellow,
1f to Color.Magenta,
)
)
.size(42.dp)
) {
Icon(Icons.Rounded.Mic, null)
}

Кастомизированные форма и цвет
Button(
onClick = { /*TODO*/ },
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Blue,
contentColor = Color.White
),
modifier = Modifier
.doublePulseEffect(
brush = SolidColor(Color.Blue),
shape = RoundedCornerShape(16.dp)
)
.height(42.dp)
) {
Text("CLICK ME")
}

Бонусный лайфхак
Если хотите отобразить этот эффект опционально, можете использовать комбинацию переменной состояния и модификатора then
:
var showPulse by remember { mutableStateOf(true) }
Button(
/* Другие параметры */
onClick = { showPulse = !showPulse },
modifier = Modifier.then(
if (showPulse) Modifier.doublePulseEffect() else Modifier
)
) { /* Контент */ }
Читайте также:
- Как создать анимацию мерцающего текста в Jetpack Compose
- Как создать анимацию кругового вытеснения в Jetpack Compose
- Как создать анимированный переключатель тем в Jetpack Compose
Читайте нас в Telegram, VK и Дзен
Перевод статьи Kappdev: How to Create a Pulse Effect in Jetpack Compose