Для применения анимации в приложении Jetpack Compose предоставляет специальный API - Animation API. Этот API состоит из классов и функций, которые предоставляют широкие возможности по созданию анимации. Рассмотрим ключевые функции и классы Animation API.
Так, Compose Animation API предоставляет ряд анимаций состояния компонентов. В частности, это функции анимации для значений типов Bounds, Color, Dp, Float, Int, IntOffset, IntSize, Offset, Rect и Size. Подобные функции покрывают большинство потребностей в анимации компонентов.
Подобные функции анимаций используют одно и то же соглашение об именах. В частности, все они называются по шаблону:
animate*AsState
где символ * представляет типо состояния, которое анимируется. Например, если нужно анимировать изменение цвета (например, цвета фона компонента), то применяется функция
animateColorAsState()
. В реальности, функции передается целевое (конечное) значение, которое должно получить состояние. И функция анимируется переход от текущего значения к
целевому значению. Рассмотрим ряд подобных функций.
Функция animateDpAsState() выполняет анимацию значений Dp, которые могут применяться для установки размеров, отступов и т.д. Она имеет следующие параметры:
@Composable public fun animateDpAsState( targetValue: Dp, animationSpec: AnimationSpec<Dp> = dpDefaultSpring, label: String = "DpAnimation", finishedListener: ((Dp) -> Unit)? = null ): State<Dp>
targetValue
: значение Dp, к которому надо выполнить переход
animationSpec
: применяемая анимация в виде объекта AnimationSpec
label
: название анимации
finishedListener
: функция, которая выполняется при завершении анимации
Обязательным параметром является только targetValue
. Рассмотрим простейший пример, в котором анимируется отступ слева, за счет чего возникает иллюзия движения объекта:
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.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth 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.unit.dp import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val startOffset = 0 // начальная позиция val endOffset = 300 // конечная позиция val boxWidth = 150 // ширина компонента var boxState by remember { mutableStateOf(startOffset)} val offset by animateDpAsState(targetValue = boxState.dp) Column(Modifier.fillMaxWidth()) { Box(Modifier.padding(start=offset).size(boxWidth.dp).background(Color.DarkGray)) Button({boxState = if (boxState==startOffset) {endOffset} else {startOffset} }, Modifier.padding(10.dp)) { Text("Move", fontSize = 25.sp) } } } } }
В данном случае по нажатию на кнопку происходит перемещение компонента с помощью анимации значений Dp:
Здесь в начале определяем некоторые значения, которые будут вычисляться для изменения позиции компонента Box:
val startOffset = 0 // начальная позиция val endOffset = 400 // конечная позиция val boxWidth = 200 // ширина компонента
То есть наш Box имеет ширину boxWidth и будет перемещаться от позиции startOffset до endOffset.
Далее определяем состояние, от которого будет зависеть позиция компонента Box:
var boxState by remember { mutableStateOf(startOffset)}
Это состояние по умолчанию равно startOffset.
Затем определяем анимацию позиции Box с помощью функции animateDpAsState()
val offset: Dp by animateDpAsState(targetValue = boxState.dp)
Параметр targetValue
на основании boxState определяет позицию, к которой надо выполнить переход. В данном случае все просто - boxState будет хранить отступ, и здесь мы его преобразуем в
значение Dp
.
Функция animateDpAsState()
возвращает значение типа State<Dp>
, и с помощью оператора by из него извлекаем непосредственно значение
Dp
и передаем его переменной offset.
Далее используем значение offset, полученное из функции animateDpAsState()
, для установки отступа от начала контейнера для компонента Box:
Box(Modifier.padding(start=offset) // устанавливаем отступ .size(boxWidth.dp) .background(Color.DarkGray))
И для запуска анимации на кнопку Move вешаем обработчик нажатия, который переключает значение boxState со startOffset на endOffset и обратно.
Button({boxState = if (boxState==startOffset) {endOffset} else {startOffset} },
То есть при нажатии на кнопку изменится значение boxState. Поскольку функция animateDpAsState зависит от boxState и на его основании устанавливает целевое значение, к которому надо перейти, то будет выполняться анимация для перехода к новому целевому значению.
Подобным образом мы можем анимировать любые параметры на основе Dp, не только отсутпы. Однако чуть улучшим предыдущий код. Так, выше значение endOffset взято случайным образом. Теперь сделаем движение Box до конца экрана:
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.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth 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.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.platform.LocalConfiguration class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val boxWidth = 150 // ширина компонента val startOffset = 0 // начальная позиция val endOffset = LocalConfiguration.current.screenWidthDp - boxWidth // конечная позиция var boxState by remember { mutableStateOf(startOffset)} val offset by animateDpAsState(targetValue = boxState.dp) Column(Modifier.fillMaxWidth()) { Box(Modifier.padding(start=offset).size(boxWidth.dp).background(Color.DarkGray)) Button({boxState = if (boxState==startOffset) {endOffset} else {startOffset} }, Modifier.padding(10.dp)) { Text("Move", fontSize = 25.sp) } } } } }
Теперь для вычисления конечной позиции применяется свойство LocalConfiguration.current.screenWidthDp
, которое возвращает ширину экрана:
val endOffset = LocalConfiguration.current.screenWidthDp - boxState
Кроме того, при вычислении конечной позиции учитываем ширину компонента, чтобы он не вылез за экран.