Compose 实践与探索六 —— 动画的流程控制与 Transition

news2025/3/14 10:53:12

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: Animatable
                    paddingRed = 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() 也会引发正在执行的动画所在的协程抛 CancellationException
            LaunchedEffect(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 = true
                        val initVelocityY = result.endState.velocity.y
                        animY.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;
    @UnsupportedAppUsage
    private 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 progress
        if (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 应该就是求模后的 usedValue
                usedValue
            } 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 对象,也就是 Boolean
        if (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 = big
    val 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 操作符:

	@Stable
    operator 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)) with
            fadeOut(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? = sizeTransform
        internal 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)) with
                            fadeOut(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 {

    @Stable
    operator 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(
    @ExperimentalAnimationApi
    infix 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)) with
            fadeOut(animationSpec = tween(90))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    contentKey: (targetState: S) -> Any? = { it },
    content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
) {...}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2314816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟机Contos7为啥不能被本机电脑访问?

1.查看防火墙是否开启 systemctl status firewalld.service 2.如果防火墙关闭就可以直接被访问 3.如果防火墙打开了我们需要开放端口(下面为防火墙一系列指令) # 关闭防火墙 systemctl stop firewalld.service# 打开防火墙 systemctl start firewalld.service# 关闭开启自启…

idea超级AI插件,让 AI 为 Java 工程师

引言​ 用户可在界面中直接通过输入自然语言的形式描述接口的需求&#xff0c;系统通过输入的需求自动分析关键的功能点有哪些&#xff0c;并对不确定方案的需求提供多种选择&#xff0c;以及对需求上下文进行补充&#xff0c;用户修改确定需求后&#xff0c;系统会根据需求设…

µCOS-III从入门到精通 第十四章(软件定时器)

参考教程&#xff1a;【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili 一、软件定时器简介 1、定时器的概念与种类 &#xff08;1&#xff09;定时器的概念&#xff1a;从指定的时刻开始&#xff0c;经过一个指定时间&#xff0c;然后触发一个超时事件&…

MySQL数据库复杂的增删改查操作

在前面的文章中&#xff0c;我们主要学习了数据库的基础知识以及基本的增删改查的操作。接下去将以一个比较实际的公司数据库为例子&#xff0c;进行讲解一些较为复杂且现时需求的例子。 基础知识&#xff1a; 一文清晰梳理Mysql 数据库基础知识_字段变动如何梳理清楚-CSDN博…

KCD 北京站丨Volcano 邀您畅聊云原生智能调度技术与应用

AI与云原生技术正以前所未有的速度改变着我们的世界&#xff0c;而云原生技术则如同一座坚实的桥梁&#xff0c;连接着传统IT与现代化的数字世界。当AI与云原生相遇&#xff0c;它们相互赋能&#xff0c;相互促进&#xff0c;为开发者们打开了一个全新的技术宇宙。 3 月 15 日&…

BLEU评估指标

一、介绍 用于评估模型生成的句子和实际句子差异的指标&#xff0c;取值在[0,1]&#xff0c;匹配度高就距离1近&#xff0c;反之距离0近。这个指标计算代价小&#xff0c;容易理解&#xff0c;与语言无关&#xff0c;与人类评价结果高度相关。 BLEU主要基于n-gram匹配&#x…

高效自动化测试:打造Python+Requests+Pytest+Allure+YAML的接口测试框架

一、背景 在快节奏的开发周期中&#xff0c;如何确保接口质量&#xff1f;自动化测试是关键。通过构建标准化、可复用的测试框架&#xff0c;能显著提升测试效率与准确性&#xff0c;为项目质量保驾护航[1][7]。 二、目标 ✅ 核心目标&#xff1a; ● 实现快速、高效的接口测试…

BSides Vancouver: 2018 (Workshop)

BSides Vancouver: 2018 (Workshop) 来自 <https://www.vulnhub.com/entry/bsides-vancouver-2018-workshop,231/> 1&#xff0c;将两台虚拟机网络连接都改为NAT模式 2&#xff0c;攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23…

rStar论文精读

论文简介 论文标题&#xff1a;《Mutual reasoning makes smaller LLMs stronger problem-solvers》 论文地址&#xff1a;https://arxiv.org/abs/2408.06195 录用会议&#xff1a;ICLR2025 背景与挑战 挑战1&#xff1a;在SLM中平衡exploration与exploitation。一些方法有很…

247g 的工业级电调,如何让无人机飞得更 “聪明“?——STONE 200A-M 深度测评

一、轻量化设计背后的技术取舍 当拿到 STONE 200A-M 时&#xff0c;247g 的重量让人意外 —— 这个接近传统 200A 电调 70% 的重量&#xff0c;源自 1205624.5mm 的紧凑结构&#xff08;0.1mm 公差控制&#xff09;。实测装机显示&#xff0c;相比同规格产品&#xff0c;其体积…

Node.js:快速启动你的第一个Web服务器

Node.js 全面入门指南 文章目录 Node.js 全面入门指南一 安装Node.js1. Windows2. MacOS/Linux 二 配置开发环境1. VSCode集成 三 第一个Node.js程序1. 创建你的第一个Node.js程序 四 使用Express框架1. 快速搭建服务器 一 安装Node.js 1. Windows 以下是Windows环境下Node.j…

自定义日志回调函数实现第三方库日志集成:从理论到实战

一、应用场景与痛点分析 在开发过程中&#xff0c;我们经常会遇到以下场景&#xff1a; 日志格式统一&#xff1a;第三方库使用自己的日志格式&#xff0c;导致系统日志混杂&#xff0c;难以统一管理和分析。日志分级过滤&#xff1a;需要动态调整第三方库的日志输出级别&…

Linux练级宝典->任务管理和守护进程

任务管理 进程组概念 每个进程除了进程ID以外&#xff0c;还有一个进程组&#xff0c;进程组就是一个或多个进程的集合 同一个进程组&#xff0c;代表着他们是共同作业的&#xff0c;可以接收同一个终端的各种信号&#xff0c;进程组也有其唯一的进程组号。还有一个组长进程&a…

C语言:计算并输出三个整数的最大值 并对三个数排序

这是《C语言程序设计》73页的思考题。下面分享自己的思路和代码 思路&#xff1a; 代码&#xff1a; #include <stdio.h> int main() {int a,b,c,max,min,mid ; //设置大中小的数分别为max&#xff0c;mid&#xff0c;min&#xff0c;abc为输入的三个数printf("ple…

工具(十二):Java导出MySQL数据库表结构信息到excel

一、背景 遇到需求&#xff1a;将指定数据库表设计&#xff0c;统一导出到一个Excel中&#xff0c;存档查看。 如果一个一个弄&#xff0c;很复杂&#xff0c;耗时长。 二、写一个工具导出下 废话少絮&#xff0c;上码&#xff1a; 2.1 pom导入 <dependency><grou…

ACL初级总结

ACL–访问控制列表 1.访问控制 在路由器流量流入或者流出的接口上,匹配流量,然后执行相应动作 permit允许 deny拒绝 2.抓取感兴趣流 3.ACL匹配规则 自上而下逐一匹配,若匹配到了则按照对应规则执行动作,而不再向下继续匹配 思科:ACL列表末尾隐含一条拒绝所有的规则 华为:AC…

调优案例一:堆空间扩容提升吞吐量实战记录

&#x1f4dd; 调优案例一&#xff1a;堆空间扩容提升吞吐量实战记录 &#x1f527; 调优策略&#xff1a;堆空间扩容三部曲 # 原配置&#xff08;30MB堆空间&#xff09; export CATALINA_OPTS"$CATALINA_OPTS -Xms30m -Xmx30m"# 新配置&#xff08;扩容至120MB&am…

C语言 —— 此去经年梦浪荡魂音 - 深入理解指针(卷一)

目录 1. 内存和地址 2. 指针变量和地址 2.1 取地址操作符&#xff08;&&#xff09; 2.2 指针变量 2.3 解引用操作符 &#xff08;*&#xff09; 3. 指针的解引用 3.1 指针 - 整数 3.2 void* 指针 4. const修饰指针 4.1 const修饰变量 4.2 const修饰指针变量 5…

计算机毕业设计:留守儿童的可视化界面

留守儿童的可视化界面mysql数据库创建语句留守儿童的可视化界面oracle数据库创建语句留守儿童的可视化界面sqlserver数据库创建语句留守儿童的可视化界面springspringMVChibernate框架对象(javaBean,pojo)设计留守儿童的可视化界面springspringMVCmybatis框架对象(javaBean,poj…

golang算法二叉树对称平衡右视图

100. 相同的树 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q [1,2,3] 输出&#xff1a…