Встроенная функция spring() позволяет добавить к анимации эффект отскока, подобно тому, как мяч ударяется об землю и после этого еще несколько раз подпрыгивает, делая несколько отскоков, пока совсем не остановит свое движение. Функция spring() имеет следующие параметры:
public fun <T> spring( dampingRatio: Float = Spring.DampingRatioNoBouncy, stiffness: Float = Spring.StiffnessMedium, visibilityThreshold: T? = null ): SpringSpec<T>
dampingRatio
: степень затухания - определяет скорость, с которой затухает эффект подпрыгивания. Определяется как значение типа Float где 1.0
представляет отсутствие отскока, а 0.1 — самый высокий отскок. Вместо использования значений Float при настройке степени затухания также доступны следующие предопределенные константы:
DampingRatioHighBouncy
DampingRatioLowBouncy
DampingRatioMediumBouncy
DampingRatioNoBouncy
stiffness
: жесткость при отскоке. При использовании меньшей жесткости диапазон движения при подпрыгивании будет больше. Для определения жесткости можно использовать ряд встроенных констант:
StiffnessHigh
StiffnessLow
StiffnessMedium
StiffnessMediumLow
StiffnessVeryLow
visibilityThreshold
: предел видимости
Для демонстрации эффекта отскока рассмотрим следующее приложение:
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.spring 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.foundation.shape.CircleShape 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.draw.clip 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 circleHeight = 150 // высота (диаметр) круга val startOffset = 10 // начальный отступ val endOffset = LocalConfiguration.current.screenHeightDp - circleHeight // предельная позиция var circleOffset by remember { mutableStateOf(startOffset)} val offset: Dp by animateDpAsState( targetValue = circleOffset.dp, animationSpec =if (circleOffset==endOffset) { spring(dampingRatio = 0.3f) // сильный отскок } else { spring(dampingRatio = 1.0f) // отсутствие отскока } ) Row(Modifier.fillMaxSize()) { Button({circleOffset = if (circleOffset==startOffset) endOffset else startOffset }, Modifier.padding(10.dp)) { Text("Start", fontSize = 22.sp) } Box(Modifier.padding(top=offset).size(circleHeight.dp).clip(CircleShape).background(Color.DarkGray)) } } } }
Здесь определяется компонент Box в виде круга, и по нажатию на кнопку запускается анимация, в результате которой компонент движется вниз, пока не ударится о низ устройства.
Для целей анимации определяем ряд переменных, как начальная и конечная позиция круга и его диаметр (высота)
val circleHeight = 150 // высота (диаметр) круга val startOffset = 10 // начальный отступ val endOffset = LocalConfiguration.current.screenHeightDp - circleHeight // предельная позиция
Далее определем состояние, которое будет хранить текущий отступ круга:
var circleOffset by remember { mutableStateOf(startOffset)}
Затем создаем анимацию с помощью функции animateDpAsState()
, которая будет определять отступ:
val offset: Dp by animateDpAsState( targetValue = circleOffset.dp, animationSpec =if (circleOffset==endOffset) { spring(dampingRatio = 0.3f) // сильный отскок } else { spring(dampingRatio = 1.0f) // отсутствие отскока } )
Здесь прежде всего определяем targetValue
- значение, к которому надо перейти в процессе анимации. Ему передается значение переменной circleOffset, преобразованное в значение Dp.
Так как, поскольку по нажатию на кнопку мы будем изменять значение circleOffset
со startOffset на endOffset и обратно, то впоследствии circleOffset будет хранить следующее значение, к которому надо перейти.
Параметр animationSpec
, который задает анимацию, также использует условное выражение. Если значение, к которому надо перейти, является конечным - endOffset, то с помощью функции spring()
в конце формируем отскок. Значение 0.3f
можно характеризовать как сильный отскок. Иначе если мы находимся на конечной позиции, когда круг упал вниз, и его движением с отскоком завершилось, то просто возвращаем круг в начальную точку. В этом случае нам отскок не нужен,
поэтому передаем в функцию spring()
значение 1.0f
Для запуска анимации определяем кнопку, которая меняет значение circleOffset и тем самым вызывает анимацию:
Button({circleOffset = if (circleOffset==startOffset) endOffset else startOffset },
И в конце идет собственно компонент Box, стилизованный под круг:
Box(Modifier.padding(top=offset).size(circleHeight.dp).clip(CircleShape).background(Color.DarkGray))
Его значение отступа с верху привязано к переменной offset, которая генерируется функцией animateDpAsState()
В примере выше в функцию spring()
передавались числовые значения, и также можно передавать предустановленные константы:
import androidx.compose.animation.core.Spring.DampingRatioHighBouncy import androidx.compose.animation.core.Spring.DampingRatioNoBouncy ........................................... val offset: Dp by animateDpAsState( targetValue = circleOffset.dp, animationSpec =if (circleOffset==endOffset) { spring(dampingRatio = DampingRatioHighBouncy) // сильный отскок } else { spring(dampingRatio = DampingRatioNoBouncy) // отсутствие отскока } )
Аналогичным образом можно использовать другие параметры функции spring(). Например, установим жесткость:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.core.Spring.DampingRatioMediumBouncy import androidx.compose.animation.core.Spring.DampingRatioNoBouncy import androidx.compose.animation.core.Spring.StiffnessHigh import androidx.compose.animation.core.Spring.StiffnessVeryLow import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.spring 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.foundation.shape.CircleShape 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.draw.clip 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 circleHeight = 150 val startOffset = 10 val endOffset = LocalConfiguration.current.screenHeightDp - circleHeight var circleOffset by remember { mutableStateOf(startOffset)} val offset: Dp by animateDpAsState( targetValue = circleOffset.dp, animationSpec =if (circleOffset==endOffset) { spring(dampingRatio = DampingRatioMediumBouncy, stiffness = StiffnessVeryLow) // жесткость StiffnessVeryLow } else { spring(dampingRatio = DampingRatioNoBouncy) } ) val offset1: Dp by animateDpAsState( targetValue = circleOffset.dp, animationSpec =if (circleOffset==endOffset) { spring(dampingRatio = DampingRatioMediumBouncy, stiffness = StiffnessHigh) // жесткость StiffnessHigh } else { spring(dampingRatio = DampingRatioNoBouncy) } ) Row(Modifier.fillMaxSize()) { println("Offset: ${offset.value}") Button({circleOffset = if (circleOffset==startOffset) endOffset else startOffset }, Modifier.padding(10.dp)) { Text("Start", fontSize = 22.sp) } Box(Modifier.padding(top=offset).size(circleHeight.dp).clip(CircleShape).background(Color.DarkGray)) Box(Modifier.padding(top=offset1).size(circleHeight.dp).clip(CircleShape).background(Color.DarkGray)) } } } }