Ключевые кадры (keyframes) позволяют применять различные значения длительности и замедления в определенных точках временной шкалы анимации. Ключевые кадры применяются к анимации через
параметр animationSpec
и определяются с помощью функции keyframes():
public fun <T> keyframes( init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit ): KeyframesSpec<T>
Эта функция возвращает объект KeyframesSpec
принимает другую функцию, которая содержит данные о ключевых кадрах.
Определение анимации по ключевым кадрам содержит свойства durationMillis (общее время анимации) и delayMillis (задержка анимации - необязательное свойство), а также определения ключевых кадров. Каждый ключевой кадр содержит метку времени, которая указывает, какая часть общей анимации должна быть завершена в этот момент в зависимости от типа единицы состояния (например, Float, Dp, Int и т. д.). Эти временные метки создаются посредством вызовов функции at(). Например:
animationSpec = keyframes { durationMillis = 1000 100.dp.at(10) 110.dp.at(500) 200.dp.at(700) }
Здесь общее время анимации (durationMillis) 1000 миллисекунд. Для этой анимации задается три ключевых кадра. К примеру, первый кадр 100.dp.at(10)
указывает, что смещение в 100.dp надо достигнуть через
10 миллисекунд. При 500 миллисекундах смещение должно составлять 110dp и, наконец, 200dp по истечении 700 миллисекунд. Это оставляет 300 миллисекунд для завершения оставшейся анимации.
Рассмотрим применение анимации по ключевым кадрам на следующем примере:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.keyframes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val boxHeight = 150 // высота Box val startOffset = 10 // начальный отступ val endOffset = LocalConfiguration.current.screenHeightDp - boxHeight // конечный отступ var boxState by remember { mutableStateOf(startOffset)} val offset: Dp by animateDpAsState( targetValue = boxState.dp, animationSpec = keyframes { durationMillis = 1000 if (boxState==endOffset) { 100.dp.at(100) 110.dp.at(500) 200.dp.at(800) } } ) Row(Modifier.fillMaxSize()) { println("Offset: ${offset.value}") Button({boxState = if (boxState==startOffset) endOffset else startOffset }, Modifier.padding(10.dp)) { Text("Start", fontSize = 22.sp) } Box(Modifier.padding(top=offset).size(boxHeight.dp).background(Color.DarkGray)) } } } }
Здесь мы анимируем движение компонента Box по вертикали сверху вниз и обратно:
Для целей анимации определяем ряд переменных, как начальная и конечная позиция круга и его диаметр (высота)
val boxHeight = 150 // высота Box val startOffset = 10 // начальный отступ val endOffset = LocalConfiguration.current.screenHeightDp - boxHeight // конечный отступ
Далее определем состояние, которое будет хранить текущий текущий отступ с верху:
var boxState by remember { mutableStateOf(endOffset)}
Затем создаем анимацию с помощью функции animateDpAsState()
, которая будет определять отступ:
val offset: Dp by animateDpAsState( targetValue = boxState.dp, animationSpec = keyframes { durationMillis = 1000 if (boxState==endOffset) { 100.dp.at(100) 110.dp.at(500) 200.dp.at(800) } } )
Здесь прежде всего определяем targetValue
- значение, к которому надо перейти в процессе анимации. Поскольку при нажати на кнопку изменяется boxState, то именно эту переменная и будет
хранить следующее состояние, к которому надо выполнить анимационный переход. Соответственно просто передаем параметру targetValue
значение этой переменной, преобразуя его в объект
Dp.
Параметру animationSpec
передаем результат функции keyframes(). В ней устанавливаем общее время анимации - 1000 миллисекунд. И если надо перейти к конечному отступу
(в boxState хранится endOffset), то есть box находится вверху, то задаем три ключевых кадра. Если Box находится внизу, то ключевые кадры отсутствуют, а Box будет двигаться на начальную позицию равномерно.
Для запуска анимации определяем кнопку, которая меняет значение boxState и тем самым вызывает анимацию:
Button({boxState = if (boxState==startOffset) endOffset else startOffset },
И в конце идет собственно компонент Box, у которого анимируется отступ сверху:
Box(Modifier.padding(top=offset).size(boxHeight.dp).background(Color.DarkGray))
Его значение отступа с верху привязано к переменной offset, которая генерируется функцией animateDpAsState()
Стоит отметить, что в качестве альтернативы для установки ключевых кадров в функции keyframes()
мы могли бы использовать другой синтаксис, где функция at()
применялась бы аналогично операторам:
animationSpec = keyframes { durationMillis = 1000 if (boxState==endOffset) { 100.dp at 100 110.dp at 500 200.dp at 800 } }