当前位置: 首页> 文旅> 旅游 > 公众号微官网_凡科网小程序怎么样_南昌网站建设_什么是引流推广

公众号微官网_凡科网小程序怎么样_南昌网站建设_什么是引流推广

时间:2025/7/12 13:59:12来源:https://blog.csdn.net/tmacfrank/article/details/146244901 浏览次数:2次
公众号微官网_凡科网小程序怎么样_南昌网站建设_什么是引流推广

1、Block 参数:监听每一帧

animateTo() 与 animateDecay() 中都有一个函数类型的 block 参数:

	suspend fun animateDecay(initialVelocity: T,animationSpec: DecayAnimationSpec<T>,block: (Animatable<T, V>.() -> Unit)? = null): AnimationResult<T, V>

block 会在动画执行的每一帧刷新时被调用,相当于是对动画的监听。举个简单例子,让红色的方块跟随绿色方块一起滑动相同的距离:

	override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {val anim = remember { Animatable(0.dp, Dp.VectorConverter) }var paddingRed by remember { mutableStateOf(anim.value) }val decay = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {anim.animateDecay(1000.dp, decay) { // this: AnimatablepaddingRed = value}}Row {Box(Modifier.padding(0.dp, anim.value, 0.dp, 0.dp).size(100.dp).background(Color.Green))Box(Modifier.padding(0.dp, paddingRed, 0.dp, 0.dp).size(100.dp).background(Color.Red))}}}

2、动画的边界限制、结束和取消

本节介绍动画的非正常终止,即还没结束就被叫停了的情况,大致有如下三种:

  1. 新动画的执行会导致正在执行的动画被打断:同一个 Animatable 对象在第一个动画还没执行结束时就开启了第二个动画,Compose 会直接结束第一个动画。
  2. 主动结束一个动画的执行
  3. 动画的值到达边界后结束,特殊场景,前两种属于打断,而这一种是动画边界的触发,属于正常结束
	override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {val anim = remember { Animatable(0.dp, Dp.VectorConverter) }val decay = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {delay(1000)try {anim.animateDecay(2000.dp, decay)} catch (e: CancellationException) {// 协程取消时会抛出 CancellationException,为了演示效果才捕获这个异常,// 正常开发中一定不要捕获它,因为一旦捕获了会影响协程的结构化取消println("糟糕!被打断了")}}// 第二个协程比第一个协程多延时一些时间以打断第一个动画的执行LaunchedEffect(Unit) {delay(1500)anim.animateDecay((-1000).dp, decay)}Box(Modifier.padding(0.dp, anim.value, 0.dp, 0.dp).size(100.dp).background(Color.Green))}}

运行代码发现 Box 在向下移动过程中会向上移动,并且打印"糟糕!被打断了"。说明第一个协程内抛出了 CancellationException,证明第一个协程确实被取消了,协程取消就会导致协程内部执行的动画取消。

animateDecay()、animateTo() 以及 snapTo() 都会导致正在执行的动画被打断。

业务开发中一定会有需要主动停止动画执行的场景,调用 Animatable 的 stop() 即可停止动画,该函数是一个挂起函数,也应该在协程环境中调用。使用时需要注意它不应该紧接着一个执行动画的函数后面调用,例如:

LaunchedEffect(Unit) {delay(1500)anim.animateDecay((-1000).dp, decay)anim.stop()
}

因为执行动画的函数也是挂起函数,在该挂起函数运行完毕之前,stop() 得不到执行。也就是说,这种情况下,stop() 得以执行时,动画已经运行完毕,也就失去调用 stop() 的意义了。因此通常情况下 stop() 应该是在另一个协程中,执行一些代码之后被调用。

此外,还需注意,stop() 一般是与业务逻辑相关的,而不是与 Compose 界面相关的。比如点击了界面的某个按钮,触发了相应的业务流程后结束动画,这时使用传统的启动协程的方式就可以了,比如 lifecycleScope.launch(),在这里面调用 stop(),而不是在 LaunchedEffect() 中。

调用 stop() 也会引发正在执行中的动画所在的协程抛出 CancellationException,示例代码:

	override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {val anim = remember { Animatable(0.dp, Dp.VectorConverter) }val decay = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {delay(1000)try {anim.animateDecay(2000.dp, decay)} catch (e: CancellationException) {println("糟糕!被打断了")}}// 调用 stop() 也会引发正在执行的动画所在的协程抛 CancellationExceptionLaunchedEffect(Unit) {delay(1200)anim.stop()}Box(Modifier.padding(0.dp, anim.value, 0.dp, 0.dp).size(100.dp).background(Color.Green))}}

Compose 认为的动画正常停止有两种情况:动画正常运行完毕以及动画运行到边界条件,这个信息可以通过 animateTo() 和 animateDecay() 的返回值类型 AnimationResult 中查看到:

class AnimationResult<T, V : AnimationVector>(val endState: AnimationState<T, V>,val endReason: AnimationEndReason
)enum class AnimationEndReason {/*** 动画值到达上限或下限时会被强制结束,这种状态下结束通常会比初始目标值短一些,* 并且剩余速度通常不为零。可以通过 [AnimationResult] 获取结束值和剩余速度。*/BoundReached,/*** 动画已经成功完成,没有任何中断。*/Finished
}

代码示例:

	override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {// 用以获取整个屏幕的最大宽度从而设置动画边界BoxWithConstraints {val anim = remember { Animatable(0.dp, Dp.VectorConverter) }val decay = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {delay(1000)anim.animateDecay(2000.dp, decay)}// 更新动画的边界,上边界应该是最大宽度减去 Box 的宽度,即是 Box 左边的最大位移值anim.updateBounds(upperBound = maxWidth - 100.dp)Box(Modifier.padding(anim.value, 0.dp, 0.dp, 0.dp).size(100.dp).background(Color.Green))}}}

上面是一维动画,由于 Compose 支持多维动画,最多是到四维。在多维空间中,只要有一个维度到达边界,Compose 就会停止动画。但假如需求是多维动画的所有维度均到达边界后才停止动画,该如何实现呢?

一种比较直观的想法是在一个维度到达边界后,通过 AnimationResult 的 endReason 先判断是因为到达边界引发的动画停止,然后获取 endState 从中获取尚未到达边界的维度数据,然后用这个数据在该维度重新开启一段动画,直到其到达边界。以二维 Offset 为例的代码如下:

	override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {BoxWithConstraints {// Offset 动画val anim = remember { Animatable(DpOffset.Zero, DpOffset.VectorConverter) }val decay = remember { exponentialDecay<DpOffset>() }// Y 轴动画val animY = remember { Animatable(0.dp, Dp.VectorConverter) }val decayY = remember { exponentialDecay<Dp>() }// Offset 动画是否已经结束并开始 Y 轴动画了var startY by remember { mutableStateOf(false) }LaunchedEffect(Unit) {delay(1000)val result = anim.animateDecay(DpOffset(2000.dp, 3000.dp), decay)if (result.endReason == AnimationEndReason.BoundReached) {// 开始 Y 轴动画,用 Offset 动画结束时 Y 轴速度作为 Y 轴衰减动画的初速度startY = trueval initVelocityY = result.endState.velocity.yanimY.animateDecay(initVelocityY, decayY)}}// 更新动画的边界,上边界应该是最大宽度减去 Box 的宽度,即是 Box 左边的最大位移值anim.updateBounds(upperBound = DpOffset(maxWidth - 100.dp, maxHeight - 100.dp))// Y 轴动画边界最大值应该是屏幕高度减去 Box 高度再减去 Offset 动画在 Y 轴上的位移animY.updateBounds(upperBound = maxHeight - 100.dp - anim.value.y)Box(Modifier.padding(anim.value.x,if (!startY) anim.value.y else (anim.value.y + animY.value),0.dp,0.dp).size(100.dp).background(Color.Green))}}}

经过上述一通操作实现了如下效果:

请添加图片描述

上述操作实际上是有些繁琐的,还有一种比较简便的思路,就是将多个维度的动画分开实现。实际上,原生的 OverScroller 就是这样做的:

public class OverScroller {private final SplineOverScroller mScrollerX;@UnsupportedAppUsageprivate final SplineOverScroller mScrollerY;public void fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY, int overX, int overY) {// Continue a scroll or fling in progressif (mFlywheel && !isFinished()) {float oldVelocityX = mScrollerX.mCurrVelocity;float oldVelocityY = mScrollerY.mCurrVelocity;if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&Math.signum(velocityY) == Math.signum(oldVelocityY)) {velocityX += oldVelocityX;velocityY += oldVelocityY;}}mMode = FLING_MODE;// 两个方向上分别惯性滑动mScrollerX.fling(startX, velocityX, minX, maxX, overX);mScrollerY.fling(startY, velocityY, minY, maxY, overY);}
}

因此上面的例子也可以采用这种方式,代码会方便很多:

@Composable
fun BoundsSample2() {BoxWithConstraints {// X 轴动画val animX = remember { Animatable(0.dp, Dp.VectorConverter) }val decayX = remember { exponentialDecay<Dp>() }// Y 轴动画val animY = remember { Animatable(0.dp, Dp.VectorConverter) }val decayY = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {delay(1000)animX.animateDecay(2000.dp, decayX)}LaunchedEffect(Unit) {delay(1000)animY.animateDecay(3000.dp, decayY)}animX.updateBounds(upperBound = maxWidth - 100.dp)animY.updateBounds(upperBound = maxHeight - 100.dp)Box(Modifier.padding(animX.value, animY.value, 0.dp, 0.dp).size(100.dp).background(Color.Green))}
}

下面再看一个复杂一点的需求,让 Box 碰到边缘后反弹:

请添加图片描述

示例代码如下:

@Composable
fun BoundsSample3() {BoxWithConstraints {// X 轴动画val animX = remember { Animatable(0.dp, Dp.VectorConverter) }val decayX = remember { exponentialDecay<Dp>() }// Y 轴动画val animY = remember { Animatable(0.dp, Dp.VectorConverter) }val decayY = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {delay(1000)var result = animX.animateDecay(4000.dp, decayX)// 每次碰到边界都反速运行动画while (result.endReason == AnimationEndReason.BoundReached) {result = animX.animateDecay(-result.endState.velocity, decayX)}}LaunchedEffect(Unit) {delay(1000)animY.animateDecay(2000.dp, decayY)}// 需要更新一下下界,因为动画 value 要作为 padding 不能是负值animX.updateBounds(upperBound = maxWidth - 100.dp, lowerBound = 0.dp)animY.updateBounds(upperBound = maxHeight - 100.dp)Box(Modifier.padding(animX.value, animY.value, 0.dp, 0.dp).size(100.dp).background(Color.Green))}
}

这样做能实现一个大致准确的动画,之所以说大致准确,是因为速度 AnimationResult.endState.velocity 的精度不够高。Compose 在动画的每一帧中去采集图形的位置,再通过计算时间段进行相除来计算出实时速度。这个计算出来的速度显然与 Box 解除到边界的一瞬间的速度是有毫厘之差的。

我们可以自己进行数学计算,得到一个没有误差的精确值,这里仅以 X 轴方向为例,主要的改进在于 paddingX 的计算上:

@Composable
fun BoundsSample4() {BoxWithConstraints {// X 轴动画val animX = remember { Animatable(0.dp, Dp.VectorConverter) }val decayX = remember { exponentialDecay<Dp>() }// Y 轴动画val animY = remember { Animatable(0.dp, Dp.VectorConverter) }val decayY = remember { exponentialDecay<Dp>() }LaunchedEffect(Unit) {delay(1000)var result = animX.animateDecay(4000.dp, decayX)while (result.endReason == AnimationEndReason.BoundReached) {result = animX.animateDecay(-result.endState.velocity, decayX)}}LaunchedEffect(Unit) {delay(1000)animY.animateDecay(2000.dp, decayY)}animY.updateBounds(upperBound = maxHeight - 100.dp)// 自己计算 X 轴的距离val paddingX = remember(animX.value) {var usedValue = animX.value// 将从左到右,再从右到左这一个来回视为一次循环,碰撞后的位移,就是总的动画值// 这个位移对一次循环求模的结果while (usedValue >= (maxWidth - 100.dp) * 2) {usedValue -= (maxWidth - 100.dp) * 2}if (usedValue < maxWidth - 100.dp) {// 从左到右移动过程中,paddingX 应该就是求模后的 usedValueusedValue} else {// 从右向左移动过程中,paddingX 应该是一个来回的距离减去已经移动过的距离(maxWidth - 100.dp) * 2 - usedValue}}Box(Modifier// 使用 paddingX 作为 start 值.padding(paddingX, animY.value, 0.dp, 0.dp).size(100.dp).background(Color.Green))}
}

实测发现精确值与前面通过 AnimationResult 提供的速度值计算出的结果确实存在一定偏差。

3、Transition

Compose 中的 Transition 是指 Compose 内部的转场动画,这个词在 Android 原生中也是有的,只不过是负责 Activity 或 Fragment 的转场动画。由于 Activity 与 Fragment 是 View 以外更高层次的结构,因此 View 中的动画是无法应用到 Activity 与 Fragment 上的,只能借助 Transition 实现它们的转场动画。

实际上前面我们已经讲过一种转场动画的实现方式:【状态转移动画?】 animateXxxAsState(),那为什么还要单独再提供一种 Transition?下面我们来详细介绍。

3.1 多属性的状态切换

作为对比,我们还是先用状态转移动画 API animateXxxAsState() 实现一个简单的动画:

@Composable
fun TransitionSample() {var big by remember { mutableStateOf(false) }val size by animateDpAsState(if (big) 96.dp else 48.dp)Box(modifier = Modifier.size(size).background(Color.Green).clickable { big = !big })
}

它的效果是点击绿色的 Box 就会变大或变小,这个效果如果通过 Transition 实现,应该是下面这样的:

@Composable
fun TransitionSample() {var big by remember { mutableStateOf(false) }
//    val size by animateDpAsState(if (big) 96.dp else 48.dp)val bigTransition = updateTransition(targetState = big)val size by bigTransition.animateDp { // it 是 state 对象,也就是 Booleanif (it) 96.dp else 48.dp}Box(modifier = Modifier.size(size).background(Color.Green).clickable { big = !big })
}

Transition 实现同样的动画效果要比使用 animateDpAsState() 多出一步,你需要先创建一个 Transition 对象,然后调用相应的函数创建动画。

我们先看这两步的具体内容。

Transition 的创建与使用

首先是 updateTransition() 创建 Transition 对象:

@Composable
fun <T> updateTransition(targetState: T,label: String? = null
): Transition<T> {// 创建 Transition 对象并通过 remember 缓存,无参 remember 只会在第一次调用时执行,// 从而保证了 Transition 只被创建一次val transition = remember { Transition(targetState, label = label) }// 动画转移到目标状态transition.animateTo(targetState)DisposableEffect(transition) {onDispose {// Clean up on the way out, to ensure the observers are not stuck in an in-between// state.transition.onTransitionEnd()}}return transition
}

源码表明,updateTransition() 实际上是做两件事:创建 Transition 与更新状态。注意它只更新了状态,让 Transition 处于旧状态与新状态之间的中间状态,但是不负责根据状态的转移生成动画。你需要调用它的返回值 Transition 提供的对应的函数以展示状态转移动画,比如 animateDp()、animateInt()、animateFloat() 等等。

接下来我们再看 animateXxx() 的内容,以示例代码使用的 animateDp() 为例:

@Composable
inline fun <S> Transition<S>.animateDp(noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Dp> = {spring(visibilityThreshold = Dp.VisibilityThreshold)},label: String = "DpAnimation",targetValueByState: @Composable (state: S) -> Dp // 泛型 S 表示状态类型
): State<Dp> =animateValue(Dp.VectorConverter, transitionSpec, label, targetValueByState)

第三个参数 targetValueByState 是一个 Composable 函数,作用是根据传入的目标状态参数 state 计算出动画的目标值。

第二个参数 label 是在 Android Studio 的预览模式下,区分同一个 Transition 下不同动画的标识。具体说来,就是给可组合函数加上 @Preview 注解进入模式时,可以点击如下图所示的按钮进入动画预览模式:

请添加图片描述

在动画预览模式下,可以看到 Transition 下的状态以及所有动画:

请添加图片描述

当然,由于我们目前使用的是一个非常简单的例子,因此你可以很容易的判断出,左侧方框内的 Boolean 表示的是 big 状态,而右侧方框内的 DpAnimation : 48.0dp 表示的是 size(DpAnimation 是参数默认值)。但倘若一个非常复杂的动画,它的 Transition 内部可能会包含很多的状态和动画值,如果没有标识就很难区分它们。所以,第二个参数的 label 此时就派上用场了,当你调用 updateTransition() 和 animateDp() 时传入一个特定的 label,在预览时就能区分它们了:

@Preview
@Composable
fun TransitionSample() {var big by remember { mutableStateOf(false) }
//    val size by animateDpAsState(if (big) 96.dp else 48.dp)val bigTransition = updateTransition(big, "big")val size by bigTransition.animateDp(label = "size") {if (it) 96.dp else 48.dp}Box(modifier = Modifier.size(size).background(Color.Green).clickable { big = !big })
}

效果如下:

在这里插入图片描述

最后说第一个参数 transitionSpec,它是一个返回有限动画 FiniteAnimationSpec 的 Composable 函数,animateDp() 的动画曲线就是用这个函数生成的。它要求你传一个函数而不是直接提供一个 FiniteAnimationSpec 对象,是为了方便在不同的状态下提供不同的 FiniteAnimationSpec,比如:

val size by bigTransition.animateDp({if (!initState && targetState) spring() else tween() }, label = "size") { if (it) 96.dp else 48.dp }

initState 与 targetState 是 Segment 提供的。Segment 是指状态转移中的某一段状态,可以提供这一段的状态信息。Segment 提供了简便方法 isTransitioningTo():

infix fun S.isTransitioningTo(targetState: S): Boolean {return this == initialState && targetState == this@Segment.targetState
}

因此上面的等价写法是:

val size by bigTransition.animateDp({if (false isTransitioningTo true) spring() else tween() }, label = "size") { if (it) 96.dp else 48.dp }

这样更加直观的表达出状态从 false 转移到 true。

如果想创建一个无限循环类型的 Transition,可以使用 rememberInfiniteTransition(),它会返回一个 InfiniteTransition。

Transition 与 animateXxxAsState() 的区别

既然二者能实现相同的效果,那么问题来了,为啥还要多提供一个 Transition?它用起来还比 animateXxxAsState() 多一些步骤。因为二者的关注点不同:animateDpAsState() 面向属性本身,而 Transition 面向属性背后的状态,这样就容易建立多属性的状态模型。

比如,给上例增加一个功能,在点击时改变图形的圆角大小:

@Composable
fun TransitionSample() {var big by remember { mutableStateOf(false) }val bigTransition = updateTransition(targetState = big)val size by bigTransition.animateDp { if (it) 96.dp else 48.dp }val corner by bigTransition.animateDp { if (it) 0.dp else 18.dp }Box(modifier = Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Green).clickable { big = !big })
}

假如使用 animateDpAsState() 来计算 size 和 corner,那么 Compose 会为每个属性启动一个协程进行计算动画,而使用 Transition 则只开一个协程,在 Transition 内部对所有的属性动画做统一管理。因此属性越多,Transition 的性能优势越明显。

此外还有一个好处,就是前面介绍 animateDp() 的第二个参数 label 时说到可以进行动画的预览,这个是 animateXxxAsState() 所不具备的功能。

还有,经过前面对 Transition 的介绍,你会发现它与 animateXxxAsState() 并没有本质上的区别,都是针对状态转移的。只不过一个是瞄准的是具体的属性,一个瞄准的是所有状态。因此,animateDpAsState() 具有的弱点 —— 不能设置动画的初始值,Transition 也是有的。虽然不能直接设置初始值,但是 Transition 可以通过一种比较迂回的方式去设置动画的初始值。先观察 updateTransition() 的源码:

@Composable
fun <T> updateTransition(targetState: T,label: String? = null
): Transition<T> {val transition = remember { Transition(targetState, label = label) }transition.animateTo(targetState)DisposableEffect(transition) {onDispose {// Clean up on the way out, to ensure the observers are not stuck in an in-between// state.transition.onTransitionEnd()}}return transition
}

通过 Transition 的次构造函数传入 targetState:

@Stable
class Transition<S> @PublishedApi internal constructor(private val transitionState: MutableTransitionState<S>,val label: String? = null
) {internal constructor(initialState: S,label: String?) : this(MutableTransitionState(initialState), label)
}

次构造调用主构造时会将 targetState 包装进 MutableTransitionState 对象中,实际上真正进行状态管理的就是 MutableTransitionState。

既然如此,我们可以在创建 Transition 时,使用另一个 updateTransition() 直接传入 MutableTransitionState:

@Composable
fun <T> updateTransition(transitionState: MutableTransitionState<T>,label: String? = null
): Transition<T> {val transition = remember(transitionState) {Transition(transitionState = transitionState, label)}transition.animateTo(transitionState.targetState)DisposableEffect(transition) {onDispose {// Clean up on the way out, to ensure the observers are not stuck in an in-between// state.transition.onTransitionEnd()}}return transition
}

这样做的好处是,可以在界面刚开始展示时,就直接开始动画:

@Preview
@Composable
fun TransitionSample() {var big by remember { mutableStateOf(true) }val bigState = remember { MutableTransitionState(!big) }bigState.targetState = bigval bigTransition = updateTransition(bigState, "big")val size by bigTransition.animateDp(label = "size") { if (it) 96.dp else 48.dp }val corner by bigTransition.animateDp(label = "corner") { if (it) 0.dp else 18.dp }Box(modifier = Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Green).clickable { big = !big })
}

这一点是 animateXxxAsState() 做不到的。

3.2 AnimatedVisibility()

AnimatedVisibility() 会让其内容的显示与隐藏以动画的方式呈现,而不是突然的出现或消失。

举一个简单的代码示例,点击按钮切换方块的显示或隐藏:

@Composable
fun AnimatedVisibilitySample() {Column {var shown by remember { mutableStateOf(true) }if (shown) {Box(modifier = Modifier.size(100.dp).background(Color.Green))}Button(onClick = { shown = !shown }) {Text("切换")}}
}

上述代码完成的是没有动画效果的显示与隐藏,将 if 修改为 AnimatedVisibility() 即可实现动画效果:

请添加图片描述

可以看到,消失的动画是从下至上的,而显示的动画是由上至下的。这是因为,我们在 Column 中调用的 AnimatedVisibility() 实际上是 ColumnScope 的扩展函数:

@Composable
fun ColumnScope.AnimatedVisibility(visible: Boolean,modifier: Modifier = Modifier,enter: EnterTransition = fadeIn() + expandVertically(),exit: ExitTransition = fadeOut() + shrinkVertically(),label: String = "AnimatedVisibility",content: @Composable AnimatedVisibilityScope.() -> Unit
) {val transition = updateTransition(visible, label)AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

该扩展函数的参数 enter 指定了入场动画,默认值是 fadeIn() + expandVertically(),即淡入 + 垂直展开动画,exit 指定出场动画,默认值是 fadeOut() + shrinkVertically(),即淡出 + 垂直收缩。演示效果正是因为使用了这两个参数的默认值。

既然 Column 有扩展函数,那么 Row 也会有相应的扩展函数:

@Composable
fun RowScope.AnimatedVisibility(visibleState: MutableTransitionState<Boolean>,modifier: Modifier = Modifier,enter: EnterTransition = expandHorizontally() + fadeIn(),exit: ExitTransition = shrinkHorizontally() + fadeOut(),label: String = "AnimatedVisibility",content: @Composable() AnimatedVisibilityScope.() -> Unit
) {val transition = updateTransition(visibleState, label)AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

而如果不是以上两种布局,就会使用通用的 AnimatedVisibility():

@Composable
fun AnimatedVisibility(visibleState: MutableTransitionState<Boolean>,modifier: Modifier = Modifier,enter: EnterTransition = fadeIn() + expandIn(),exit: ExitTransition = fadeOut() + shrinkOut(),label: String = "AnimatedVisibility",content: @Composable() AnimatedVisibilityScope.() -> Unit
) {val transition = updateTransition(visibleState, label)AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

AnimatedVisibility() 在内部先用 updateTransition() 创建出一个 Transition 对象并使用该对象进行后续的动画操作,因此 AnimatedVisibility() 可以视为对 updateTransition() 的扩展。

下面来详细看一下 enter 与 exit 的类型 EnterTransition 与 ExitTransition,由于二者相似,因此我们只详解 EnterTransition:

@Immutable
sealed class EnterTransition {internal abstract val data: TransitionData
}@Immutable
private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()

EnterTransition 是一个密封类,它只有一个子类 EnterTransitionImpl,实现了父类的抽象属性 data,类型为 TransitionData:

@Immutable
internal data class TransitionData(val fade: Fade? = null, // 淡入淡出val slide: Slide? = null, // 滑动val changeSize: ChangeSize? = null, // 尺寸改变,裁切方式val scale: Scale? = null // 缩放
)

TransitionData 定义了四个属性,实际上就是四种类型的动画。在使用时,不用自己创建这四种类型的对象再传进来,可以直接使用 Compose 提供的函数,比如淡入效果 fadeIn() 会完成整个创建流程并返回一个 EnterTransitionImpl:

@Stable
fun fadeIn(animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),initialAlpha: Float = 0f
): EnterTransition {// 为 EnterTransitionImpl 传入 TransitionData,该 TransitionData 指定了 fade 属性return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}

我们可以传入定制的 FiniteAnimationSpec 以及初始透明度:

@Composable
fun AnimatedVisibilitySample() {Column {var shown by remember { mutableStateOf(true) }AnimatedVisibility(shown, enter = fadeIn(tween(4000), 0.3f)) {Box(modifier = Modifier.size(100.dp).background(Color.Green))}Button(onClick = { shown = !shown }) {Text("切换")}}
}

为了让动画效果显得明显一些,特意将动画时长设置为 4 秒,效果如下:

请添加图片描述

同理,slide 对应的入场效果动画是 slideIn():

@Stable
fun slideIn(animationSpec: FiniteAnimationSpec<IntOffset> =spring(stiffness = Spring.StiffnessMediumLow,visibilityThreshold = IntOffset.VisibilityThreshold),initialOffset: (fullSize: IntSize) -> IntOffset, 
): EnterTransition {return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}

必须要给 initialOffset 这个函数参数赋值,因为需要通过它来确定从哪个位置滑入:

@Composable
fun AnimatedVisibilitySample() {Column {var shown by remember { mutableStateOf(true) }AnimatedVisibility(shown, enter = slideIn { IntOffset(-it.width, -it.height) }) {Box(modifier = Modifier.size(100.dp).background(Color.Green))}Button(onClick = { shown = !shown }) {Text("切换")}}
}

initialOffset() 在参数中提供了组件的尺寸 fullSize,因此可以很容易地获取到左上顶点的偏移量,据此创建 IntOffset 并返回即可。效果如下:

请添加图片描述

此外,slideIn() 还有只进行垂直方向滑入或水平方向滑入的衍生版本 slideInVertically() 与 slideInHorizontally(),只需要传入对应方向的一维偏移量即可,不多赘述。

changeSize 的入场动画由 expandIn() 提供:

@Stable
fun expandIn(animationSpec: FiniteAnimationSpec<IntSize> =spring(stiffness = Spring.StiffnessMediumLow,visibilityThreshold = IntSize.VisibilityThreshold),expandFrom: Alignment = Alignment.BottomEnd,clip: Boolean = true,initialSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
): EnterTransition {return EnterTransitionImpl(TransitionData(changeSize = ChangeSize(expandFrom, initialSize, animationSpec, clip)))
}

这种是指裁切方式入场,默认是保留组件右下角的区域,然后在组件左上角的位置,慢慢展开保留区域直到展示出整个组件。我们先只给 animationSpec 传一个时长为 5 秒的 TweenSpec 来观察 expandIn() 的默认效果,并且为了证明默认保留右下角区域,我们在绿色方块的右下角放了一个小的红色方块作为参照,代码如下:

@Composable
fun AnimatedVisibilitySample() {Column {var shown by remember { mutableStateOf(true) }AnimatedVisibility(shown, enter = expandIn(tween(5000))) {Box(modifier = Modifier.size(100.dp).background(Color.Green)) {Box(modifier = Modifier.size(30.dp).background(Color.Red).align(Alignment.BottomEnd))}}Button(onClick = { shown = !shown }) {Text("切换")}}
}

效果图如下:

请添加图片描述

可以看到,是右下角保留的红色方块现在左上角的位置显现,然后慢慢扩大直到整个组件完全展示。

接下来再看 expandIn() 参数的含义:

  • expandFrom:扩展边界的起点,默认为 Alignment.BottomEnd,这解释了为何默认情况下组件是从右下角展开的
  • clip:是否应剪切动画边界之外的内容,默认为 true,如果设置为 false,则不进行裁切,只进行简单的位移
  • initialSize:扩展边界的起始大小,默认返回 IntSize(0, 0)

我们对以上参数逐个进行测试。首先修改 expandFrom 的位置,比如让它从左上角开始展开:

AnimatedVisibility(shown, enter = expandIn(tween(5000), Alignment.TopStart))

效果如下,右下角的红色方块最后才展示出来:

请添加图片描述

然后设置 initialSize,让宽高都是一半的位置作为动画开始的初始位置:

AnimatedVisibility(shown,enter = expandIn(tween(5000), Alignment.TopStart) {IntSize(it.width / 2, it.height / 2)},
)

动画初始就会展示左上角的 1/4,然后慢慢展开,效果如下:

请添加图片描述

再看 clip,将其修改为 false 查看效果:

AnimatedVisibility(shown,enter = expandIn(tween(5000), Alignment.TopStart, false) {IntSize(it.width / 2, it.height / 2)},
)

效果如下:

请添加图片描述

expandIn() 也有两个在单一方向上的衍生函数 expandHorizontally() 与 expandVertically()。

最后,通过 scaleIn() 提供缩放入场:

@Stable
@ExperimentalAnimationApi
fun scaleIn(animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),initialScale: Float = 0f,transformOrigin: TransformOrigin = TransformOrigin.Center,
): EnterTransition {return EnterTransitionImpl(TransitionData(scale = Scale(initialScale, transformOrigin, animationSpec)))
}

默认以组件中心 TransformOrigin.Center 为缩放中心,默认效果如下:

请添加图片描述

最后我们来说一下这四种函数之间的加号,是 EnterTransition 重写了 plus 操作符:

	@Stableoperator fun plus(enter: EnterTransition): EnterTransition {return EnterTransitionImpl(TransitionData(fade = data.fade ?: enter.data.fade,slide = data.slide ?: enter.data.slide,changeSize = data.changeSize ?: enter.data.changeSize,scale = data.scale ?: enter.data.scale))}

plus 是创建一个新的 EnterTransitionImpl,里面的 TransitionData 的四个参数,优先选择等号左侧的,如果等号左侧没有配置某种动画,才会选择等号右侧的。

出场动画 ExitTransition 的内容与 EnterTransition 大同小异,只不过入场动画的 expandIn() 对应出场动画的 shrinkOut():

AnimatedVisibility(shown,enter = fadeIn() + expandIn(),exit = fadeOut() + shrinkOut()
)

此外,Transition 还有一个 AnimatedVisibility() 的扩展函数:

@ExperimentalAnimationApi
@Composable
fun <T> Transition<T>.AnimatedVisibility(visible: (T) -> Boolean,modifier: Modifier = Modifier,enter: EnterTransition = fadeIn() + expandIn(),exit: ExitTransition = shrinkOut() + fadeOut(),content: @Composable() AnimatedVisibilityScope.() -> Unit
) = AnimatedEnterExitImpl(this, visible, modifier, enter, exit, content)

第一个参数 visible 是返回 Boolean 类型的函数,其参数 T 是 Transition 的状态,你需要根据这个状态决定 AnimatedVisibility() 的内容是否可见并作为 visible 的返回值。

无论使用哪一种 AnimatedVisibility,它里面都只能存放一个 Composable 组件,放多了行为不正常。如果有多个 Composable 组件想使用 AnimatedVisibility,那么就为每一个组件配一个 AnimatedVisibility。

3.3 Crossfade()

Crossfade() 会以动画方式切换显示内容,动画是透明度渐变动画,也就是淡入淡出动画,与上一节介绍的 fadeIn()、fadeOut() 完全相同。我们直接来看 Crossfade() 的参数,因为这些参数在前面介绍其他 API 时已经见过很多次了:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun <T> Crossfade(targetState: T,modifier: Modifier = Modifier,animationSpec: FiniteAnimationSpec<Float> = tween(),label: String = "Crossfade",content: @Composable (T) -> Unit
) {val transition = updateTransition(targetState, label)transition.Crossfade(modifier, animationSpec, content = content)
}

targetState 是代表目标布局状态的键,每当更改一个键时,动画将被触发,使用旧键调用的 content 将淡出,而使用新键调用的 content 将淡入。

假设在未使用动画的情况下,根据不同状态显示相应组件的代码如下:

@Composable
fun CrossfadeSample() {Column {var shown by remember { mutableStateOf(false) }if (shown) {Box(Modifier.size(48.dp).background(Color.Red))} else {Box(Modifier.size(24.dp).background(Color.Green))}Button(onClick = { shown = !shown }) {Text("切换")}}
}

如果想实现动画切换效果,可以用 Crossfade() 包住负责组件切换的 if - else 代码块,并且将 if 的判断条件修改为参数传入的 state 对象:

@Composable
fun CrossfadeSample() {Column {var shown by remember { mutableStateOf(false) }Crossfade(targetState = shown, animationSpec = tween(5000)) { state ->if (state) {Box(Modifier.size(48.dp).background(Color.Red))} else {Box(Modifier.size(24.dp).background(Color.Green))}}Button(onClick = { shown = !shown }) {Text("切换")}}
}

这里照例是为了让动画效果更明显,将动画时间设置为 5 秒了,效果如下:

请添加图片描述

可以看到,在渐变过程中,就是两个组件同时存在的时间段内,它会让两个组件都完整显示,但是在渐变完成之后,就只保留新组件的尺寸范围。

由于 Crossfade() 就是一个功能很简单的函数,因此它不能修改动画类型,只能是淡入淡出动画。此外,Crossfade() 的状态值可以是任意类型,不止是上面示例的 Boolean 类型。比如 Int 类型,它可以有多个状态值,需要结合 when 使用。

3.4 AnimatedContent()

前两节我们讲了 AnimatedVisibility() 与 Crossfade(),它们有各自的适用场景:

  • AnimatedVisibility():对单个组件的出现与消失的动画,可以配置多种动画规格(透明度、偏移位置、尺寸裁剪、缩放)
  • Crossfade():对不同状态下的多个组件间切换施以透明度淡入淡出的动画效果,无法配置其他动画效果

AnimatedContent() 覆盖了上述两个函数的功能,可以对两个组件根据状态进行切换,并可以配置切换时的动画效果。它的使用方式也与上面的函数类似:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentSample() {Column {var shown by remember { mutableStateOf(false) }AnimatedContent(shown) { state ->if (state) {Box(Modifier.size(48.dp).background(Color.Red))} else {Box(Modifier.size(24.dp).background(Color.Green))}}Button(onClick = { shown = !shown }) {Text("切换")}}
}

效果如下:

请添加图片描述

这种效果是 AnimatedContent() 的 transitionSpec 参数的默认值配置出的动画效果:

@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(targetState: S,modifier: Modifier = Modifier,transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {fadeIn(animationSpec = tween(220, delayMillis = 90)) +scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) withfadeOut(animationSpec = tween(90))},contentAlignment: Alignment = Alignment.TopStart,content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
) {val transition = updateTransition(targetState = targetState, label = "AnimatedContent")transition.AnimatedContent(modifier,transitionSpec,contentAlignment,content = content)
}

入场的两个动画 fadeIn() 与 scaleIn() 都是延迟 90ms 开始执行,持续 220ms。出场动画 fadeOut() 则是立即执行,时长 90ms。实际上就是 fadeOut() 执行完,让原始组件消失之后,再开始执行入场的两个动画。

再观察 transitionSpec 的返回值 ContentTransform:

@ExperimentalAnimationApi
class ContentTransform(val targetContentEnter: EnterTransition,val initialContentExit: ExitTransition,targetContentZIndex: Float = 0f,sizeTransform: SizeTransform? = SizeTransform()
) {var targetContentZIndex by mutableStateOf(targetContentZIndex)var sizeTransform: SizeTransform? = sizeTransforminternal set
}

ContentTransform 有四个属性:

  • targetContentEnter:目标内容的入场动画
  • initialContentExit:初始内容的出场动画
  • targetContentZIndex:Z 轴的绘制顺序,用于配置各个组件间的覆盖关系
  • sizeTransform:尺寸渐变动画

假如不想使用 AnimatedContent() 的参数 transitionSpec 的默认值,想要自己选取出入场动画,可以自己写一个返回 ContentTransform 的函数:

AnimatedContent(shown,transitionSpec = { ContentTransform(fadeIn(), fadeOut()) }
)

它还有一种等价的方式 —— 使用 EnterTransition 的扩展函数 with():

AnimatedContent(shown, transitionSpec = { fadeIn() with fadeOut() })

with() 实际上就是帮助我们实现了第一种方式:

@ExperimentalAnimationApi
infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)

假如还想修改出入场组件之间的覆盖关系,可以通过修改 targetContentZIndex 实现。默认情况下,targetContentZIndex 是无需配置的,因为入场的组件会在出场组件的上面,遮住出场组件,这个行为是符合预期与需求的。比如使用以下测试代码:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentSample1() {Column {var shown by remember { mutableStateOf(false) }AnimatedContent(shown,transitionSpec = {fadeIn(tween(3000)) with fadeOut(tween(3000, 3000))}) {if (it) {Box(Modifier.size(48.dp).clip(RoundedCornerShape(10.dp)).background(Color.Red))} else {Box(Modifier.size(24.dp).background(Color.Green))}}Button(onClick = { shown = !shown }) {Text("切换")}}
}

效果如下:

请添加图片描述

我们设置出场动画延迟 3s 再开始执行,目的是等待入场动画执行完毕后再开始出场动画,这样看的更清楚一些。上图可以明显看出,红色方块入场时先立即覆盖了即将出场的绿色方块,在入场动画结束后,绿色方块开始出场,左上角的绿色才缓缓消失。

但是有些情况下,默认行为不能满足开发需求,比如想让一个组件做背景,那么这个组件在动画过程中,不论是入场还是出场,它一定是永远在最下层的。再比如一个悬浮的按钮,在动画过程中永远都是在最上层的,不管是入场还是出场。因此我们需要酌情修改 targetContentZIndex。比如对于上面的例子,改为不论入场还是出场,红色方块永远在绿色方块下方:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentSample2() {Column {var shown by remember { mutableStateOf(false) }AnimatedContent(shown,transitionSpec = {// targetState 就是 shown,当其为 true 时就是要显示红色方块了,此时将 targetContentZIndex 调小if (targetState) {(fadeIn(tween(3000)) withfadeOut(tween(3000, 3000))).apply {targetContentZIndex = -1f}} else {fadeIn(tween(3000)) with fadeOut(tween(3000, 3000))}}) {if (it) {Box(Modifier.size(48.dp).clip(RoundedCornerShape(10.dp)).background(Color.Red))} else {Box(Modifier.size(24.dp).background(Color.Green))}}Button(onClick = { shown = !shown }) {Text("切换")}}
}

由于 targetContentZIndex 的默认值是 0f,如果想让红色方块在下面,给它赋一个小于 0 的值即可:

请添加图片描述

最后看 sizeTransform 的设置,直接使用 SizeTransform():

@ExperimentalAnimationApi
fun SizeTransform(clip: Boolean = true,sizeAnimationSpec: (initialSize: IntSize, targetSize: IntSize) -> FiniteAnimationSpec<IntSize> ={ _, _ -> spring(visibilityThreshold = IntSize.VisibilityThreshold) }
): SizeTransform = SizeTransformImpl(clip, sizeAnimationSpec)

clip 表示是否裁切,一般为 true;sizeAnimationSpec 是计算动画曲线的函数,返回 FiniteAnimationSpec,前面也说过多次了。

如果想把配置好的 SizeTransform 合并到现有的 ContentTransform 对象中,可以其扩展函数 using():

AnimatedContent(shown,transitionSpec = {(fadeIn() + scaleIn(tween(500)) with fadeOut()) using SizeTransform()}
)

两个入场动画合并通过重写 EnterTransition 的 plus 操作符实现:

sealed class EnterTransition {@Stableoperator fun plus(enter: EnterTransition): EnterTransition {return EnterTransitionImpl(TransitionData(fade = data.fade ?: enter.data.fade,slide = data.slide ?: enter.data.slide,changeSize = data.changeSize ?: enter.data.changeSize,scale = data.scale ?: enter.data.scale))}
}

入场动画与出场动画合并使用 with():

@ExperimentalAnimationApi
infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)

最后将 SizeTransform 合并到已有的 ContentTransform 中,用 using():

@ExperimentalAnimationApi
class AnimatedContentScope<S> internal constructor(@ExperimentalAnimationApiinfix fun ContentTransform.using(sizeTransform: SizeTransform?) = this.apply {this.sizeTransform = sizeTransform}
}

可以回头看一下 AnimatedContent() 的 transitionSpec 参数,就是通过以上方式将入场、出场效果配置在一起的。

最后,AnimatedContent() 也有一个 Transition 版本,内容与使用方式与本节讲述的通用版本类似:

@ExperimentalAnimationApi
@Composable
fun <S> Transition<S>.AnimatedContent(modifier: Modifier = Modifier,transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {fadeIn(animationSpec = tween(220, delayMillis = 90)) +scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) withfadeOut(animationSpec = tween(90))},contentAlignment: Alignment = Alignment.TopStart,contentKey: (targetState: S) -> Any? = { it },content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
) {...}
关键字:公众号微官网_凡科网小程序怎么样_南昌网站建设_什么是引流推广

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: