Connect with us

Разработка

Добавляем анимации встряхивания в Composable

В этой статье мы рассмотрим, как этого добиться, а также построим систему, позволяющую легко создавать трясущуюся анимацию с помощью кастомного модификатора.

Опубликовано

/

     
     

Анимации могут придать пользовательскому интерфейсу привлекательность и динамичность. Например, анимации встряхивания могут использоваться для выделения элемента, требующего внимания пользователя.

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

И наконец, мы узнаем, как сделать интерактивную анимацию, подобную этой:

Добавляем анимации встряхивания в Composable

Простая анимация встряхивания

Для этого мы будем использовать 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 на новое уникальное значение, например, на текущее время, и запускаем анимацию встряхивания.

В итоге мы получаем кнопку, которая трясется при нажатии.

Добавляем анимации встряхивания в Composable

Кастомный 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,  
)

Добавляем анимации встряхивания в Composable

А для ввода правильного пароля мы будем кивать кнопкой вверх-вниз, также с некоторым вращением. Это будет означать перевод по оси Y и вращение вокруг оси X.

ShakeConfig будет выглядеть следующим образом:

ShakeConfig(  
    iterations = 4,  
    intensity = 1_000f,  
    rotateX = -20f,  
    translateY = 20f,  
)

Добавляем анимации встряхивания в Composable

И вот теперь у нас есть анимированная кнопка с размашистыми движениями, похожими на голову человека.

Осталось добавить вокруг нее пользовательский интерфейс для сбора пароля и состояние для отслеживания правильности пароля. Вот полный код:

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,  
            )  
        }  
    }}

Теперь вы знаете, как встряхнуть свои композиции. Попробуйте сами и, надеюсь, у вас получится что-то восхитительное.

Спасибо за прочтение и удачи!

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Хорошие новости

Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: