加载动画——旋转Spinner与点加载的实现
2026/6/8 3:13:26 网站建设 项目流程

做过PC端应用的朋友都知道,加载动画是用户等待时最直接的"安慰剂"。一个好看的loading效果,能让用户觉得"这应用挺精致的",而不是"这破玩意是不是卡死了"。

说实话,HarmonyOS6 PC开发中,系统自带的LoadingProgress组件能用,但太普通了。你做个正经项目,设计师肯定不让你用默认的——太没辨识度了。

今天就带大家从零手搓两种最常见的加载动画:旋转Spinner点状加载器。都是在PC端应用里出镜率极高的样式。

旋转Spinner:经典永不过时

你肯定见过这种效果——一个圆环,有一段颜色不一样,然后不停地转。Chrome浏览器加载页面的时候就是这玩意。

坦白讲,原理特别简单:画一个圆环,让顶部边框颜色和其余三边不同,然后持续旋转就行了。

核心思路拆解

先看圆环怎么画。不用Canvas,不用图片,直接用Column加边框就能搞定:

Column().width(50).height(50).border({width:{top:4},color:'#007DFF'}).border({width:{bottom:4},color:'#E0E0E0'}).border({width:{left:4},color:'#E0E0E0'}).border({width:{right:4},color:'#E0E0E0'}).borderRadius(25).rotate({angle:this.spinnerRotate}).animation({duration:1000,curve:Curve.Linear})

关键点在这:

  • borderRadius(25)正好是宽高的一半(50/2),这样就把方形变成了正圆
  • 四条边分别设置不同颜色,顶部是主题蓝#007DFF,其余三边是浅灰#E0E0E0
  • rotate({ angle: this.spinnerRotate })负责旋转,角度由状态变量控制
  • animation设置了Curve.Linear线性缓动,保证旋转速度均匀

让它转起来

光有静态圆环不够,得让它持续转。这里用了一个递归 +animateTo的经典套路:

@StatespinnerRotate:number=0@StateisSpinning:boolean=false_spinLoop(){if(!this.isSpinning)returnanimateTo({duration:1000,curve:Curve.Linear},()=>{this.spinnerRotate+=360})setTimeout(()=>{this._spinLoop()},1000)}

逻辑很直白:每次调用让角度加360度(刚好转一圈),动画时长1秒。等这圈转完,再递归调用自己。

isSpinning是控制开关。想停的时候把它设成false,下一次递归进来就直接return了。

这里有个细节——animateTodurationsetTimeout的延迟都是1000ms,刚好对接上。如果setTimeout比animateTo短,会出现动画还没结束就开始下一圈,视觉上会"跳"。

点状加载器:更有节奏感

第二种是点状加载——几个圆点依次亮起、熄灭,像流水灯一样。这种效果在微信、支付宝的加载场景里很常见。

实现思路

用4个圆点,通过控制每个点的opacity(透明度)和scale(缩放),让它们依次变化:

@StatedotStates:number[]=[1,1,1,1]Row({space:8}){ForEach([0,1,2,3],(idx:number)=>{Column().width(12).height(12).backgroundColor('#007DFF').borderRadius(6).opacity(this.dotStates[idx]).scale({x:this.dotStates[idx],y:this.dotStates[idx]}).animation({duration:300,curve:Curve.EaseInOut})})}

dotStates数组存储每个点的状态值,1表示"亮",0.3表示"暗"。这个值同时控制透明度和缩放——暗的时候点也会缩小,看起来更有立体感。

交替动画的逻辑

_dotLoop(){if(!this.isSpinning)return// 先把所有点变暗for(leti=0;i<4;i++){this.dotStates[i]=0.3}// 然后依次点亮每个点,间隔200msfor(leti=0;i<4;i++){setTimeout(()=>{if(!this.isSpinning)returnthis.dotStates[i]=1},i*200)}// 800ms后开始下一轮setTimeout(()=>{this._dotLoop()},800)}

整个流程是这样的:

  1. 所有点瞬间变暗(0.3)
  2. 第1个点在0ms亮起,第2个在200ms,第3个在400ms,第4个在600ms
  3. 800ms后重新开始下一轮

因为有.animation({ duration: 300 })修饰,每个点的亮暗变化不是突变的,而是有300ms的过渡。这就产生了那种柔和的"呼吸"效果。

LoadingProgress vs 自定义加载动画

你可能会问,系统有LoadingProgress组件,干嘛还要自己写?

说实话,LoadingProgress就一个环形转圈,样式固定。你改改颜色、改改大小还行,想做更个性化的效果就没辙了。

自定义加载动画的优势:

  • 完全可控:颜色、速度、大小、形状,全都能自定义
  • 品牌一致性:可以和你的应用主题风格完全统一
  • 组合灵活:旋转 + 缩放 + 透明度,想怎么搭怎么搭

不过也别过度设计。加载动画毕竟是功能性组件,花里胡哨反而影响体验。PC端屏幕上,一个干净的旋转环或者几个跳动的小点,够用了。

更多加载指示器设计思路

给大家拓展几种常见的设计方向:

脉冲环

一个圆环不断放大同时变透明,然后重置,循环往复。用scale+opacity+animateTo就能做:

// 思路:scale从0.5到1.5,opacity从0.8到0,循环animateTo({duration:1200,iterations:-1,curve:Curve.EaseOut},()=>{this.pulseScale=1.5this.pulseOpacity=0})

渐变旋转环

和Spinner类似,但不是用不同颜色的边框,而是用conicGradient(锥形渐变)做出彩虹色圆环,再整体旋转。PC端的大屏数据加载页面用这个效果很出彩。

骨架屏

严格来说不算"加载动画",但效果比转圈圈好太多。用灰色色块模拟内容区域的大致布局,数据加载完成后替换为真实内容。HarmonyOS6 PC端的大尺寸屏幕特别适合骨架屏——因为屏幕大,一个转圈圈的loading在屏幕中间显得特别孤单。

踩坑记录

写这类加载动画的时候,有几个容易踩的坑:

1. 动画内存泄漏

递归调用 + setTimeout,如果组件销毁了但没清理,setTimeout还会继续执行。建议在aboutToDisappear生命周期里把isSpinning设为false

2. 旋转角度累积

spinnerRotate会一直累加,长时间运行后数值会很大。虽然ArkUI能处理大数值的rotate,但如果你介意,可以每次加完后取模360。不过注意,取模的时候如果角度突变,视觉上会闪一下。所以其实不取模反而更稳。

3. ForEach的key

点状加载器里用ForEach渲染圆点,最好提供稳定的key。虽然这个例子中数组固定不会有问题,但如果你动态增减点数,没有key会导致复用异常。

完整代码参考

最后把完整的组件代码放出来,方便大家直接拿去改:

@Entry@Componentstruct LoadingAnimationDemo{@StateisShow:boolean=true@StatespinnerRotate:number=0@StateisSpinning:boolean=false@StatedotStates:number[]=[1,1,1,1]build(){Column(){if(this.isShow){Scroll(){Column(){Text('加载动画').fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:8})Column(){// ---- 旋转加载器 ----Text('旋转加载器').fontSize(14).fontWeight(FontWeight.Medium).margin({bottom:12})Row(){Column().width(50).height(50).border({width:{top:4},color:'#007DFF'}).border({width:{bottom:4},color:'#E0E0E0'}).border({width:{left:4},color:'#E0E0E0'}).border({width:{right:4},color:'#E0E0E0'}).borderRadius(25).rotate({angle:this.spinnerRotate}).animation({duration:1000,curve:Curve.Linear})}.width('100%').justifyContent(FlexAlign.Center)// ---- 点状加载器 ----Text('点状加载器').fontSize(14).fontWeight(FontWeight.Medium).margin({top:20,bottom:12})Row({space:8}){ForEach([0,1,2,3],(idx:number)=>{Column().width(12).height(12).backgroundColor('#007DFF').borderRadius(6).opacity(this.dotStates[idx]).scale({x:this.dotStates[idx],y:this.dotStates[idx]}).animation({duration:300,curve:Curve.EaseInOut})})}.width('100%').justifyContent(FlexAlign.Center)// ---- 控制按钮 ----Row({space:10}){Button('开始加载').onClick(()=>{if(this.isSpinning)returnthis.isSpinning=truethis._spinLoop()this._dotLoop()})Button('停止加载').onClick(()=>{this.isSpinning=false})}.width('100%').justifyContent(FlexAlign.SpaceEvenly).margin({top:16})}.width('100%').backgroundColor('#FFFFFF').borderRadius(12).padding(16)}.width('100%')}.layoutWeight(1)}}.width('100%').height('100%').backgroundColor('#F5F6FA').padding(16)}_spinLoop(){if(!this.isSpinning)returnanimateTo({duration:1000,curve:Curve.Linear},()=>{this.spinnerRotate+=360})setTimeout(()=>{this._spinLoop()},1000)}_dotLoop(){if(!this.isSpinning)returnfor(leti=0;i<4;i++){this.dotStates[i]=0.3}for(leti=0;i<4;i++){setTimeout(()=>{if(!this.isSpinning)returnthis.dotStates[i]=1},i*200)}setTimeout(()=>{this._dotLoop()},800)}}

做HarmonyOS6 PC应用的时候,加载动画虽然是个小功能,但真的能体现出品质感。与其用千篇一律的系统组件,不如花半小时自己写一个。代码量不大,效果提升明显。

试试看,把你的加载动画换成自己的设计,用户虽然说不清哪里变了,但就是觉得"这应用用着舒服"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询