news 2026/9/9 14:37:39

Jetpack Compose动画实战:状态驱动UI动效与性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Jetpack Compose动画实战:状态驱动UI动效与性能优化

Jetpack Compose 动画实战:让你的 UI 动起来

做 Android 开发这几年,我踩过 View 系统动画的不少坑,写一长串ObjectAnimatorAnimationSet还要手写插值器,动效稍微复杂一点就容易在屏幕旋转时崩掉。转 Jetpack Compose 之后最大的感受就是:动画终于不再是“附加项”,而是 UI 的一部分。你只要改一个状态,界面自己会过渡、会移动、会消失,整个过程自然得就像它天生该这样。

这篇文章我会从动画的核心思路说起,把animate*AsStateAnimatableAnimatedVisibilityAnimatedContentupdateTransition这些 API 一个个拆开讲,最后带一套完整的可运行代码,专门聊聊多动画编排、手势动画、列表增删动画这些实战场景,以及我踩过的坑。无论你是刚接触 Compose 的入门者,还是已经写了半年觉得动画体系太散、不知道怎么串起来的开发者,这篇文章应该都能给你一点启发。

1. 为什么 Compose 动画和以前完全不是一回事

1.1 先忘掉 View 系统的老思路

在讲 Compose 动画怎么做之前,得先聊清楚它和传统 View 动画的本质区别。View 时代我们写动画,核心操作是“控制一个对象的变化过程”。比如ObjectAnimator.ofFloat(view, "alpha", 0f, 1f),你要盯着那个view对象,手动指定属性名、起始值、结束值,还要让它跑起来。动画的执行是命令式的:我说要做这件事,你立刻去做。

这种模式在简单场景下够用,但一旦涉及“状态切换”,就要自己管理很多中间变量。比如按钮从“未收藏”变成“收藏”,需要同时改图标、改文字、改颜色、改背景,你要为每个属性分别创建动画,还要处理互相之间的时序。写多了就有点像绣花,一针一针全得自己来,稍不留神就漏了一针。

Compose 的动画重构了这套逻辑:它把动画看作是“状态的变化”。你不需要告诉 Compose“先变透明度,再变位移”,你只要声明“这个状态是这样”,然后在代码里改变状态的数值,Compose 的动画系统会自动把旧值过渡到新值。

1.2 状态驱动才是核心

Compose 里的 UI 本质是状态的函数:UI = f(state)。动画也是一样,你定义的动画值本身就是一个可观察的状态。它不断变化,UI 就不断重组,表现出来就是你看到的那段流畅动画。

举个例子。写一个“收藏按钮”,用户点一下,心形图标缩放变大再变回正常大小,同时填充颜色从灰色变成红色。用 View 方式要写两个动画互相监听;用 Compose 方式是这样:

@Composable fun FavoriteButton(isFavorite: Boolean, onClick: () -> Unit) { val scale by animateFloatAsState( targetValue = if (isFavorite) 1.2f else 1f, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), label = "favoriteScale" ) val color by animateColorAsState( targetValue = if (isFavorite) Color.Red else Color.Gray, label = "favoriteColor" ) Icon( imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, contentDescription = null, tint = color, modifier = Modifier .graphicsLayer { scaleX = scale scaleY = scale } .clickable { onClick() } ) }

看到区别了吗?isFavorite一旦变,scalecolor会自动开始过渡,而且spring让它带一点回弹效果。你完全不需要关注动画“跑到了哪一步”,只要管好isFavorite这个状态就行。

理解这一点之后,整个 Compose 动画体系在你眼里就不再是零散的 API 大杂烩,它们都是在围绕“状态过渡”做文章。

1.3 三个动画层级:值、可见性、内容

Compose 官方把动画库分成了好几个模块,但实战中我习惯按“影响范围”分为三层:

  • 值动画:针对单个值做过渡,比如透明度从 0 到 1、尺寸从 50dp 到 100dp、颜色从灰到红。animate*AsStateAnimatable都属于这一层,是使用频率最高的。
  • 可见性动画:控制某个 Composable 的出现和消失,比如弹窗、提示条、列表项展开收起。AnimatedVisibility就是干这个的,它能在“加入视图树”和“移出视图树”这两个时刻自动补入场/退场动画。
  • 内容切换动画:界面从一个内容切换到另一个内容,比如 Tab 切换、页面跳转、数字滚动。AnimatedContentCrossfadeupdateTransition属于这一层。

这三层可以独立使用,也可以组合。比如一个列表项,先是AnimatedVisibility控制展开/收起,展开后再用animateDpAsState让内部高度平滑过渡,这就把两层动画串起来了。理解这个分层,你拿到一个需求时就能快速判断:该用哪个 API,怎么组织代码。

2. 基础动画 API 实战拆解

2.1 单值动画:animate*AsState 系列

animate*AsState系列是入门最友好的 API,也是实际项目里用得最多的。官方提供了浮点、颜色、尺寸、偏移、边距等常用类型:animateFloatAsStateanimateColorAsStateanimateDpAsStateanimateOffsetAsStateanimateSizeAsStateanimateValueAsState(自定义类型)。

它们的用法高度一致:传入一个目标值targetValue,返回一个当前值,目标值改变时,当前值会自动从旧值插值到新值。支持用animationSpec指定动画规格,比如tweenspringkeyframes

这个系列最典型的应用场景就是“点击开关”。比如做一个小卡片,点击后展开更多信息,高度从 100dp 变到 200dp,透明度变化,背景色变化:

@Composable fun ExpandableCard(expanded: Boolean, content: @Composable ColumnScope.() -> Unit) { val background by animateColorAsState( targetValue = if (expanded) Color(0xFFE3F2FD) else Color.White, label = "cardBackground" ) val cornerRadius by animateDpAsState( targetValue = if (expanded) 16.dp else 4.dp, label = "cardCorner" ) Column( modifier = Modifier .fillMaxWidth() .background(background) .clip(RoundedCornerShape(cornerRadius)) .clickable { /* 切换 expanded */ } .padding(16.dp) ) { content() } }

注意这里有个细节:backgroundclip都用了动画值,点击后背景色和圆角会同时开始过渡。这种“多个单值动画并行”的做法很常见,Compose 会把它们放在同一个重组周期里处理,不会出现一个先变一个后变的错乱感。

animationSpec参数值得单独说说。tween(durationMillis, delayMillis, easing)适合标准过渡,spring适合带弹性的交互,keyframes适合需要中间节点的动画。比如你想让圆角“先快速变、再放慢”,可以用keyframes

val radius by animateDpAsState( targetValue = if (expanded) 24.dp else 4.dp, animationSpec = keyframes { durationMillis = 400 16.dp at 150 with FastOutLinearInEasing 24.dp at 400 }, label = "cardCornerKeyframe" )

提示:keyframes里的at时间必须是递增的,最后一个时间要和durationMillis一致,否则会有奇怪的行为。

2.2 可见性动画:AnimatedVisibility

AnimatedVisibility解决的是“一个组件要不要出现”的动画问题。它的用法非常直白:

@Composable fun TipBar(showTip: Boolean) { AnimatedVisibility( visible = showTip, enter = fadeIn() + slideInVertically { -it }, exit = fadeOut() + shrinkVertically() ) { Text("这是一条提示信息", modifier = Modifier.background(Color.Yellow).padding(8.dp)) } }

enterexit都是EnterTransition/ExitTransition类型,可以通过+组合多种效果。我用得最多的是fadeInslideInVerticallyexpandVertically这几种;expandIn可以从某个锚点展开,scaleIn适合做图片预览。

这里有一个特别容易踩的坑:如果页面里还有其他可见性变化,或者父容器在做布局动画,AnimatedVisibility的“消失”过程有时会被跳过。比如你同时用animateDpAsState把高度缩小到 0,又让AnimatedVisibility退场,两个动画互相干扰,就会出现“闪一下就没”的鬼畜效果。这类问题我放在第 4 节详细讲,先记住一个原则:同一个组件的出现/消失动画,尽量只在AnimatedVisibility里做,不要和外层布局动画重复叠加。

AnimatedVisibility还有一种进阶用法,就是配合MutableTransitionState做初始可见性动画:

val transitionState = remember { MutableTransitionState(false) } transitionState.targetState = true AnimatedVisibility( visibleState = transitionState, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { ... }

这样组件首次进入组合时,也会从头播放一次入场动画,而不是瞬间显示。做列表首次加载、引导页动画时非常有用。

2.3 内容切换:AnimatedContent 与 updateTransition

AnimatedContent解决的是“同一个位置,内容从 A 换成 B”的动画。最常用的场景是 Tab 切换:

@Composable fun TabContent(tab: TabType) { AnimatedContent( targetState = tab, transitionSpec = { (fadeIn(tween(300)) + slideInHorizontally { it / 4 }) togetherWith (fadeOut(tween(200)) + slideOutHorizontally { -it / 4 }) }, label = "tabContent" ) { currentTab -> when (currentTab) { TabType.Home -> HomeScreen() TabType.Profile -> ProfileScreen() } } }

togetherWithAnimatedContenttransitionSpec专用关键字:A togetherWith B表示内容 A 进场时播放 A,内容 B 退场时播放 B。这里有个细节:content块里的it是当前的targetState,也就是currentTab,不是动画进度值。如果你需要读取动画进度(0f 到 1f),可以用transitioncurrentStatetargetState配合animateFloat去拿。

AnimatedContent有个比较头疼的问题:如果两个目标状态的 UI 结构差异很大,整个内容都会被替换,动画看起来像“两页翻过去”。想要更精细地控制,就得用updateTransition

updateTransition适合用来管理“同一个组件有多个同时变化的属性”的场景。比如一个按钮,在“加载中”“成功”“失败”三种状态之间切换,背景色、图标、文字、旋转角都要跟着变。写多个animate*AsState会显得零散,用updateTransition可以把它们组织在一个“动画事务”里:

@Composable fun StatusButton(status: Status) { val transition = updateTransition(targetState = status, label = "status") val background by transition.animateColor(label = "bg") { state -> when (state) { Status.Loading -> Color.Gray Status.Success -> Color.Green Status.Failed -> Color.Red } } val rotation by transition.animateFloat(label = "rotation") { state -> if (state == Status.Loading) 360f else 0f } val iconAlpha by transition.animateFloat(label = "alpha") { state -> if (state == Status.Loading) 0.6f else 1f } Box( modifier = Modifier .size(48.dp) .background(background, CircleShape) .graphicsLayer { rotationZ = rotation } ) { Icon(..., modifier = Modifier.alpha(iconAlpha)) } }

updateTransition的好处是:它帮你管理“从任意状态到任意状态”的完整过渡。不管是 Loading -> Success 还是 Failed -> Success,所有变化都一起开始、一起结束,不会出现一个属性已经变完了另一个才开始的情况。

3. 进阶实战:多动画编排与手势动画

3.1 队列执行动画:让动画按顺序跑起来

很多场景下,一个动画要等另一个动画结束再开始,而不是同步播放。比如购物车加购:数字先飞到购物车图标上,然后购物车图标再跳动一下。网上搜“Android 队列执行动画”的需求很多,在 View 时代我们要写AnimatorSet.playSequentially,在 Compose 里其实思路更简单——用协程顺序执行。

把动画值定义成Animatable,然后在协程里依次animateTo

@Composable fun AddToCartAnimation() { val scope = rememberCoroutineScope() val flyOffset = remember { Animatable(0f) } val cartScale = remember { Animatable(1f) } Box { // 商品区域 Button(onClick = { scope.launch { // 第一步:数字上浮并淡出 flyOffset.animateTo( targetValue = -200f, animationSpec = tween(300, easing = FastOutSlowInEasing) ) // 第二步:购物车图标弹跳 cartScale.animateTo( targetValue = 1.4f, animationSpec = tween(150, easing = FastOutLinearInEasing) ) cartScale.animateTo( targetValue = 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium ) ) // 第三步:偏移复位,准备下次动画 flyOffset.snapTo(0f) } }) { Text("加入购物车") } Text( "+1", modifier = Modifier .graphicsLayer { translationY = flyOffset.value } ) // 购物车图标 Icon( imageVector = Icons.Filled.ShoppingCart, contentDescription = null, modifier = Modifier.graphicsLayer { scaleX = cartScale.value scaleY = cartScale.value } ) } }

这里核心就是Animatable。它有一个内部状态,你可以通过animateTo启动动画,snapTo瞬间跳转,stop停止动画。每次animateTo都返回一个Job,在协程里它默认会等当前动画完成再继续执行下一行,所以天然支持队列。

提示:Animatable配合rememberCoroutineScope().launch使用最顺手。如果是在LaunchedEffect里做一次性动画也可以,但要注意LaunchedEffect的协程会在重组时被取消,不适合“用户点击后播放”的场景。

3.2 无限循环动画:loading 状态的正确打开方式

loading 动画是每个应用都绕不开的。用rememberInfiniteTransition可以创建无限重复的动画值,最常见的用法是做一个旋转的进度环:

@Composable fun LoadingIndicator() { val infiniteTransition = rememberInfiniteTransition(label = "loading") val rotation by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(800, easing = LinearEasing), repeatMode = RepeatMode.Restart ), label = "rotation" ) CircularProgressIndicator( progress = { 0.3f }, modifier = Modifier.graphicsLayer { rotationZ = rotation } ) }

repeatMode有两个值:RepeatMode.Restart是每次从头播放,比如 0 到 360 再到 0;RepeatMode.Reverse是正反交替,比如 0 到 1 再到 0。做滑块动画、呼吸闪烁效果时,Reverse非常实用。

rememberInfiniteTransition有个性能隐患:如果动画作用范围太大,或者内部做了复杂计算,会导致每帧都重组大片 UI。我见过一个项目在InfiniteTransitionanimateFloat里直接Math.sin计算坐标,结果列表滑动时卡到 20fps。正确做法是:把无限循环动画限制在graphicsLayerdrawBehind等轻量作用域里,避免触发大范围布局重组。

关于轻量作用域,简单解释下:graphicsLayer只是改绘制参数,不触发测量和布局;而Modifier.offset虽然也不算布局,但如果你改的是size这种就会触发布局。所以能用graphicsLayer的地方尽量用它。

3.3 手势 + 弹簧动画:拖拽回弹效果

Compose 里做“按住拖拽、松手回弹”的效果,核心是Animatable+ 手势 API + 弹簧动画。代码不复杂,但要注意手势事件的消费:

@Composable fun DraggableCard(content: @Composable () -> Unit) { val offsetX = remember { Animatable(0f) } val offsetY = remember { Animatable(0f) } val coroutineScope = rememberCoroutineScope() Box( modifier = Modifier .pointerInput(Unit) { detectDragGestures( onDrag = { change, dragAmount -> change.consume() coroutineScope.launch { offsetX.snapTo(offsetX.value + dragAmount.x) offsetY.snapTo(offsetY.value + dragAmount.y) } }, onDragEnd = { coroutineScope.launch { offsetX.animateTo( targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMedium ) ) offsetY.animateTo( targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMedium ) ) } } ) } .graphicsLayer { translationX = offsetX.value translationY = offsetY.value } ) { content() } }

几个关键点:

  • 拖拽过程中用snapTo而不是animateTo,保证手指移动和卡片位移零延迟。
  • change.consume()必须调用,否则手势事件还会传给父容器,可能出现“卡片移动了,列表也滑动了”的问题。
  • 松手后用spring回弹,Spring.DampingRatioNoBouncy表示不过度回弹,Spring.StiffnessMedium保证回弹速度适中。如果你想做那种“甩出去”的感觉,可以加大stiffness,或者让回弹的目标值根据松手时的速度来计算。

如果想让回弹更带感,可以稍微加一点点弹跳,比如Spring.DampingRatioLowBouncy,但注意不要所有地方都用低阻尼,不然整个 App 会显得很“活泼”过头,用户会觉得晃眼。

3.4 列表 item 增删动画

列表增删动画在 Compose 里比 View 时代简单太多。LazyColumn/LazyRow的 item 支持直接添加动画:

@Composable fun AnimatedList(items: List<Item>) { LazyColumn { items(items, key = { it.id }) { item -> Row( modifier = Modifier .fillMaxWidth() .animateItem() // 新增/删除/移动时自动动画 .clickable { /* 删除 */ } .padding(8.dp) ) { Text(item.title) } } } }

新版 Compose 里,Modifier.animateItem()会为 item 的插入、移除、移动自动应用动画。移除时 item 会先播放退场动画,再真正从列表移除;重新排序时会平滑移动到新位置。前提是记得给items指定key,否则 Compose 不知道这个 item 是“新加的”还是“原来的”,动画会失效或者表现错乱。

如果你需要更精细的控制,比如自定义插入动画是“从左边滑入”还是“从底部展开”,可以给animateItem()传入fadeInSpecplacementSpec等参数。实测下来默认效果已经能应付绝大多数场景。

4. 常见问题与排查技巧实录

4.1 动画“显示不全”或“闪一下就没”

这个可以说是 Compose 动画新手最容易遇到的问题。“动画显示不全”常见的有两种表现:动画只播放了一部分就突然结束,或者动画在还没播放时就瞬间跳到最终状态。

排查看几个方向:

  • 状态没有保持:如果你用remember { mutableStateOf(false) }保存动画开关,而组件在动画进行中被重建(比如父组件重组导致当前组件退出组合),状态会重置,动画自然断掉。这种情况建议改用rememberSaveable或把状态提升到 ViewModel。
  • AnimatedVisibility作用域问题AnimatedVisibility的 content lambda 里如果直接引用外部状态,当外部状态先变的瞬间,visible参数可能已经改变了 content 的结构,导致退场动画被“跳过”。我建议退场动画的 UI 结构尽量保持稳定,不要依赖正在变化的状态。
  • 多个动画互相覆盖:同一个值被两个不同的animate*AsState同时驱动时,后启动的会覆盖前一个。如果你发现“动画只闪了一下”,很可能是某个重组把目标值改回了初始值。

出现“闪一下”时,我的排查套路是:先加日志确认状态是否连续变化,再看是否有外部重组强制重置状态,最后才怀疑动画 API 本身的问题。大部分情况都不是 API 的问题,而是状态管理的问题。

4.2 动画卡顿与性能优化

Compose 动画卡顿的原因往往不是动画本身,而是动画期间每帧都触发了大范围重组。排查方法很直接:打开 Layout Inspector 看重组范围,或者用Modifier.layoutId配合CompositionLocal观察哪些子组件在动画期间被重新组合。

常见优化手段:

  • graphicsLayer代替布局属性Modifier.offsetModifier.graphicsLayer都能实现位移,但前者在动画中更容易触发布局,后者只影响绘制。缩放、旋转、透明度同理。只要不影响布局测量,一律优先用graphicsLayer
  • 把动画值封装在derivedStateOf:如果一个高开销的 Composable 依赖动画值,动画每帧变化都会导致它重组。用derivedStateOf做一次“过滤”,比如只在动画值过某个阈值时才更新 UI,能显著减少重组频率。
  • 拆分组件:动画值只传给真正需要它的子组件,而不是放在一个巨大的 Column 顶层,否则整个 Column 的子树都会跟着重组。
  • 谨慎使用key变更key变化会销毁重建组件,如果动画期间key变了,动画目标组件都不存在了,动画自然不流畅。

我做过一个图标充电动画,刚开始把旋转值放在整个页面顶层,结果充电数字每秒重绘 60 次,耗电明显。后来把旋转动画收敛到图标自己的组件里,页面其他部分完全不受影响。

4.3 旋转屏幕后动画状态丢失

rememberrememberSaveable的区别在这里显得特别重要。remember在活动重建后会丢失,而rememberSaveable能通过 SavedState 机制恢复。动画状态如果是“显式的布尔值”比如是否展开,可以用rememberSaveable保存;如果是Animatable这种运行时对象,不能直接存,需要把它的目标值用rememberSaveable保存,重建后手动snapTo到目标值:

@Composable fun PersistedAnimatedValue() { var target by rememberSaveable { mutableStateOf(0f) } val animatable = remember { Animatable(target) } // 重建后同步 LaunchedEffect(target) { animatable.animateTo(target) } Button(onClick = { target = if (target == 0f) 100f else 0f }) { Text("移动") } }

这里有个小技巧:AnimatableanimateTo如果目标值和当前值一样,它会直接跳过,不会造成多余动画。所以用LaunchedEffect(target)在初始化时同步一次是安全的。

5. 我的实操心得与避坑总结

文章最后,把自己这几百个日子的动画实战经验浓缩成几条,供你参考。

第一,动画是状态管理的延伸。你在 Compose 里写动画,本质是在写状态机。动画写不清楚,先回头看状态设计是不是混乱了。animate*AsState用起来顺手,是因为它把“状态变化”和“UI 过渡”绑在了一起;如果你的状态本身就在乱变,再好的动画 API 也救不回来。

第二,默认动画参数大多够用,不要频繁改spring的默认参数DampingRatioNoBouncyStiffnessMedium已经比较克制,适用于大多数交互。过度调参会让 UI 显得浮躁。我见过有人把所有按钮点击动画都改成LowBouncy,看起来像在弹棉花。真正需要个性的时候,再针对性地调keyframes,不要一开始就搞花活。

第三,动画性能问题,大多数不是动画本身的问题。Compose 动画每一帧都只改几个数值,真正的重头开销在于重组。所以遇到卡顿,先查重组范围,再查布局计算,最后再回过头看动画频率。graphicsLayer是你的好帮手,但它只解决绘制层的问题,解决不了设计层面的“大树重组”。

第四,队列编排用协程,是 Compose 动画最舒服的姿势Animatable.animateTo天然支持“上一个动画跑完再跑下一个”,配合rememberCoroutineScope处理用户触发动画的场景,配合LaunchedEffect处理一次性动画场景,几乎没有写不出来的编排。

第五,动画要克制。UI 动画的最高境界是让用户感受到流畅和自然,而不是炫技。列表多一条数据时来个平滑插入,按钮点击时有一点反馈,Tab 切换时不生硬——这些细节会让用户觉得“这 App 很顺”,但如果你处处放大动画,反而会让用户觉得拖沓。我做过的项目里,动画做得好的,都不是动画数量多的,而是动画与该出现的时刻配合得精准的。

Jetpack Compose 的动画体系还在快速迭代,新版本不断有更优雅的 API 出来。但只要掌握了“状态驱动 + 分层使用 + 协程编排”这三个核心思路,后续学什么新 API 都只是换个写法的事。希望这篇文章能帮你把动画这条线捋顺,早日写出流畅自然、让人追着用的界面。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 14:37:28

diagram-design:前端可视化接口层的核心原理与工程实践

1. Diagram-Design 不是画图工具&#xff0c;而是现代前端可视化工程的核心接口层你打开一个网页&#xff0c;看到一张清晰的流程图、系统架构图或状态机图——它很可能不是设计师用 Photoshop 导出的 PNG&#xff0c;也不是产品经理拖拽 draw.io 生成的截图&#xff0c;而是由…

作者头像 李华
网站建设 2026/9/9 14:37:24

2025十大畅销车型灯光升级方案全解析

这两年做灯光升级的店面越来越卷&#xff0c;但咨询量最大的&#xff0c;翻来覆去其实是同一批车&#xff1a;都是销量榜上常年霸榜的型号。我自己在店里接单接到手软的十款车&#xff0c;恰好在2025年又是最热门的畅销车型。这篇文章就把这十个车型的灯光升级方案挨个捋一遍&a…

作者头像 李华
网站建设 2026/9/9 14:37:21

2025畅销车灯光升级攻略:从卤素到矩阵的适配与避坑指南

1. 2025畅销车灯光升级&#xff0c;先看懂原厂状态再动手做灯光升级这行有年头了&#xff0c;每年都会遇到一大波新车车主来问“我这车灯该怎么改”。前几年问得最多的还是“疝气灯多少钱”&#xff0c;现在明显不一样了——2025年的畅销车&#xff0c;原厂灯光配置已经卷到飞起…

作者头像 李华
网站建设 2026/9/9 14:37:17

别再手写密钥了:CAMEL 多模型 API 密钥配置完全指南

别再手写密钥了&#xff1a;CAMEL 多模型 API 密钥配置完全指南 【免费下载链接】camel &#x1f42b; CAMEL: The first and the best multi-agent framework. Finding the Scaling Law of Agents. https://www.camel-ai.org 项目地址: https://gitcode.com/GitHub_Trending…

作者头像 李华
网站建设 2026/9/9 14:36:01

数位平方和最大值:从暴力枚举到数位DP的优化攻略

第 168 场双周赛的 Q2&#xff0c;题目编号 3723&#xff0c;名字叫“数位平方和的最大值”。这道题我在比赛时花了 8 分钟 AC&#xff0c;属于典型的“看着像难题、实际有套路”的送分题。很多选手卡住是因为一开始就想着暴力枚举&#xff0c;看到 n 的范围直接懵了&#xff1…

作者头像 李华
网站建设 2026/9/9 14:31:32

从二进制包到K8s集群:kube1.18.8离线部署全解析

简介&#xff1a;面向需要在内网或离线环境部署Kubernetes集群的运维与开发人员&#xff0c;这份kube1.18.8.tar.gz资源包提供了一套完整的K8s 1.18.8安装组件。资源共21个文件&#xff0c;压缩包体积约610MB&#xff0c;涵盖Shell脚本、YAML配置、Markdown说明文档、Systemd服…

作者头像 李华