Разработка
Добавляем анимации встряхивания в Composable
В этой статье мы рассмотрим, как этого добиться, а также построим систему, позволяющую легко создавать трясущуюся анимацию с помощью кастомного модификатора.
Анимации могут придать пользовательскому интерфейсу привлекательность и динамичность. Например, анимации встряхивания могут использоваться для выделения элемента, требующего внимания пользователя.
В Jetpack Compose это очень легко реализовать с помощью функций анимации. В этой статье мы рассмотрим, как этого добиться, а также построим систему, позволяющую легко создавать трясущуюся анимацию с помощью кастомного модификатора.
И наконец, мы узнаем, как сделать интерактивную анимацию, подобную этой:
Простая анимация встряхивания
Для этого мы будем использовать Animatable, который будем анимироваться вперед и назад при изменении значения.
@Composable fun Shaker() { val shake = remember { Animatable(0f) } var trigger by remember { mutableStateOf(0L) } LaunchedEffect(trigger) { if (trigger != 0L) { for (i in 0..10) { when (i % 2) { 0 -> shake.animateTo(5f, spring(stiffness = 100_000f)) else -> shake.animateTo(-5f, spring(stiffness = 100_000f)) } } shake.animateTo(0f) } } Box( modifier = Modifier .clickable { trigger = System.currentTimeMillis() } .offset { x = IntOffset(shake.value.roundToInt(), y = 0) } .padding(horizontal = 24.dp, vertical = 8.dp) ) { Text(text = "Shake me") } }
Мы создаем Animatable shake и инициализируем его значением 0. Мы также создаем триггер, который также инициализируется значением 0. Анимация встряхивания запускается, когда значение триггера изменяется на ненулевое число.
Это предотвращает запуск анимации при первой композиции.
При изменении значения триггера на ненулевое число мы анимируем тряску 10 раз от 5f до -5f. После завершения цикла мы возвращаем значение shake в ноль.
Мы используем значение из shake для смещения композиции по оси x.
Наконец, мы просто меняем значение параметра trigger on click на новое уникальное значение, например, на текущее время, и запускаем анимацию встряхивания.
В итоге мы получаем кнопку, которая трясется при нажатии.
Кастомный Modifier
Приведенная выше реализация хорошо работает при анимации только одного элемента. Но что если мы хотим анимировать несколько элементов в нашем приложении. Мы не хотим переписывать эту логику для каждого элемента, который мы хотим встряхнуть. Кроме того, мы можем добавить дополнительные настройки, чтобы встряхивать не только вдоль оси x.
Сначала мы создадим класс ShakeController и функцию для его создания внутри композиции.
@Composable fun rememberShakeController(): ShakeController { return remember { ShakeController() } } class ShakeController { var shakeConfig: ShakeConfig? by mutableStateOf(null) private set fun shake(shakeConfig: ShakeConfig) { this.shakeConfig = shakeConfig } }
ShakeController имеет параметр shakeConfig, определяющий параметры анимации встряхивания, и функцию shake, которую мы можем вызвать для запуска встряхивания.
С помощью этого мы можем создать ShakeController следующим образом:
val shakeController = rememberShakeController()
Прежде чем рассматривать процесс запуска встряхивания, давайте посмотрим, как определяется ShakeConfig.
data class ShakeConfig( val iterations: Int, val intensity: Float = 100_000f, val rotate: Float = 0f, val rotateX: Float = 0f, val rotateY: Float = 0f, val scaleX: Float = 0f, val scaleY: Float = 0f, val translateX: Float = 0f, val translateY: Float = 0f, val trigger: Long = System.currentTimeMillis(), )
Это класс, который мы можем использовать для определения нескольких анимаций встряхивания. Мы передаем итерации, интенсивность и значения анимации (вращение, масштабирование и т.д.). Все эти значения мы будем использовать для создания анимации.
Можно использовать не только эти значения. Это можно расширить, чтобы охватить другие свойства в анимации.
Наконец, мы создаем пользовательский модификатор, который можно просто передать в ShakeController, и он применит все анимации, основанные на нашей конфигурации shakeConfig.
fun Modifier.shake(shakeController: ShakeController) = composed { shakeController.shakeConfig?.let { shakeConfig -> val shake = remember { Animatable(0f) } LaunchedEffect(shakeController.shakeConfig) { for (i in 0..shakeConfig.iterations) { when (i % 2) { 0 -> shake.animateTo(1f, spring(stiffness = shakeConfig.intensity)) else -> shake.animateTo(-1f, spring(stiffness = shakeConfig.intensity)) } } shake.animateTo(0f) } this .rotate(shake.value * shakeConfig.rotate) .graphicsLayer { rotationX = shake.value * shakeConfig.rotateX rotationY = shake.value * shakeConfig.rotateY } .scale( scaleX = 1f + (shake.value * shakeConfig.scaleX), scaleY = 1f + (shake.value * shakeConfig.scaleY), ) .offset { IntOffset( (shake.value * shakeConfig.translateX).roundToInt(), (shake.value * shakeConfig.translateY).roundToInt(), ) } } ?: this }
Это упрощает наш предыдущий шейкер, сокращая объем кода до такого:
@Composable fun Shaker() { val shakeController = rememberShakeController() Box( modifier = Modifier .clickable { shakeController.shake(ShakeConfig(10, translateX = 5f)) } .shake(shakeController) .padding(horizontal = 24.dp, vertical = 8.dp) ) { Text(text = "Shake me") } }
Такой способ позволяет снизить сложность создания еще большего числа анимаций встряхивания с большим количеством анимируемых свойств.
Анимация встряхивания при входе в систему
Чтобы показать, как можно использовать этот модификатор в нашей работе, давайте создадим анимацию кнопки входа в систему, которая будет трястись при вводе неправильного пароля и кивать при вводе правильного.
Как применить эти движения для кнопки? Давайте потренируем свои шеи, чтобы выяснить это!
Время упражнений!
Где бы вы ни находились, попробуйте покачать головой из стороны в сторону в знак «нет» и кивнуть вверх-вниз в знак «да». Что вы заметили, кроме того, что на вас странно смотрят окружающие?
Положение вашего лица перемещается и вращается в трехмерном пространстве. Эту информацию мы будем использовать для моделирования движений нашей кнопки.
Мы можем использовать параметры rotate и translate, чтобы двигать кнопку, как человеческое лицо.
Для ввода неправильного пароля мы будем трясти кнопку из стороны в сторону с небольшим поворотом. Точнее, переведем ее на несколько пикселей вдоль оси X и повернем на несколько градусов вокруг оси Y.
Конфигурация ShakeConfig будет выглядеть следующим образом:
ShakeConfig( iterations = 4, intensity = 2_000f, rotateY = 15f, translateX = 40f, )
А для ввода правильного пароля мы будем кивать кнопкой вверх-вниз, также с некоторым вращением. Это будет означать перевод по оси Y и вращение вокруг оси X.
ShakeConfig будет выглядеть следующим образом:
ShakeConfig( iterations = 4, intensity = 1_000f, rotateX = -20f, translateY = 20f, )
И вот теперь у нас есть анимированная кнопка с размашистыми движениями, похожими на голову человека.
Осталось добавить вокруг нее пользовательский интерфейс для сбора пароля и состояние для отслеживания правильности пароля. Вот полный код:
sealed class LogInState { object Input : LogInState() object Wrong : LogInState() object Correct : LogInState() } val red = Color(0xFFDD5D5D) val green = Color(0xFF79DD5D) val white = Color(0xFFF7F7F7) @Composable fun LoginExample() { var password by remember { mutableStateOf("") } var logInState: LogInState by remember { mutableStateOf(LogInState.Input) } val color: Color by animateColorAsState( when (logInState) { LogInState.Correct -> green LogInState.Input -> white LogInState.Wrong -> red }, label = "Button color" ) val shakeController = rememberShakeController() TextField( value = password, onValueChange = { logInState = LogInState.Input password = it }, isError = logInState == LogInState.Wrong, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) Box(modifier = Modifier.height(12.dp)) Box( modifier = Modifier .padding(8.dp) .shake(shakeController = shakeController) .border(2.dp, color, RoundedCornerShape(5.dp)) .background(color = color.copy(alpha = .1f), shape = RoundedCornerShape(5.dp)) .pointerInput(Unit) { detectTapGestures { logInState = when (password) { "password" -> LogInState.Correct else -> LogInState.Wrong } when (logInState) { LogInState.Correct -> { shakeController.shake( ShakeConfig( iterations = 4, intensity = 1_000f, rotateX = -20f, translateY = 20f, ) ) } LogInState.Wrong -> { shakeController.shake( ShakeConfig( iterations = 4, intensity = 2_000f, rotateY = 15f, translateX = 40f, ) ) } LogInState.Input -> {} } } } .clip(RoundedCornerShape(5.dp)) .padding(horizontal = 24.dp, vertical = 8.dp), contentAlignment = Alignment.Center, ) { AnimatedContent( targetState = logInState, transitionSpec = { slideInVertically(spring(stiffness = Spring.StiffnessMedium)) { -it } + fadeIn() with slideOutVertically(spring(stiffness = Spring.StiffnessHigh)) { it } + fadeOut() using SizeTransform( clip = false ) }, contentAlignment = Alignment.Center ) { logInState -> Text( text = when (logInState) { LogInState.Correct -> "Success" LogInState.Input -> "Login" LogInState.Wrong -> "Try Again" }, color = Color.White, fontWeight = FontWeight.Medium, ) } }}
Теперь вы знаете, как встряхнуть свои композиции. Попробуйте сами и, надеюсь, у вас получится что-то восхитительное.
Спасибо за прочтение и удачи!