做过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了。
这里有个细节——animateTo的duration和setTimeout的延迟都是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)}整个流程是这样的:
- 所有点瞬间变暗(0.3)
- 第1个点在0ms亮起,第2个在200ms,第3个在400ms,第4个在600ms
- 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应用的时候,加载动画虽然是个小功能,但真的能体现出品质感。与其用千篇一律的系统组件,不如花半小时自己写一个。代码量不大,效果提升明显。
试试看,把你的加载动画换成自己的设计,用户虽然说不清哪里变了,但就是觉得"这应用用着舒服"。