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

Создано Kappdev

Определение модификатора импульсного эффекта

Начнем с определения функции-модификатора 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"
)
Создано Kappdev

●  pulseAlpha — анимацию прозрачности (alpha) эффекта, изменяя ее от 1f до 0f и повторяя непрерывно:

val pulseAlpha by pulseTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(animationSpec),
label = "PulseAlpha"
)
Создано Kappdev

Отрисовка эффекта

Наконец, возвращаем модификатор 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
)
) { /* Контент */ }

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

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


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

Предыдущая статьяКак писать безопасный код на Go
Следующая статьяRuby: рефакторинг без лишних сложностей