1. 简单值动画
//将一个Color简单值 从一个值 变化到另一个 另一个简单值 就用 animateColorAsState
val backgroundColor by animateColorAsState(if (tabPage == TabPage.Home) Purple100 else Green300)
动画其实就是 一个状态不停发生改变导致 组件不断重组产生的过程
2. 可见性动画
2.1 按钮展开收缩
LazyColumn
val lazyListState = rememberLazyListState()
/**
* Returns whether the lazy list is currently scrolling up.
* 返回LazyColumn是否 当前向上滚动
*/
@Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
// Toggle the visibility of the content with animation.
AnimatedVisibility(visible = extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
可见性动画 AnimatedVisibility
2.2 消息从顶部滑入和滑出
@Composable
private fun EditMessage(shown: Boolean) {
Log.d("ning","".plus(shown))
AnimatedVisibility(
visible = shown,
//垂直出来
enter = slideInVertically(
// 通过从 initialOffsetY 向下滑动到 0 来进入
initialOffsetY = { fullHeight -> fullHeight },
//这里要传入一个带参数的函数,返回的是你需要告诉动画系统控件初始位置或结束位置,参数是动画系统告诉你的控件的高度
//LinearOutSlowInEasing:传入元素使用减速缓和设置动画,减速缓和以峰值速度(元素移动的最快点)开始过渡 , 慢慢减速 ,直到停止
animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)//帧动画
),
//垂直回去
exit = slideOutVertically(
// 通过从 initialOffsetY 向上滑动到 targetOffsetY 来退出
targetOffsetY = { fullHeight -> -fullHeight },
//FastOutLinearInEasing :退出屏幕的元素使用加速度缓和,从静止开始,以峰值速度结束
animationSpec = tween(durationMillis = 150, easing = FastOutLinearInEasing)
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
shadowElevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
}
3.内容大小动画
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
// This `Column` animates its size when its content changes.
//加了,当内容发生变化的时候,会慢慢的撑开
.animateContentSize()
) {
Row {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = topic,
style = MaterialTheme.typography.bodyLarge
)
}
if (expanded) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.lorem_ipsum),
textAlign = TextAlign.Justify
)
}
}
4.多值动画
@Composable
private fun HomeTabBar(
backgroundColor: Color,
tabPage: TabPage,
onTabSelected: (tabPage: TabPage) -> Unit
) {
TabRow(
selectedTabIndex = tabPage.ordinal,
containerColor = backgroundColor,
indicator = { tabPositions ->
HomeTabIndicator(tabPositions, tabPage)
}
) {
HomeTab(
icon = Icons.Default.Home,
title = stringResource(R.string.home),
onClick = { onTabSelected(TabPage.Home) }
)
HomeTab(
icon = Icons.Default.AccountBox,
title = stringResource(R.string.work),
onClick = { onTabSelected(TabPage.Work) }
)
}
}
@Composable
private fun HomeTabIndicator(
//TabPosition 当前选项卡的位置信息
//对应tabrow里面的组件集合 有顺序索引的
tabPositions: List<TabPosition>,
tabPage: TabPage
) {
// 表示当前选择的选项卡的指示器。
// 默认情况下,这将是一个 TabRowDefaults.Indicator,
// 使用 TabRowDefaults.tabIndicatorOffset 修饰符对其位置进行动画处理。
// 请注意,此指示器将强制填满整个选项卡,
// 因此您应该使用 TabRowDefaults.tabIndicatorOffset 或类似工具在此空间内对实际绘制的指示器进行动画处理,并从头开始提供偏移量。
//自定义选项卡指示器
//多值动画 :在状态发生改变时,有多个动画值要一起发生改变
//设置一个Transition 并使用 targetState 提供的目标 对其进行更新.
//当 targetState 更改时,Transition 将朝着为 新 targetState 指定的目标值 运行其所有子动画
//可以使用 Transition 动态添加子动画 :Transition.animateFloat 、animateColor、 animateValue 等。
val transition = updateTransition(
tabPage,//目标状态
label = "Tab indicator"
)
//当前选项卡的位置信息的左侧 用动画的方式描绘当前选项卡位置信息的左侧
val indicatorLeft by transition.animateDp(
//实现弹性效果
transitionSpec = {
//指标向右移动,左边缘比右边缘移动得慢
if (TabPage.Home isTransitioningTo TabPage.Work) {
spring(
//刚度
// 右边比左边快
stiffness = Spring.StiffnessVeryLow
)
} else {
spring(
//刚度
stiffness = Spring.StiffnessMedium
)
}
},
label = "Indicator left"
) { tabPage ->//参数就是那个枚举状态
tabPositions[tabPage.ordinal].left
}
//当前选项卡的位置信息的右侧
val indicatorRight by transition.animateDp(
//实现弹性效果
transitionSpec = {
//指标向右移动,左边缘比右边缘移动得慢
if (TabPage.Home isTransitioningTo TabPage.Work) {
spring(
//刚度
// 右边比左边快
stiffness = Spring.StiffnessMedium
)
} else {
spring(
//刚度
stiffness = Spring.StiffnessVeryLow
)
}
},
label = "Indicator right"
) { tabPage ->//参数就是那个枚举状态
tabPositions[tabPage.ordinal].right
}
val color by transition.animateColor(
label = "Border color"
) { page ->
if (page == TabPage.Home) Purple700 else Green800
}
Box(
Modifier
//这个修饰符用于包裹 Box 的内容,使其尺寸与内容尺寸相匹配,并将内容对齐到底部的起始位置。
.wrapContentSize(align = Alignment.BottomStart)
.offset(x = indicatorLeft)//偏移量
.width(indicatorRight - indicatorLeft)
.padding(4.dp)
//这个一定要放下面,要不然撑不开
.fillMaxSize()
.border(
BorderStroke(2.dp, color),
RoundedCornerShape(4.dp)
)
)
}
5.重复动画
@Composable
private fun LoadingRow() {
// Creates an `InfiniteTransition` that runs infinite child animation values.
//无限
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
// `infiniteRepeatable` repeats the specified duration-based `AnimationSpec` infinitely.
animationSpec = infiniteRepeatable(
// The `keyframes` animates the value by specifying multiple timestamps.
animation = keyframes {
//关键字
// One iteration is 1000 milliseconds.
durationMillis = 1000 //一次动画的时间 就是从0-1的时间
// 0.7f at the middle of an iteration.
//500毫秒的时候 alpha的值是1f
1f at 1000 //外面延迟 协程3秒 所以 执行了三次
},
// When the value finishes animating from 0f to 1f, it repeats by reversing the
// animation direction.
repeatMode = RepeatMode.Reverse // 0 - 1 -> 0 ->1
), label = ""
)
Row(
modifier = Modifier
.heightIn(min = 64.dp)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.LightGray.copy(alpha = alpha))
)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(Color.LightGray.copy(alpha = alpha))
)
}
}
6.手势动画
/**
* The modified element can be horizontally swiped away.
* 修改后的元素可以水平滑动
* 当元素轻扫到屏幕边缘时调用。
* @param onDismissed Called when the element is swiped to the edge of the screen.
*/
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
//composed 合成的意思
): Modifier = composed {
val offsetX = remember {
Animatable(0f)
}
pointerInput(Unit) {
//衰减动画通常在投掷姿势后使用,用于计算投掷动画最后的固定位置 样条衰减 指数衰减 两个衰减器 这个是样条衰减
val decay = splineBasedDecay<Float>(this)
coroutineScope {
//可以做一些手势相关的操作
//创建一个修饰符,用于处理修改元素区域内的光标输入
//pointerInputs 可以调用 PointerInputScope.awaitPointerEventScope,
//以安装可以等待PointerEventScope 的光标输入处理程序
while (true) {
//等待触摸按下事件
//awaitPointerEventScope :挂起并安装指针输入块,该块可以等待输入事件并立即响应他们
//awaitFirstDown: 读取事件,直到收到第一个 down
val pointerId = awaitPointerEventScope {
awaitFirstDown().id
}
val velocityTracker = VelocityTracker()//专门计算速度的类
//等待拖动事件
awaitPointerEventScope {
//监听水平滑动
horizontalDrag(pointerId) { change ->
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
offsetX.snapTo(horizontalDragOffset)//平滑过渡的效果
}
//记录滑动的位置
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)//偏移量 与 所用时间
//消费掉手势事件,而不是传递给外部
change.consume()
}
}
// 拖动完成,计算投掷的速度
val velocity = velocityTracker.calculateVelocity().x
//我们需要计算投掷的最终位置,以决定是将元素划回原始位置,还是将其划开并调用回调
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
offsetX.updateBounds(lowerBound = -size.width.toFloat(),upperBound = size.width.toFloat())
launch {
if (targetOffsetX.absoluteValue <= size.width) {
//元素范围之内 划回来
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity) //以原来的速度划回来
} else {
//启动衰减动画
offsetX.animateDecay(velocity,decay)
onDismissed()
}
}
}
}
}.offset {
IntOffset(offsetX.value.roundToInt(), 0)
}
}
/**
* Shows a row for one task.
*
* @param task The task description.
* @param onRemove Called when the task is swiped away and removed.
*/
@Composable
private fun TaskRow(task: String, onRemove: () -> Unit) {
Surface(
modifier = Modifier
.fillMaxWidth()
.swipeToDismiss(onRemove),
shadowElevation = 2.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = task,
style = MaterialTheme.typography.bodyLarge
)
}
}
}