news 2026/6/25 15:38:01

【共创季稿事节】 鸿蒙原生 ArkTS 布局实战:Tabs + animateTo 实现页面切换过渡动画

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【共创季稿事节】 鸿蒙原生 ArkTS 布局实战:Tabs + animateTo 实现页面切换过渡动画




目录

  1. 引言:为什么需要页面切换动画
  2. Tabs 组件基础
  3. animateTo 动画引擎详解
  4. 实战:四季主题标签页
  5. 代码逐段解析
    • 5.1 数据模型与状态变量
    • 5.2 自定义标签栏 @Builder
    • 5.3 页面内容 @Builder
    • 5.4 指示器圆点与活跃度算法
    • 5.5 switchTab 动画编排
    • 5.6 build() 主界面组装
  6. 三次编译踩坑与修复
  7. 性能优化与最佳实践
  8. 总结

1. 引言:为什么需要页面切换动画

在移动应用开发中,底部标签栏(Bottom Navigation)是最常见的导航模式之一。微信、支付宝、抖音等国民级应用均采用此布局。当用户在不同标签页之间切换时,过渡动画直接决定了应用的使用体验:

用户体验维度无动画有过渡动画
感知速度页面"闪跳",感觉突兀流畅过渡,感觉自然
空间感难以建立页面之间的位置关系清楚知道从哪来到哪去
品质感粗糙、业余精致、专业
交互反馈缺乏确认感操作有明确反馈

鸿蒙 ArkTS 提供了两套动画方案:

  1. 隐式动画(属性动画):通过.animation()链式调用,自动给属性变化添加过渡
  2. 显式动画(animateTo):在onChange等回调中显式调用animateTo()驱动状态变量变化

本文聚焦显式动画方案,因为它更灵活、可控性更强,尤其适合「页面切换」这种多变量协同动画的场景。


2. Tabs 组件基础

2.1 组件层级

Tabs ← 容器,管理所有标签页 ├── TabContent ← 第 1 个标签页的内容 │ └── ... ← 该页的 UI 组件 ├── TabContent ← 第 2 个标签页的内容 │ └── ... └── TabContent ← 第 3 个标签页的内容 └── ...

2.2 核心属性

属性类型说明示例值
barPositionBarPosition标签栏位置BarPosition.End(底部)
indexnumber当前选中页索引0
verticalboolean是否垂直方向滑动false(水平滑动)
scrollableboolean是否允许手指滑动切换true
barHeightLength标签栏高度60
barModeBarMode标签栏布局模式BarMode.Fixed(固定均分)
animationDurationnumber内置切换动画时长(ms)0(关闭内置动画)

2.3 两种模式

Tabs 支持非受控模式受控模式

  • 非受控模式:不给index属性赋值,Tabs 内部管理当前页面索引。适合简单场景。
  • 受控模式:传入index: this.currentIndex,由开发者通过@State currentIndex完全控制哪个页面可见。这次实战采用受控模式,因为我们要在onChange回调中精确编排动画时序。

2.4 tabBar 自定义

TabContent 通过.tabBar()方法绑定自定义标签栏 UI:

TabContent() { // 页面主体内容 } .tabBar(() => { // 自定义标签按钮 UI this.MyTabBuilder(item) })

.tabBar()接受一个闭包,闭包内调用 @Builder 方法。这里有一个关键语法点:闭包形式.tabBar(() => { this.Builder(param) })而非.tabBar(this.Builder, param)——后者在 SDK 6.1.1 中不支持双参数形式。


3. animateTo 动画引擎详解

3.1 函数签名

getUIContext()?.animateTo( options: AnimateOptions, callback: () => void ): void

3.2 参数说明

AnimateOptions对象:

字段类型说明默认值
durationnumber动画时长(毫秒)1000
curveCurve插值曲线Curve.EaseInOut
delaynumber延迟开始(毫秒)0
iterationsnumber重复次数,-1表示无限1
playModePlayMode播放模式(正常/反向/交替)PlayMode.Normal
onFinish() => void动画完成回调undefined

Curve 常用值

曲线效果适用场景
Curve.Linear匀速机械运动
Curve.EaseIn慢→快物体离开
Curve.EaseOut快→慢物体到达
Curve.EaseInOut慢→快→慢自然运动
Curve.FastOutSlowIn快→慢页面入场(推荐)
Curve.Friction摩擦减速滑动停止
Curve.SpringMotion弹簧回弹弹性效果

3.3 工作原理

时间轴 │ ├─ T₀: 调用 getUIContext()?.animateTo() │ 框架记录当前所有 @State 变量的值作为"起点" │ ├─ T₀~Tₙ: 动画执行中 │ 框架根据 duration + curve 计算每一帧的插值 │ 每次插值触发 UI 重新渲染 │ └─ Tₙ: 动画完成 框架设置最终值,触发 onFinish 回调

关键理解animateTo的 closure 中写的赋值语句this.xxx = newValue并不是立即生效的。框架将 closure 中的赋值解析为"终点值",然后从"起点值"到"终点值"之间进行插值。

3.4 SDK 6.1.1 的变动

在 HarmonyOS NEXT SDK 6.1.1 中,全局函数animateTo()已被标记为 deprecated。官方推荐的做法是:

// ✅ 新写法:通过 UIContext 调用 this.getUIContext()?.animateTo({ duration: 400 }, () => { this.myState = newValue; }) // ❌ 旧写法:全局函数(已弃用) animateTo({ duration: 400 }, () => { this.myState = newValue; })

getUIContext()是 Component 的内置方法,返回UIContext | undefined,通过可选链?.安全调用。


4. 实战:四季主题标签页

4.1 设计目标

创建一个包含 4 个标签页的应用,每个页面代表一个季节(春夏秋冬),切换时产生以下动画效果:

  1. 内容卡片:从 0.85 倍缩放 + 透明 → 正常大小 + 完全可见(缩放淡入)
  2. 指示器圆点:从当前索引平滑移动到目标索引(光点滑动)
  3. 背景色:每个季节配独特的背景色,切换时视觉区分

4.2 最终效果预览

┌─────────────────────────────────────┐ │ ○──○──●──○ ← 指示器(第 3 页) │ │ │ │ 🍁 │ │ 秋 · 枫 │ │ 金风送爽,层林尽染 │ │ ─────── │ │ ← 左右滑动切换 → │ │ │ ├─────────────────────────────────────┤ │ 🌸 │ 🌻 │ 🍁 │ ❄️ │ ← 标签栏 │ │ 春 │ 夏 │ 秋 │ 冬 │ │ └─────────────────────────────────────┘

5. 代码逐段解析

5.1 数据模型与状态变量

// 每个标签页的数据结构 interface PageItem { icon: string; // emoji 图标(零资源依赖) title: string; // 页面标题 bgColor: Color; // 背景色 desc: string; // 页面描述 } @Entry @Component struct Index { // 页面状态:控制当前显示哪个 Tab @State currentIndex: number = 0; // 动画状态变量 — 由 animateTo 驱动,连续变化 @State cardScale: number = 1.0; // 卡片缩放 @State cardOpacity: number = 1.0; // 卡片不透明度 @State dotPosition: number = 0; // 指示器位置(连续值 0~3) // 页面数据 private readonly pages: PageItem[] = [ { icon: '🌸', title: '春 · 樱', bgColor: Color.Pink, desc: '春暖花开,万物复苏' }, { icon: '🌻', title: '夏 · 葵', bgColor: Color.Orange, desc: '骄阳似火,生机盎然' }, { icon: '🍁', title: '秋 · 枫', bgColor: Color.Brown, desc: '金风送爽,层林尽染' }, { icon: '❄️', title: '冬 · 雪', bgColor: Color.Grey, desc: '银装素裹,瑞雪丰年' }, ]; }

设计考量

  • @State的选择:只有需要驱动 UI 重新渲染的变量才标记为@Statepages数据不会变化,所以用private readonly而非@State
  • 动画变量的粒度:将缩放 (cardScale)、透明度 (cardOpacity)、位置 (dotPosition) 拆分为三个独立变量,方便单独控制动画曲线和时间。
  • 为什么用 emoji 而非图片:减少资源依赖,使示例开箱即用。生产环境建议替换为矢量图标或 SVG。

5.2 自定义标签栏 @Builder

@Builder private TabBarItem(page: PageItem, index: number) { Column() { Text(page.icon) .fontSize(index === this.currentIndex ? 24 : 20) .lineHeight(32) .textAlign(TextAlign.Center) Text(page.title.slice(0, 3)) .fontSize(index === this.currentIndex ? 12 : 11) .fontColor(index === this.currentIndex ? '#007AFF' : '#8A8A8A') .fontWeight(index === this.currentIndex ? FontWeight.Medium : FontWeight.Regular) .lineHeight(16) .textAlign(TextAlign.Center) .margin({ top: 2 }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) }

核心模式:选中态与未选中态的视觉区分。

属性选中态未选中态
图标字号2420
文字字号1211
文字颜色#007AFF(高亮蓝)#8A8A8A(灰色)
字重MediumRegular

@Builder 语法约束:@Builder 函数体内只能写 UI 组件声明,不能写constletiffor等非 UI 语句。条件逻辑必须通过三目运算内联到组件属性中。这是 ArkTS 与标准 TypeScript 的重要区别。

5.3 页面内容 @Builder

@Builder private PageContent(page: PageItem) { Stack() { // 背景色层(半透明) Column() .width('100%') .height('100%') .backgroundColor(page.bgColor) .opacity(0.15) // 前景卡片(可动画对象) Column() { Text(page.icon).fontSize(72).lineHeight(96) Text(page.title).fontSize(26).fontWeight(FontWeight.Bold).fontColor('#2D2D2D').margin({ top: 20 }) Text(page.desc).fontSize(15).fontColor('#666666').margin({ top: 10 }) Divider().color('#BBBBBB').width(60).height(2).borderRadius(1).margin({ top: 24 }).opacity(0.6) Text('← 左右滑动切换 →').fontSize(13).fontColor('#999999').margin({ top: 24 }) } .width('80%') .padding(32) .backgroundColor('#FFFFFF') .borderRadius(20) // ─── 关键动画绑定 ─── .scale({ x: this.cardScale, y: this.cardScale }) .opacity(this.cardOpacity) } .width('100%') .height('100%') }

两点关键设计

  1. Stack叠加背景与前景:背景色层占满整个区域但透明度仅 0.15,为每个页面提供微弱的色调区分,又不干扰前景卡片的可读性。

  2. .scale()+.opacity()绑定动画状态:白色卡片容器的缩放和透明度直接绑定到this.cardScalethis.cardOpacity。当 Tab 切换时,animateTo驱动这两个变量从 0.85→1.0 和 0→1 平滑变化,卡片就产生了"弹出淡入"的效果。

5.4 指示器圆点与活跃度算法

/** * 计算圆点活跃度(0~~1) * 公式:clamp(1 - |dotPosition - idx|, 0, 1) * dotPosition ≈ idx → 活跃度 ≈ 1(全亮) * dotPosition 远离 idx → 活跃度 ≈ 0(全灭) */ private calcActiveness(dotPos: number, idx: number): number { let v: number = 1 - Math.abs(dotPos - idx); if (v < 0) { v = 0; } if (v > 1) { v = 1; } return v; } @Builder private PageIndicator() { Row() { ForEach(this.pages, (page: PageItem, idx: number) => { Circle() .width(8).height(8) .fill('#007AFF') .opacity(this.calcActiveness(this.dotPosition, idx)) .scale({ x: 0.5 + this.calcActiveness(this.dotPosition, idx) * 0.5, y: 0.5 + this.calcActiveness(this.dotPosition, idx) * 0.5 }) .margin({ left: idx === 0 ? 0 : 8 }) }) } .width('100%').height(20) .justifyContent(FlexAlign.Center) }

活跃度算法的巧妙之处

dotPosition从 0 变化到 3 时,是一个连续的浮点数。以dotPosition = 1.7为例:

圆点索引|1.7 - idx|活跃度 = 1 - 差值(截断到 0~1)视觉
01.70.0全灭
10.70.3微亮
20.30.7较亮
31.30.0全灭

索引 1 的活跃度从 0.3→0→0.3→0.7→1.0 逐渐变化,索引 2 的活跃度从 0→0.3→0.7→1.0→0.7 逐渐变化。两个相邻圆点的活跃度此消彼长,形成"光点滑动"的视觉效果。

5.5 switchTab 动画编排

这是整个示例的核心:

private switchTab(index: number): void { // ── 第 1 步:即时重置(无动画) ── // 内容缩小并隐藏,为入场动画做准备 this.cardScale = 0.85; this.cardOpacity = 0.0; // 更新当前页面索引(TabContent 立即切换到新页面) this.currentIndex = index; // ── 第 2 步:显式动画(400ms) ── // 使用 SDK 6.1.1 推荐的 getUIContext()?.animateTo() 形式 this.getUIContext()?.animateTo({ duration: 400, curve: Curve.FastOutSlowIn, // 先快后慢,自然的缓动 }, () => { // closure 内的 @State 修改都会产生平滑动画 this.cardScale = 1.0; // 0.85 → 1.0:缩放到正常(弹出效果) this.cardOpacity = 1.0; // 0.0 → 1.0:透明到可见(淡入效果) this.dotPosition = index; // old → new:指示器滑动 }) }

动画时序图

时间 │ ├─ T=0ms │ ├─ cardScale: 1.0 → 0.85 (即时,无动画) │ ├─ cardOpacity: 1.0 → 0.0 (即时,无动画) │ └─ currentIndex: old → new (即时) │ ├─ T=0ms~400ms ← animateTo 执行区间 │ ├─ cardScale: 0.85 → 1.0 (动画,FastOutSlowIn) │ ├─ cardOpacity: 0.0 → 1.0 (动画,FastOutSlowIn) │ └─ dotPosition: old → new (动画,FastOutSlowIn) │ └─ T=400ms └─ 动画完成,UI 稳定在新状态

为什么第 1 步和第 2 步分开?

如果直接把this.cardScale = 0.85放在 animateTo 的 closure 中,那么 0.85 也会被动画化,达不到"瞬间缩小"的效果。所以将"重置"放在 closure 之外(即时生效),将"恢复"放在 closure 之内(平滑动画)。

5.6 build() 主界面组装

build() { // Tabs 容器 — 标签栏在底部,受控模式 Tabs({ barPosition: BarPosition.End, index: this.currentIndex, }) { // 遍历 4 个页面生成 TabContent ForEach(this.pages, (page: PageItem, idx: number) => { TabContent() { Column() { // 顶部:页面指示器(圆点) this.PageIndicator() // 中部:页面主体(使用 Stack 包裹以实现 layoutWeight) Stack() { this.PageContent(page) } .layoutWeight(1) } .width('100%') .height('100%') .backgroundColor('#F2F2F2') } .tabBar(() => { this.TabBarItem(page, idx) }) }, (page: PageItem, idx: number): string => idx.toString()) } .width('100%') .height('100%') .onChange((index: number) => { // ⚡ 切换事件 → 触发动画 this.switchTab(index); }) .barHeight(60) .barMode(BarMode.Fixed) .edgeEffect(EdgeEffect.None) .animationDuration(0) // 关闭 Tabs 内置动画 .clip(false) }

几个重要细节

  1. animationDuration(0):将 Tabs 组件的内置切换动画时长设为 0,完全由我们的animateTo控制动画。否则两套动画会冲突,导致视觉异常。

  2. layoutWeight(1)的位置@Builder方法返回void,不能对其链式调用属性。所以用一个Stack()PageContent包裹起来,在 Stack 上设置.layoutWeight(1)

  3. ForEach的 keyGenerator:第三个参数(page, idx) => idx.toString()告诉框架用索引作为唯一标识。这在列表 diff 时提升渲染性能。


6. 三次编译踩坑与修复

在实际编译过程中,我们遇到了 3 个错误和 1 个警告。这些是初学者最常遇到的问题,值得记录。

坑 1:误导入 Tabs / TabContent

错误信息

Module '"@kit.ArkUI"' has no exported member 'Tabs'. '"@kit.ArkUI"' has no exported member named 'TabContent'.

原因:在 HarmonyOS NEXT SDK 6.1.1(API 12)中,TabsTabContentColumnText等 UI 组件是全局内置符号,不需要也不应该从@kit.ArkUI导入。这与早期版本不同。

修复:直接删除 import 语句。

- import { Tabs, TabContent } from '@kit.ArkUI';

坑 2:@Builder 内声明局部变量

错误信息

Only UI component syntax can be written here.

原因:ArkTS 对@Builder有严格的语法限制——函数体内只能包含 UI 组件声明(Column、Text、Stack 等),不能出现constletiffor等非 UI 语句。

修复:将计算逻辑提取到组件的普通方法中。

// ❌ 错误:@Builder 内不能写 let/const/if @Builder private PageIndicator() { let activeness = ...; // 编译错误 if (activeness < 0) { ... } // 编译错误 // ... } // ✅ 正确:将逻辑提取到普通方法 private calcActiveness(dotPos: number, idx: number): number { let v = 1 - Math.abs(dotPos - idx); if (v < 0) v = 0; return v; } @Builder private PageIndicator() { Circle() .opacity(this.calcActiveness(this.dotPosition, idx)) }

坑 3:tabBar 参数数量不匹配

错误信息

Expected 0-1 arguments, but got 2.

原因.tabBar(this.TabItemBuilder, item)这种传参形式在 SDK 6.1.1 中不被支持。tabBar方法签名只接受一个CustomBuilder参数(即() => void类型的闭包)。

修复:使用闭包包裹 Builder 调用。

- .tabBar(this.TabItemBuilder, item) + .tabBar(() => { this.TabItemBuilder(item) })

坑 4:layoutWeight 链式调用在 @Builder 上

错误信息

Property 'layoutWeight' does not exist on type 'void'.

原因@Builder方法的返回值是void,不是 UI 组件。不能对 builder 调用链式属性。

修复:在外层包裹容器组件。

- this.PageContent(page).layoutWeight(1) // ❌ void 上没有 layoutWeight + Stack() { + this.PageContent(page) + }.layoutWeight(1) // ✅ Stack 组件上有 layoutWeight

坑 5:全局 animateTo 已弃用

警告信息

'animateTo' has been deprecated.

原因:SDK 6.1.1 将全局函数animateTo()标记为 deprecated,需要通过UIContext调用。

修复

- animateTo({ duration: 400 }, () => { ... }) + this.getUIContext()?.animateTo({ duration: 400 }, () => { ... })

7. 性能优化与最佳实践

7.1 动画性能优化

  1. 只动画化变换属性:尽量动画opacityscaletranslate等变换属性,避免动画widthheightpadding等布局属性。变换属性由 GPU 处理,不会触发重排。

  2. 控制动画并发数量:一次animateTo中同时动画化 3~5 个变量是合理的,但如果动画化几十个变量,可能会导致帧率下降。可以将复杂动画拆分为多个阶段。

  3. 选择合适的 duration200~400ms是移动端页面过渡动画的最佳区间。短于 200ms 感觉仓促,长于 500ms 感觉拖沓。

  4. 使用合适的 curve

    • 页面入场:Curve.FastOutSlowIn(先快后慢,感觉轻快)
    • 页面退场:Curve.EaseIn(先慢后快,感觉干脆)
    • 弹性效果:Curve.SpringMotion

7.2 @Builder 最佳实践

原则说明
保持纯 UI@Builder 内只放 UI 组件声明,计算逻辑放到普通方法
参数传递使用闭包.tabBar(() => { this.Builder(param) })
避免深层嵌套超过 3 层嵌套时,抽取子 @Builder
提取公共样式多个 @Builder 共用的样式,抽取为全局常量

7.3 Tabs 组件最佳实践

配置建议原因
index使用受控模式方便在 onChange 中编排动画
animationDuration设为 0避免与自定义 animateTo 冲突
barModeFixed(4 项以内)均分排列,视觉整齐
edgeEffectNone防止边缘回弹干扰切换体验

7.4 State 管理最佳实践

  • 尽量少用 @State:只有会影响 UI 渲染的变量才标记为 @State。不变的数据用private readonly
  • 动画变量的初始值:应与 build() 中的绑定一致。例如cardScale初始值1.0对应卡片正常大小。
  • 避免在 animateTo 中读变量animateTo的 closure 中只写赋值,不写读取。读取发生在每一帧的渲染阶段。

8. 总结

通过本文的实战,我们完成了以下目标:

学习点掌握程度
Tabs + TabContent 组件使用✅ 创建多页面标签栏
自定义 tabBar @Builder✅ 图标+文字标签栏
animateTo 显式动画✅ 驱动缩放、透明度、位置
多变量协同动画✅ switchTab 动画编排
@Builder 语法约束✅ 纯 UI 组件语法
编译错误排查✅ 5 个常见问题修复

扩展思考

本文的示例只是一个起点,你可以在此基础上进行更多探索:

  1. 添加滑动退场动画:在缩小淡入新内容之前,让旧内容先放大淡出(双阶段动画)
  2. 联动背景图:每个页面的背景是一个模糊的风景图,切换时背景图也平移过渡
  3. 物理弹簧效果:使用Curve.SpringMotion替代FastOutSlowIn,让卡片有弹性弹出的感觉
  4. 交互反馈:在标签栏上添加点击波纹效果,增强触摸反馈
  5. 无障碍适配:为每个 TabContent 添加accessibilityText,确保屏幕阅读器能正确朗读

推荐阅读

  • HarmonyOS NEXT 开发文档 — Tabs 组件
  • HarmonyOS NEXT 开发文档 — animateTo 动画
  • ArkTS 语法规范 — @Builder 装饰器

本文配套完整代码entry/src/main/ets/pages/Index.ets,325 行,已通过编译验证。
编译命令hvigorw assembleApp --no-daemon
运行方式:在 DevEco Studio 中打开项目,连接鸿蒙 NEXT 模拟器或真机运行。

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

关于CLaudex/ gpt的消耗监控管理

随着 AI API 的使用场景越来越多&#xff0c;个人开发者、团队测试和企业项目对接口服务的要求也越来越高。大家不只是需要一个能调用的接口&#xff0c;更需要稳定的响应、清晰的价格、顺手的接入体验&#xff0c;以及可以长期使用的服务。 我们的#程序员有文化系列# 就是围绕…

作者头像 李华
网站建设 2026/6/25 15:35:55

如何5步高效配置通达信缠论插件:专业交易者的实战指南

如何5步高效配置通达信缠论插件&#xff1a;专业交易者的实战指南 【免费下载链接】Indicator 通达信缠论可视化分析插件 项目地址: https://gitcode.com/gh_mirrors/ind/Indicator 想在通达信中实现缠论自动化分析&#xff1f;还在手动绘制中枢和线段浪费时间&#xff…

作者头像 李华
网站建设 2026/6/25 15:32:39

苹果Siri系统级LLM重构:端侧大模型与隐私优先架构解析

1. 项目概述&#xff1a;这不是“接入”&#xff0c;而是系统级能力重构“Apple Public Betas Bring ChatGPT To Siri”这个标题&#xff0c;表面看是一条科技新闻快讯&#xff0c;但作为在iOS/macOS生态里摸爬滚打十一年、参与过7代Siri架构演进测试的从业者&#xff0c;我必须…

作者头像 李华
网站建设 2026/6/25 15:31:38

暑假机器人AI课卷不卷?冷静!零基础家长最该关心的其实是这三点

最近家长群里的画风特别统一&#xff1a;一边转发AI要取代多少职业的新闻&#xff0c;一边疯狂打听哪里有靠谱的暑假机器人AI课。好像暑假不给孩子报一个&#xff0c;下学期开学就要掉队了。 我能理解这种焦虑&#xff0c;但作为一个陪着孩子从零开始走过这条路的人&#xff0c…

作者头像 李华
网站建设 2026/6/25 15:30:16

Grok 4.1本地部署指南:纯内网启用Thinking模式实操

1. 项目概述&#xff1a;这不是“翻墙教程”&#xff0c;而是一次本地化AI推理环境的实操重建 “Grok 4.1国内使用指南&#xff1a;2026最新无需魔法镜像&#xff08;支持Thinking模式&#xff09;”——这个标题里藏着三个关键信号&#xff1a;第一&#xff0c;“Grok 4.1”指…

作者头像 李华
网站建设 2026/6/25 15:30:05

roop-unleashed:零代码AI换脸工具完整使用指南与深度技术解析

roop-unleashed&#xff1a;零代码AI换脸工具完整使用指南与深度技术解析 【免费下载链接】roop-unleashed Evolved Fork of roop with Web Server and lots of additions 项目地址: https://gitcode.com/gh_mirrors/ro/roop-unleashed 在人工智能技术飞速发展的今天&am…

作者头像 李华