Jetpack Compose动画,一篇全懂
https://blog.csdn.net/lyabc123456?type=blog
fun AnimatedVisibilityExample() {
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically { with(density) { -40.dp.roundToPx() } } // 从顶部 40dp 的地方开始滑入
+ expandVertically(expandFrom = Alignment.Top) // 从顶部开始展开
+ fadeIn(initialAlpha = 0.3f), // 从初始透明度 0.3f 开始淡入
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello",
Modifier.background(Color.Green).fillMaxWidth().height(200.dp)
.wrapContentWidth(Alignment.CenterHorizontally),
fontSize = 20.sp
)
}
Button(
onClick = { visible = !visible },
modifier = Modifier.padding(top = 200.dp)
) {
Text(text = if(visible) "隐藏" else "显示")
}
}
}
@Composable
fun AnimatedVisibilityExample3() {
var visible by remember { mutableStateOf(true) }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) {
// 外层Box组件淡入淡出进出屏幕
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(Modifier.align(Alignment.Center)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Green)
.animateEnterExit(enter = slideInVertically(), exit = slideOutVertically())
) {
Text(text = "内层Box组件滑动进出屏幕", Modifier.align(Alignment.Center))
}
Box(Modifier.padding(top = 150.dp).align(Alignment.Center)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Cyan)
.animateEnterExit(enter = scaleIn(), exit = scaleOut())
) {
Text(text = "内层层Box组件缩放进出屏幕", Modifier.align(Alignment.Center))
}
}
}
Button(
onClick = { visible = !visible },
modifier = Modifier.padding(top = 50.dp)
) {
Text(text = if(visible) "隐藏" else "显示")
}
}
}
@Composable
fun AnimatedVisibilityExample4() {
var visible by remember { mutableStateOf(true) }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(visible = visible, enter = scaleIn(), exit = scaleOut()) {
// 使用 AnimatedVisibilityScope#transition 添加自定义的动画与AnimatedVisibility同时执行
val background by transition.animateColor(label = "backgroundTransition") { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Green
}
Box(modifier = Modifier.size(100.dp).background(background))
}
Button(
onClick = { visible = !visible },
modifier = Modifier.padding(top = 120.dp)
) {
Text(text = if(visible) "隐藏" else "显示")
}
}
}
@Composable
fun AnimatedContentExample() {
Column {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("Add") }
AnimatedContent(targetState = count) { targetCount ->
// 这里要使用lambda的参数 `targetCount`, 而不是 `count`,否则将没有意义(API 会将此值用作键,以标识当前显示的内容)
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)
@Composable
fun AnimatedContentExample2() {
Column {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("Add") }
AnimatedContent(
targetState = count,
transitionSpec = {
// 从右往左切换,并伴随淡入淡出效果(initialOffsetX = width, targetOffsetX = -width)
slideInHorizontally{width -> width} + fadeIn() with
slideOutHorizontally{width -> -width} + fadeOut()
}
) { targetCount ->
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
@Composable
fun AnimatedContentExample3() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("Add") }
val animationSpec = tween<IntOffset>(200)
val animationSpec2 = tween<Float>(200)
AnimatedContent(
targetState = count,
transitionSpec = {
slideInVertically(animationSpec){ height -> height} + fadeIn(animationSpec2) with
slideOutVertically(animationSpec) {height -> height} + fadeOut(animationSpec2)
}
) { targetCount ->
Text(text = "$targetCount", fontSize = 40.sp)
}
}
}
@Composable
fun AnimatedContentExample4() {
Column {
var count by remember { mutableStateOf(0) }
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = { count-- }) { Text("Minus") }
Spacer(Modifier.size(60.dp))
Button(onClick = { count++ }) { Text("Plus ") }
}
Spacer(Modifier.size(20.dp))
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > initialState) {
// 如果targetState更大,则从下往上切换并伴随淡入淡出效果
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// 如果targetState更小,则从上往下切换并伴随淡入淡出效果
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
@Composable
fun SlideIntoContainerSample() {
val transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
if (initialState < targetState) {
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up) + fadeIn() with
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Up) + fadeOut()
} else {
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Down) + fadeIn() with
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Down) + fadeOut()
}.apply {
// 这里可指定目标内容的 zIndex ,值越大越上层,值越小越下层
// targetContentZIndex = when (targetState) {
// NestedMenuState.Level1 -> 1f
// NestedMenuState.Level2 -> 2f
// NestedMenuState.Level3 -> 3f
// }
}.using(SizeTransform(clip = false))
}
Column {
var count by remember { mutableStateOf(0) }
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = { count-- }) { Text("Minus") }
Spacer(Modifier.size(60.dp))
Button(onClick = { count++ }) { Text("Plus ") }
}
Spacer(Modifier.size(20.dp))
AnimatedContent(
targetState = count,
transitionSpec = transitionSpec,
) { targetCount ->
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
SizeTransform
@Composable
fun SizeTransformAnimatedContentSample() {
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded },
modifier = Modifier.padding(10.dp).onSizeChanged { }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// 展开时,先水平方向展开
// 150ms之前:宽度从initialSize.width增大到targetSize.width,高度保持initialSize.height不变
// 150ms之后:宽度保持targetSize.width不变,高度从initialSize.height开始增大到targetSize.height
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// 收缩时,先垂直方向收起
// 150ms之前:宽度保持initialSize.width不变,高度从initialSize.height减小到targetSize.height
// 150ms之后:宽度从initialSize.width减小到targetSize.width,高度保持targetSize.height不变
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) Expanded() else ContentIcon()
}
}
}
@Composable
fun ContentIcon() {
Icon(Icons.Default.ArrowDropDown, "")
}
@Composable
fun Expanded() {
Text(text = "SizeTransform 定义了大小应如何在初始内容与目标内容之间添加动画效果。在创建动画时,您可以访问初始大小和目标大小。SizeTransform 还可控制在动画播放期间是否应将内容裁剪为组件大小。SizeTransform 定义了大小应如何在初始内容与目标内容之间添加动画效果。在创建动画时,您可以访问初始大小和目标大小。SizeTransform 还可控制在动画播放期间是否应将内容裁剪为组件大小。",
modifier = Modifier.padding(10.dp))
}
fun AnimateContentSizeExample() {
var expand by remember { mutableStateOf(true) }
Column(Modifier.padding(16.dp)) {
Button(onClick = { expand = !expand }) {
Text(text = if (expand) "收起" else "展开")
}
Spacer(Modifier.height(16.dp))
Box(
modifier = Modifier.background(Color.Green, RoundedCornerShape(15.dp))
.padding(16.dp).wrapContentSize()
.animateContentSize()
) {
Text(
text = "Modifier.animateContentSize() animates its own size when its child modifier (or the child composable if it is already at the tail of the chain) changes size. This allows the parent modifier to observe a smooth size change, resulting in an overall continuous visual change.\n\n"
+"A FiniteAnimationSpec can be optionally specified for the size change animation. By default, spring will be used.\n\n"
+"An optional finishedListener can be supplied to get notified when the size change animation is finished. Since the content size change can be dynamic in many cases, both initial value and target value (i.e. final size) will be passed to the finishedListener. Note: if the animation is interrupted, the initial value will be the size at the point of interruption. This is intended to help determine the direction of the size change (i.e. expand or collapse in x and y dimensions).",
fontSize = 16.sp,
textAlign = TextAlign.Justify,
maxLines = if (expand) Int.MAX_VALUE else 2
)
}
}
}
fun CrossfadeExample() {
Column {
var currentPage by remember { mutableStateOf("A") }
Button(onClick = { currentPage = if(currentPage == "A") "B" else "A" }) {
Text("Change")
}
Spacer(Modifier.size(20.dp))
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A", Modifier.background(Color.Green), fontSize = 25.sp)
"B" -> Text("Page B", Modifier.background(Color.Red), fontSize = 25.sp)
}
}
}
}
fun AnimateXXXAsStateExample() {
var enabled by remember { mutableStateOf(true) }
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.2f)
Box(
Modifier
.size(200.dp)
.graphicsLayer(alpha = alpha)
.background(Color.Red)
.clickable { enabled = !enabled }
)
}
点击按钮时,buttonSize从24dp开始向32dp过渡,
当buttonSize达到32dp时,changeSize被置为false,buttonSize又会从32dp开始向24dp过渡,
同时每点击一次按钮,按钮颜色在红色和灰色之间过渡切换
size: 小 -> 大 -> 小 color: 灰色 -> 红色
fun AnimateXXXAsStateExample2() {
var changeSize by remember { mutableStateOf(false) }
var changeColor by remember { mutableStateOf(false) }
val buttonSize by animateDpAsState(if(changeSize) 32.dp else 24.dp)
val buttonColor by animateColorAsState(
targetValue = if(changeColor) Color.Red else Color.Gray,
animationSpec = spring(Spring.DampingRatioNoBouncy)
)
if (buttonSize == 32.dp) {
changeSize = false
}
Box(Modifier.fillMaxSize(),contentAlignment = Alignment.Center) {
IconButton(
onClick = {
changeSize = true
changeColor = !changeColor
}
) {
Icon(Icons.Rounded.Favorite, null, Modifier.size(buttonSize), tint = buttonColor)
}
}
}
initialValue: T, // T类型的动画初始值
val typeConverter: TwoWayConverter<T, V>, // 将T类型的数值与V类型的数组进行转换
private val visibilityThreshold: T? = null, // 动画消失时的阈值,默认为null
val label: String = "Animatable"
)
interface TwoWayConverter<T, V : AnimationVector> {
val convertToVector: (T) -> V // 将T类型的数值转换为V类型
val convertFromVector: (V) -> T // 将V类型的数值转换为T类型
}
TwoWayConverter( { AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
fun AnimateValueExample(targetSize: MySize) {
val animSize: MySize by animateValueAsState(
targetValue = targetSize,
typeConverter = TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
data class MySize(val width: Dp, val height: Dp)
Transition
Collapsed,
Expanded
}
updateTransition
val transition = updateTransition(currentState, label = "BoxTransition")
fun TransitionAnimationExample() {
var boxState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(boxState, label = "BoxTransition")
val size by transition.animateFloat(
transitionSpec = { tween(500) }, label = "size"
) { state ->
state.valueOf(collapsed = 100f, expanded = 200f)
}
val borderWidth by transition.animateDp(
transitionSpec = { tween(500) }, label = "borderWidth"
) { state ->
state.valueOf(collapsed = 1.dp, expanded = 2.dp)
}
val bgColor by transition.animateColor(
transitionSpec = {
when {
BoxState.Expanded isTransitioningTo BoxState.Collapsed -> spring(stiffness = 50f)
else -> tween(500)
}
}, label = "bgColor"
) { state ->
state.valueOf(collapsed = Color.Green, expanded = Color.Cyan)
}
val borderColor by transition.animateColor(label = "borderColor") { state ->
state.valueOf(collapsed = Color.Red, expanded = Color.Blue)
}
Box(
modifier = Modifier
.clickable {
boxState = boxState.swapState()
}
.padding(5.dp)
.size(size.dp)
.background(bgColor)
.border(BorderStroke(borderWidth, borderColor)),
contentAlignment = Alignment.Center
) {
Text(text = boxState.valueOf("Collapsed", "Expanded"))
}
}
fun <T> BoxState.valueOf(collapsed: T, expanded: T) : T {
return when (this) {
BoxState.Collapsed -> collapsed
BoxState.Expanded -> expanded
}
}
fun BoxState.swapState() : BoxState = valueOf(BoxState.Expanded, BoxState.Collapsed)
fun MutableTransitionStateExample() {
val boxState = remember {
MutableTransitionState(BoxState.Collapsed).apply {
targetState = BoxState.Expanded // 修改targetState与initialState不同立即执行动画
}
}
val transition = updateTransition(boxState, label = "BoxTransition")
val size by transition.animateFloat(
transitionSpec = { tween(500) }, label = "sizeTransition"
) { state ->
state.valueOf(collapsed = 100f, expanded = 200f)
}
val bgColor by transition.animateColor(
transitionSpec = { tween(500) }, label = "bgColorTransition"
) { state ->
state.valueOf(collapsed = Color.Green, expanded = Color.Cyan)
}
Box(
modifier = Modifier
.clickable {
boxState.targetState = boxState.targetState.swapState() // 修改状态时要修改状态的targetState
}
.padding(5.dp)
.size(size.dp)
.background(bgColor) ,
contentAlignment = Alignment.Center
) {
Text(text = boxState.valueOf("Collapsed", "Expanded"))
}
}
fun <T> MutableTransitionState<BoxState>.valueOf(collapsed: T, expanded: T) : T {
return this.targetState.valueOf(collapsed, expanded)
}
例如,使用 AnimatedVisibility 搭配 MutableTransitionState 可以实现观察动画的可见状态:
fun AnimatedVisibilityExample2() {
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
// initialState为false, targetState设置为true,
// 当AnimatedVisibility上屏时,由于两个状态不同,会立即执行动画
MutableTransitionState(false).apply {
targetState = true // Start the animation immediately.
}
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!",
Modifier.background(Color.Green).height(100.dp).fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally),
fontSize = 20.sp)
}
// 获取AnimatedVisibility当前所处的动画状态并显示
Text(text = "current Animate State: ${state.getAnimationState()}",
Modifier.padding(top = 100.dp).background(Color.Red).fillMaxWidth().padding(15.dp),
color = Color.White,
fontSize = 20.sp)
Button(
onClick = { state.targetState = !state.targetState },
Modifier.padding(top = 160.dp)
) {
Text(text = if(state.targetState) "隐藏" else "显示")
}
}
}
enum class Animate { VISIBLE, INVISIBLE, APPEARING, DISAPPEARING }
// 为MutableTransitionState定义一个扩展函数来方便的获取动画状态
fun MutableTransitionState<Boolean>.getAnimationState(): Animate {
return when {
this.isIdle && this.currentState -> Animate.VISIBLE // 动画已结束,当前处于可见状态
!this.isIdle && this.currentState -> Animate.DISAPPEARING // 动画执行中,且逐渐不可见
this.isIdle && !this.currentState -> Animate.INVISIBLE // 动画已结束,当前处于不可见状态
else -> Animate.APPEARING // 动画执行中,且逐渐可见
}
}
@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
// 不需要知道其他状态,只需关注当前isVisibleTransition是 visible 还是 not visible.
// ...
}
@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
// 不需要知道其他状态,只需关注当前isVisibleTransition是 visible 还是 not visible.
// ...
}
@OptIn(ExperimentalTransitionApi::class)
@Composable
fun Dialer(dialerState: DialerState) {
val transition = updateTransition(dialerState, label = "")
Box {
NumberPad(
transition.createChildTransition {
it == DialerState.NumberPad
}
)
DialerButton(
transition.createChildTransition {
it == DialerState.DialerMinimized
}
)
}
}
@Composable
fun TransitionWithAnimatedVisibilityAndAnimatedContent() {
var selected by remember { mutableStateOf(false) }
// 当 `selected` 变化时触发transition动画
val transition = updateTransition(selected, label = "")
val borderColor by transition.animateColor(label = "") { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "") { isSelected ->
if (isSelected) 10.dp else 2.dp
}
Surface(
modifier = Modifier.clickable { selected = !selected }
.padding(10.dp),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, borderColor),
elevation = elevation
) {
Column(modifier = Modifier
.fillMaxWidth()
.padding(16.dp)) {
Text(text = "Hello, world!")
// AnimatedVisibility 作为过渡动画的一部分
transition.AnimatedVisibility(
visible = { targetSelected -> targetSelected },
enter = expandVertically(),
exit = shrinkVertically()
) {
Text(text = "It is fine today.")
}
// AnimatedContent 作为过渡动画的一部分
transition.AnimatedContent { targetState ->
if (targetState) {
Text(text = "Selected")
} else {
Icon(Icons.Default.Favorite, "")
}
}
}
}
}
fun AnimatingBoxExample() {
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
AnimatingBox(
boxState = currentState,
Modifier.clickable { currentState = currentState.swapState() }
)
}
@Composable
fun AnimatingBox(boxState: BoxState, modifier: Modifier = Modifier) {
val transitionData = updateTransitionData(boxState)
Box(modifier.background(transitionData.color).size(transitionData.size))
}
// 保存动画值
private class TransitionData(color: State<Color>, size: State<Dp>) {
val color by color
val size by size
}
// 创建一个 Transition 并返回其动画值
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState, label = "boxState")
val color = transition.animateColor(label = "color") { state ->
state.valueOf(collapsed = Color.Gray, expanded = Color.Red)
}
val size = transition.animateDp(label = "size") { state ->
state.valueOf(collapsed = 64.dp, expanded = 128.dp)
}
return remember(transition) { TransitionData(color, size) }
}
fun <T> BoxState.valueOf(collapsed: T, expanded: T) : T {
return when (this) {
BoxState.Collapsed -> collapsed
BoxState.Expanded -> expanded
}
}
fun BoxState.swapState() : BoxState = valueOf(BoxState.Expanded, BoxState.Collapsed)
fun RememberInfiniteTransitionExample() {
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Blue,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val roundPercent by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 100f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = FastOutLinearInEasing),
repeatMode = RepeatMode.Reverse
)
)
val offset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 100f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(modifier = Modifier.size(300.dp)) {
Box(
Modifier
.padding(10.dp)
.size(100.dp)
.offset(offset.dp, offset.dp)
.clip(RoundedCornerShape(roundPercent.toInt()))
.background(color)
)
}
}
Reverse:执行到目标状态后再原路返回初始状态,逆向执行动画效果
Restart:执行到目标状态后再重新从初始状态开始执行
fun InfiniteRepeatableDemo() {
val infiniteTransition = rememberInfiniteTransition()
val degrees by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 3000
0F at 0
360f at 3000
}
)
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "infiniteRepeatable",
modifier = Modifier.rotate(degrees = degrees),
color = Color.Red,
fontSize = 22.sp
)
}
}
fun ImageBorderAnimation() {
val infiniteTransition = rememberInfiniteTransition()
val degrees by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(tween(1000, easing = LinearEasing))
)
val strokeWidth = 8.dp
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_head3),
contentDescription = "head",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(150.dp)
.drawBehind {
rotate(degrees) {
drawCircle(
brush = Brush.sweepGradient(colors = colors),
style = Stroke(strokeWidth.toPx())
)
}
}
.padding(strokeWidth / 2)
.clip(CircleShape)
)
}
}
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
dampingRatio表示弹簧的阻尼比,即弹簧的弹性,dampingRatio值越小弹性越大(震动幅度越大),反之弹性越小(dampingRatio>=0)
dampingRatio默认值是Spring.DampingRatioNoBouncy,即默认没有弹性,系统预定义的dampingRatio的各个取值及效果如下
const val DampingRatioHighBouncy = 0.2f
const val DampingRatioMediumBouncy = 0.5f
const val DampingRatioLowBouncy = 0.75f
const val DampingRatioNoBouncy = 1f
}
stiffness表示弹簧的刚度,值越大表示到静止状态的速度越快,反之越慢。默认值为Spring.StiffnessMedium。(stiffness>0)
系统预定义的stiffness常量值如下:
const val StiffnessHigh = 10_000f
const val StiffnessMedium = 1500f
const val StiffnessMediumLow = 400f
const val StiffnessLow = 200f
const val StiffnessVeryLow = 50f
}
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
// 先匀速后减速
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
// 先加速后匀速
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
// 一直匀速
val LinearEasing: Easing = Easing { fraction -> fraction }
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // 0ms初始值为0f, 开始使用LinearOutSlowInEasing
0.2f at 15 with FastOutLinearInEasing // 15ms达到0.2f, 从15ms开始使用FastOutLinearInEasing
0.4f at 75 // 75ms达到0.4f
0.4f at 225 // 225ms达到0.4f
}
)
targetValue = 1f,
animationSpec = repeatable(
iterations = 3, // 重复播放的次数
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
RepeatMode.Reverse:往返执行,达到目标值后再原路返回初始值
RepeatMode.Restart:从头执行,达到目标值后,再重新从初始值开始执行
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
snap快闪动画
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
动画形式的矢量资源
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))
fun AnimatableExample() {
var changeSize by remember { mutableStateOf(false) }
var changeColor by remember { mutableStateOf(false) }
val buttonSize = remember { Animatable(24.dp, Dp.VectorConverter) }
val buttonColor = remember { Animatable(Color.Gray) }
LaunchedEffect(changeSize, changeColor) {
// 注意,因为animateTo是挂起函数,会阻塞当前协程,
// 所以这里必须分别放在launch中启动子协程执行,否则动画效果是顺序执行的
// 或者,也可以分开放在两个LaunchedEffect里执行
launch { buttonSize.animateTo(if(changeSize) 32.dp else 24.dp) }
launch { buttonColor.animateTo(if(changeColor) Color.Red else Color.Gray) }
}
if (buttonSize.value == 32.dp) {
changeSize = false
}
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
IconButton(
onClick = {
changeSize = true
changeColor = !changeColor
}
) {
Icon(Icons.Rounded.Favorite, null,
Modifier.size(buttonSize.value), tint = buttonColor.value)
}
}
}
println(value) // 监听动画状态值的变化
}
if (result.endReason == AnimationEndReason.BoundReached) {
buttonSize.animateTo(...) // 例如可以反向执行动画
}
TargetBasedAnimation(
animationSpec = tween(200),
typeConverter = Float.VectorConverter,
initialValue = 200f,
targetValue = 1000f
)
}
var playTime by remember { mutableStateOf(0L) }
LaunchedEffect(anim) {
val startTime = withFrameNanos { it }
do {
playTime = withFrameNanos { it } - startTime
val animationValue = anim.getValueFromNanos(playTime)
} while (someCustomCondition())
}
@Composable
fun GestureWithAnimation() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope { awaitFirstDown().position }
// Animate to the tap position.
launch { offset.animateTo(position) }
}
}
}
) {
Circle(Modifier.offset { offset.value.toIntOffset() })
}
}
@Composable
fun Circle(modifier: Modifier = Modifier) {
Box(modifier.size(100.dp).clip(CircleShape).background(Color.Red))
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch { offsetX.snapTo(offsetX.value + change.positionChange().x) }
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(argetValue = 0f,initialVelocity = velocity)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
@Composable
fun SwipeToDismissItem(color: Color = Color.Red) {
var isDismissed by remember { mutableStateOf(false) }
if (!isDismissed) {
Box(
Modifier
.swipeToDismiss { isDismissed = true }
.fillMaxWidth()
.height(100.dp)
.background(color)
)
}
}
@Composable
fun SwipeToDismissAnimationExample() {
Column {
repeat(7) {
SwipeToDismissItem(color = colors[it % colors.size])
}
}
}
@Composable
fun LazyColumnListAnimation() {
var list by remember { mutableStateOf(('A'..'Z').toList()) }
LazyColumn(
Modifier.fillMaxWidth(),
contentPadding = PaddingValues(35.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
item {
Button(onClick = { list = list.shuffled() }) {
Text("Shuffle")
}
}
items(list, key = { it }) {
CardContent(
"Item $it",
modifier = Modifier.animateItemPlacement(),
Color.Blue,
Color.White
)
}
}
}
fun ListItemAnimationComponent() {
val personList = getPersonList()
val deletedPersonList = remember { mutableStateListOf<Person>() }
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
itemsIndexed(items = personList) { index, person ->
AnimatedVisibility(
visible = !deletedPersonList.contains(person),
enter = expandVertically(),
exit = shrinkVertically(animationSpec = tween(1000))
) {
Card(
shape = RoundedCornerShape(4.dp),
backgroundColor = colors[index % colors.size],
modifier = Modifier.fillParentMaxWidth()
) {
Row(
modifier = Modifier.fillParentMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
person.name,
style = TextStyle(
color = Color.Black,
fontSize = 20.sp,
textAlign = TextAlign.Center
),
modifier = Modifier.padding(16.dp)
)
IconButton(onClick = { deletedPersonList.add(person) }) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete"
)
}
}
}
}
}
}
}
确保app/build.gradle中添加了constraintlayout-compose依赖库
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
在res/raw/motion_scene.json5中进行配置
在MotionLayout Composable函数中解析并使用配置
ConstraintSets: {
start: {
profile_pic: {
width: 48,
height: 48,
start: ['parent', 'start', 16],
top: ['parent', 'top', 16],
custom: {
background: '#08ff04'
}
},
username: {
top: ['profile_pic', 'top'],
bottom: ['profile_pic', 'bottom'],
start: ['profile_pic', 'end', 16]
},
box: {
width: 'spread',
height: 'spread',
start: ['parent', 'start'],
end: ['parent', 'end'],
top: ['parent', 'top'],
bottom: ['parent', 'bottom', -16],
}
},
end: {
profile_pic: {
width: 150,
height: 150,
start: ['parent', 'start'],
end: ['parent', 'end'],
top: ['parent', 'top', 16],
custom: {
background: '#FFFFFF'
}
},
username: {
top: ['profile_pic', 'bottom', 16],
end: ['parent', 'end'],
start: ['parent', 'start']
},
box: {
width: 'spread',
height: 'spread',
start: ['parent', 'start'],
end: ['parent', 'end'],
top: ['parent', 'top'],
bottom: ['parent', 'bottom', -16],
}
}
},
Transitions: {
default: {
from: 'start',
to: 'end',
pathMotionArc: 'startHorizontal',
KeyFrames: {
KeyAttributes: [
{
target: ['profile_pic'],
frames: [0, 100]
},
{
target: ['username'],
frames: [0, 50, 100],
translationX: [0, 90, 0],
}
]
}
}
}
}
fun MotionLayoutAnimationDemo() {
Column {
var progress by remember { mutableStateOf(0f) }
ProfileHeader(progress = progress)
Spacer(modifier = Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = { progress = it },
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
@OptIn(ExperimentalMotionApi::class)
@Composable
fun ProfileHeader(progress: Float) {
val context = LocalContext.current
val motionScene = remember {
context.resources
.openRawResource(R.raw.motion_scene)
.readBytes()
.decodeToString()
}
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = progress,
modifier = Modifier.fillMaxWidth()
) {
val properties = motionProperties(id = "profile_pic")
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.DarkGray)
.layoutId("box")
)
Image(
painter = painterResource(id = R.drawable.ic_head3),
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.border(
width = 2.dp,
color = properties.value.color("background"),
shape = CircleShape
)
.layoutId("profile_pic")
)
Text(
text = "MotionLayout",
fontSize = 24.sp,
modifier = Modifier.layoutId("username"),
color = properties.value.color("background")
)
}
}
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
val barHeight = 10.dp
val spacerPadding = 3.dp
val roundedCornerShape = RoundedCornerShape(3.dp)
val shimmerColors = listOf(
Color.LightGray.copy(alpha = 0.6f),
Color.LightGray.copy(alpha = 0.2f),
Color.LightGray.copy(alpha = 0.6f),
)
@Preview(showBackground = true)
@Composable
fun AnimatedShimmerItem() {
val transition = rememberInfiniteTransition()
val translateAnim = transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart
)
)
// 微光渐变效果
val brush = Brush.linearGradient(
colors = shimmerColors,
start = Offset.Zero,
end = Offset(x = translateAnim.value, y = translateAnim.value) // 动画改变end坐标位置产生渐变色位移效果
)
ShimmerItem(brush)
}
@Preview(showBackground = true)
@Composable
fun ShimmerItem(brush: Brush = Brush.linearGradient(shimmerColors)) {
Column(Modifier.fillMaxWidth().padding(10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(
modifier = Modifier
.size(100.dp)
.clip(roundedCornerShape)
.background(brush)
)
Spacer(modifier = Modifier.width(10.dp))
Column(verticalArrangement = Arrangement.Center) {
repeat(5) {
Spacer(modifier = Modifier.padding(spacerPadding))
Spacer(
modifier = Modifier
.height(barHeight)
.clip(roundedCornerShape)
.fillMaxWidth()
.background(brush)
)
Spacer(modifier = Modifier.padding(spacerPadding))
}
}
}
repeat(3) {
Spacer(modifier = Modifier.padding(spacerPadding))
Spacer(
modifier = Modifier
.height(barHeight)
.clip(roundedCornerShape)
.fillMaxWidth()
.background(brush)
)
Spacer(modifier = Modifier.padding(spacerPadding))
}
}
}
@Preview(showBackground = true)
@Composable
fun ShimmerListPreview() {
Column(Modifier.padding(5.dp).verticalScroll(rememberScrollState())) {
repeat(5) {
AnimatedShimmerItem()
}
}
}
收藏按钮动画效果
import androidx.compose.animation.core.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.fly.mycompose.application.ui.theme.MyComposeApplicationTheme
import com.fly.mycompose.application.ui.theme.Purple500
data class UiState(
val backgroundColor: Color,
val textColor: Color,
val roundedCorner: Int,
val buttonWidth: Dp
)
enum class ButtonState(val ui: UiState) {
Idle(UiState(Color.White, Purple500, 6, 300.dp)),
Pressed(UiState(Purple500, Color.White, 50, 60.dp))
}
const val animateDuration = 500
fun changeButtonState(buttonState: ButtonState) : ButtonState {
return when(buttonState) {
ButtonState.Idle -> ButtonState.Pressed
ButtonState.Pressed -> ButtonState.Idle
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedFavButton(modifier: Modifier = Modifier) {
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
Box(modifier) {
AnimatedContent(
targetState = buttonState,
transitionSpec = {
fadeIn(tween(animateDuration)) with
fadeOut(tween(animateDuration))
}
) { state ->
FavButton(buttonState = state) {
buttonState = changeButtonState(buttonState)
}
}
}
}
@Composable
fun AnimatedFavButton2(modifier: Modifier = Modifier) {
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
val transition = updateTransition(targetState = buttonState, label = "")
val backgroundColor by transition.animateColor(
transitionSpec = { spring() }, label = ""
) { it.ui.backgroundColor }
val textColor by transition.animateColor(
transitionSpec = { spring() }, label = ""
) { it.ui.textColor }
val roundedCorner by transition.animateInt(
transitionSpec = { spring() }, label = ""
) { it.ui.roundedCorner }
val buttonWidth by transition.animateDp(
transitionSpec = { spring() }, label = ""
) { it.ui.buttonWidth }
FavButton(
modifier, buttonState, textColor, backgroundColor, roundedCorner, buttonWidth,
) {
buttonState = changeButtonState(buttonState)
}
}
@Composable
fun FavButton(
modifier: Modifier = Modifier,
buttonState: ButtonState,
textColor: Color = buttonState.ui.textColor,
backgroundColor: Color = buttonState.ui.backgroundColor,
roundedCorner: Int = buttonState.ui.roundedCorner,
buttonWidth: Dp = buttonState.ui.buttonWidth,
onClick: () -> Unit
) {
Button(
border = BorderStroke(1.dp, Purple500),
modifier = modifier.size(buttonWidth, height = 60.dp),
shape = RoundedCornerShape(roundedCorner.coerceIn(0..100)),
colors = ButtonDefaults.buttonColors(backgroundColor),
onClick = onClick,
) {
if (buttonState == ButtonState.Idle) {
Row {
Icon(
tint = textColor,
imageVector = Icons.Default.FavoriteBorder,
modifier = Modifier.size(24.dp).align(Alignment.CenterVertically),
contentDescription = null
)
Spacer(Modifier.width(16.dp))
Text(
"ADD TO FAVORITES!",
softWrap = false,
modifier = Modifier.align(Alignment.CenterVertically),
color = textColor
)
}
} else {
Icon(
tint = textColor,
imageVector = Icons.Default.Favorite,
modifier = Modifier.size(24.dp),
contentDescription = null
)
}
}
}
@Preview(showBackground = true)
@Composable
fun PreviewFavButton() {
MyComposeApplicationTheme {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("High Level API(AnimatedContent)")
Spacer(Modifier.height(10.dp))
AnimatedFavButton()
Spacer(Modifier.height(50.dp))
Text("Low Level API(updateTransition)")
Spacer(Modifier.height(10.dp))
AnimatedFavButton2()
}
}
}
}
使用Box布局组件叠加TopBar组件和LayColumn列表组件,TopBar固定高度,LayColumn顶部预留出对应TopBar高度的padding距离
如果LayColumn列表向上滑动时,就修改opBar高度为0,同时修改LayColumn顶部的padding为0,反之则都修改为默认的固定高度值
TopBar组件上可以应用Modifier.animateContentSize(), 当高度被修改变化时,会执行动画效果,LayColumn组件可以使用animateDpAsState估值padding自动执行属性动画效果
判断LayColumn列表向上滑动的条件:先rememberLazyListState(),然后判断其firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0就是向上滚动了,TopBar组件和LayColumn组件都要依赖观察该状态值来修改高度和padding
import androidx.compose.runtime.Composable
import androidx.compose.material.Text
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
val TOP_BAR_HEIGHT = 56.dp
val LazyListState.isScrolled: Boolean
get() = firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0
@Composable
fun AnimationTopBarDemo() {
val lazyListState = rememberLazyListState()
Scaffold(
content = {
Box(modifier = Modifier.padding(it).fillMaxSize()) {
MainContent(lazyListState = lazyListState)
TopBar(lazyListState = lazyListState)
}
}
)
}
@Composable
fun TopBar(lazyListState: LazyListState) {
TopAppBar(
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colors.primary)
.animateContentSize(animationSpec = tween(durationMillis = 300))
.height(height = if (lazyListState.isScrolled) 0.dp else TOP_BAR_HEIGHT),
contentPadding = PaddingValues(start = 16.dp)
) {
Text(
text = "Title",
style = TextStyle(
fontSize = MaterialTheme.typography.h6.fontSize,
color = MaterialTheme.colors.surface
)
)
}
}
@Composable
fun MainContent(lazyListState: LazyListState) {
val numbers = remember { List(size = 25) { it } }
val padding by animateDpAsState(
targetValue = if (lazyListState.isScrolled) 0.dp else TOP_BAR_HEIGHT,
animationSpec = tween(durationMillis = 300)
)
LazyColumn(
modifier = Modifier.padding(top = padding),
state = lazyListState
) {
items(items = numbers, key = { it }) {
NumberHolder(number = it)
}
}
}
@Composable
fun NumberHolder(number: Int) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = number.toString(),
style = TextStyle(
fontSize = MaterialTheme.typography.h3.fontSize,
fontWeight = FontWeight.Bold
)
)
}
}