
HarmonyOS ArkUI 雷达扫描动画案例 —— 模拟超级终端连接设备界面效果## 前言鸿蒙超级终端是HarmonyOS的标志性功能用户可以在手机上发现并连接附近的平板、耳机、手表、电脑等设备。本案例将使用ArkUI开发一个模拟超级终端连接设备界面的雷达扫描动画涵盖扫描光束旋转、波纹扩散、设备逐个发现等核心效果。本案例使用**状态管理V2ComponentV2 ObservedV2 Local Trace**开发综合运用animateTo显式动画和transition组件内转场两大动画技术。一、效果概述1.1 功能特性雷达扫描光束从中心向外扩散的扇形光束匀速旋转animateTo驱动同心圆环背景4层同心圆环构成雷达网格波纹扩散效果从中心周期性向外扩散的波纹transition实现设备逐个发现每1.2秒发现一个新设备淡入缩放动画出现transition实现设备列表发现的设备在下方列表中逐个滑入显示transition实现扫描控制一键开始/停止扫描1.2 技术栈技术点说明状态管理V2ComponentV2、ObservedV2、Local、TraceanimateTo驱动扫描光束无限旋转transition波纹扩散、设备节点淡入、列表项滑入setInterval控制波纹周期和设备发现节奏Stack布局雷达区域的多层叠加二、项目结构entry/src/main/ets/ ├── common/ │ └── RadarScanConstants.ets // 常量定义 ├── entryability/ │ └── EntryAbility.ets // 入口Ability └── pages/ └── RadarScanPage.ets // 雷达扫描主页面三、实现步骤Task 1定义常量文件创建RadarScanConstants.ets集中管理所有动画参数和布局常量便于维护和调整。// 先定义位置类ArkTS严格模式禁止对象字面量作为类型声明exportclassDevicePosition{x:number0;y:number0;constructor(x:number,y:number){this.xx;this.yy;}}exportclassRadarScanConstants{// 布局尺寸staticreadonlyRADAR_SIZE300;staticreadonlyCENTER_DEVICE_SIZE56;staticreadonlyNODE_SIZE40;// 扫描光束参数staticreadonlySCAN_DURATION3000;staticreadonlyFULL_ROTATION360;staticreadonlyMIN_FRAME_RATE10;staticreadonlyMAX_FRAME_RATE120;staticreadonlyEXPECTED_FRAME_RATE60;// 波纹扩散参数staticreadonlyRIPPLE_DURATION2500;staticreadonlyRIPPLE_SCALE3.5;staticreadonlyRIPPLE_INITIAL_OPACITY0.4;// 设备发现参数staticreadonlyDISCOVER_INTERVAL1200;staticreadonlyMAX_DEVICES6;staticreadonlyDEVICE_APPEAR_DURATION600;// 设备位置使用显式类实例化不能用对象字面量staticreadonlyDEVICE_POSITIONS:DevicePosition[][newDevicePosition(80,-60),newDevicePosition(-70,-80),newDevicePosition(100,40),newDevicePosition(-90,50),newDevicePosition(30,100),newDevicePosition(-40,-110)];// 设备名称staticreadonlyDEVICE_NAMES:string[][MatePad Pro,FreeBuds 5,MateBook X,Watch GT4,Sound X,Vision V5];}设计思路将设备位置、动画时长、帧率范围等参数全部提取为常量避免在UI代码中硬编码数值后续如需调整动画速度或设备位置只需修改此文件。ArkTS严格模式注意ArkTS禁止使用对象字面量作为类型声明如Array{ x: number; y: number }和无类型的对象字面量如{ x: 80, y: -60 }。必须定义显式的类如DevicePosition并通过new关键字实例化。详见本项目配套的《ArkTS常见编译错误修复指南》。Task 2定义设备数据模型使用ObservedV2和Trace定义设备数据模型实现细粒度的响应式更新ObservedV2classDiscoveredDevice{Tracename:string;TraceoffsetX:number0;TraceoffsetY:number0;Tracediscovered:booleanfalse;}为什么用ObservedV2而非ObservedObservedV2配合Trace可以精确追踪每个属性的变化当某个设备的discovered变为true时只有该设备的UI会重新渲染避免整个设备列表的无效重渲染提升性能Task 3构建雷达同心圆背景使用ForEach和Circle组件绘制4层同心圆环BuilderradarRings(){Stack(){ForEach([1,2,3,4],(index:number){Circle().width(RC.RADAR_SIZE*index/RC.RING_COUNT).height(RC.RADAR_SIZE*index/RC.RING_COUNT).fill(Color.Transparent).stroke(RC.COLOR_RING).strokeWidth(RC.RING_BORDER_WIDTH).opacity(RC.RING_OPACITY(index*0.03))},(index:number)index.toString())// ForEach必须提供keyGenerator}.width(RC.RADAR_SIZE).height(RC.RADAR_SIZE)}每层圆环的尺寸按比例递增75px → 150px → 225px → 300px透明度从内到外逐渐增加营造雷达网格的纵深感。Task 4实现扫描光束旋转animateTo扫描光束是雷达动画的核心视觉效果使用animateTo驱动无限匀速旋转BuilderscanBeam(){Stack(){// 上半部分渐变光束位于中心上方Column().width(RC.RADAR_SIZE*0.5).height(RC.RADAR_SIZE*0.5).linearGradient({angle:180,colors:[[Color.Transparent,0.0],// 顶部透明[RC.COLOR_SCAN_BEAM,0.6],// 中部实色[RC.COLOR_SCAN_BEAM,1.0]// 底部实色]}).borderRadius({topLeft:0,topRight:RC.RADAR_SIZE*0.5,bottomRight:0,bottomLeft:0}).opacity(0.5).position({x:RC.RADAR_SIZE*0.5,y:0})}.width(RC.RADAR_SIZE).height(RC.RADAR_SIZE).rotate({angle:this.rotateAngle,x:0,y:0,z:1})}启动旋转动画的方法startBeamRotation():void{this.getUIContext().animateTo({duration:RC.SCAN_DURATION,// 3秒一圈curve:Curve.Linear,// 匀速iterations:-1,// 无限循环playMode:PlayMode.Normal,expectedFrameRateRange:{min:10,max:120,expected:60}},(){this.rotateAngleRC.FULL_ROTATION;// 目标旋转360度});}关键技术点线性渐变光束使用linearGradient从透明渐变到实色模拟雷达扫描的扇形波束圆角裁剪通过borderRadius的topRight设置为半圆形形成扇形外观Stack旋转容器光束放置在全尺寸的Stack内Stack绕自身中心旋转自然实现绕雷达中心旋转帧率控制expectedFrameRateRange告诉系统期望60fps在低端设备上可以降低以保证核心功能Task 5实现波纹扩散效果transition波纹从中心向外扩散使用transition的消失效果实现BuilderrippleEffect(){Circle().width(RC.RADAR_SIZE/RC.RIPPLE_SCALE).height(RC.RADAR_SIZE/RC.RIPPLE_SCALE).fill(RC.COLOR_RIPPLE).opacity(RC.RIPPLE_INITIAL_OPACITY).transition(TransitionEffect.asymmetric(TransitionEffect.IDENTITY,// 出现时无过渡瞬间出现TransitionEffect.OPACITY// 消失时放大淡出.animation({duration:RC.RIPPLE_DURATION,curve:Curve.EaseOut}).combine(TransitionEffect.scale({x:RC.RIPPLE_SCALE,y:RC.RIPPLE_SCALE}))))}通过setInterval周期性切换布尔值触发transitionstartRipple():void{this.rippleFlag!this.rippleFlag;// 首次false→true组件出现this.rippleTimerIdsetInterval((){this.rippleFlag!this.rippleFlag;// 周期切换消失→出现→消失...},RC.RIPPLE_DURATION);}原理说明rippleFlag从false变为true时Circle组件插入容器触发appear效果IDENTITY无过渡rippleFlag从true变为false时Circle组件移除触发disappear效果放大3.5倍 淡出每2.5秒循环一次形成连续的波纹扩散Task 6实现设备逐个发现transition每个设备节点在发现时以淡入缩放动画出现BuilderdeviceNode(device:DiscoveredDevice){Column(){Stack(){Circle().width(RC.NODE_SIZE).height(RC.NODE_SIZE).fill(RC.COLOR_NODE_BG).stroke(RC.COLOR_NODE_ACTIVE).strokeWidth(1.5)Text(this.getDeviceIcon(device.name)).fontSize(18)}Text(device.name).fontSize(RC.FONT_DEVICE_NAME).fontColor(RC.COLOR_TEXT_SECONDARY).margin({top:4}).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis}).constraintSize({maxWidth:60})// 注意Text不支持maxWidth需用constraintSize}.alignItems(HorizontalAlign.Center).transition(TransitionEffect.asymmetric(TransitionEffect.OPACITY.animation({duration:RC.DEVICE_APPEAR_DURATION,curve:Curve.EaseOut}).combine(TransitionEffect.scale({x:0.3,y:0.3})),TransitionEffect.IDENTITY)).offset({x:device.offsetX,y:device.offsetY})}设备发现定时器startDeviceDiscovery():void{this.discoverTimerIdsetInterval((){if(this.discoveredCountRC.MAX_DEVICES){this.devices[this.discoveredCount].discoveredtrue;this.discoveredCount;}else{clearInterval(this.discoverTimerId);}},RC.DISCOVER_INTERVAL);}每1.2秒将一个设备的discovered设为true触发if (device.discovered)条件渲染设备节点插入容器时自动执行transition淡入动画。Task 7设备列表滑入效果下方设备列表中每个item也有独立的transition动画.transition(TransitionEffect.asymmetric(TransitionEffect.OPACITY.animation({duration:400,curve:Curve.EaseOut}).combine(TransitionEffect.translate({x:-30})),TransitionEffect.IDENTITY))列表项从左侧30px处滑入同时透明度从0渐变到1。Task 8页面整体布局build(){Column(){// 标题Text(超级终端).fontSize(RC.FONT_TITLE).fontWeight(FontWeight.Bold)Text(搜索并连接附近设备).fontSize(RC.FONT_SUBTITLE)// 雷达区域Stack多层叠加this.radarArea();// 状态信息this.statusSection();// 控制按钮this.controlButton();// 设备列表Scroll(){this.deviceList();}.layoutWeight(1)}.width(100%).height(100%).backgroundColor(RC.COLOR_BG).alignItems(HorizontalAlign.Center)}四、关键代码讲解4.1 状态管理V2的应用EntryComponentV2struct RadarScanPage{LocalisScanning:booleanfalse;LocalrotateAngle:number0;LocalrippleFlag:booleanfalse;LocaldiscoveredCount:number0;Localdevices:DiscoveredDevice[][];// ...}V2 vs V1对比V1装饰器V2装饰器说明StateLocal组件内部状态ObservedObservedV2Trace对象属性追踪PropParam父组件传入参数LinkEvent子组件回调4.2 animateTo与transition的协作本案例中两种动画技术各司其职动画效果技术方案原因光束旋转animateTo需要无限循环 精确角度控制波纹扩散transition组件出现/消失的自然过渡设备发现transition条件渲染触发的淡入效果列表滑入transitionForEach增删的自动过渡4.3 资源清理在组件销毁时必须清理定时器避免内存泄漏aboutToDisappear():void{this.stopAllTimers();}stopAllTimers():void{if(this.rippleTimerId!-1){clearInterval(this.rippleTimerId);this.rippleTimerId-1;}if(this.discoverTimerId!-1){clearInterval(this.discoverTimerId);this.discoverTimerId-1;}}4.4 路由配置在main_pages.json中注册新页面{src:[pages/Index,pages/RadarScanPage]}在EntryAbility.ets中加载新页面windowStage.loadContent(pages/RadarScanPage,(err){// ...});五、实现流程总结1. 创建常量文件 RadarScanConstants.ets ↓ 2. 创建数据模型 DiscoveredDeviceObservedV2 Trace ↓ 3. 创建主页面 RadarScanPageComponentV2 Local ↓ 4. 构建雷达同心圆背景Circle ForEach Stack ↓ 5. 实现扫描光束旋转animateTo rotate linearGradient ↓ 6. 实现波纹扩散效果transition setInterval Circle ↓ 7. 实现设备逐个发现transition setInterval offset ↓ 8. 实现设备列表滑入transition ForEach translate ↓ 9. 定义DevicePosition类解决ArkTS对象字面量编译错误 ↓ 10. 注册路由配置 更新EntryAbility加载页 ↓ 11. 运行调试修复constraintSize等API兼容问题调整动画参数六、性能优化建议帧率范围控制旋转动画设置expectedFrameRateRange在低端设备上自动降帧避免过度渲染使用Trace精确追踪属性变化只重渲染受影响的组件及时清理定时器在aboutToDisappear中清理所有setIntervalclip裁剪雷达区域设置clip(true)防止超出边界的波纹或设备节点影响其他区域控制同时动画数波纹扩散和设备发现使用不同的定时器避免同一时刻触发过多动画ArkTS严格模式兼容定义显式类替代对象字面量类型使用constraintSize替代maxWidth等不存在的API七、扩展思路添加连接动画点击已发现设备后播放一条从设备到中心的连线动画声音反馈设备发现时播放短促的提示音真实蓝牙/WiFi扫描集成ohos.bluetooth.ble或ohos.wifiAPI实现真实设备扫描暗色模式适配使用$r(app.color.xxx)资源引用替代硬编码颜色值Canvas绘制使用Canvas组件替代Circle/Stack绘制更精细的雷达网格和刻度八、总结本案例通过animateTo和transition两种动画技术的组合成功实现了超级终端连接设备界面的雷达扫描动画。关键技术要点animateTo负责持续驱动的旋转动画无限循环 匀速transition负责组件出现/消失时的过渡效果波纹扩散、设备淡入、列表滑入ComponentV2 ObservedV2提供V2状态管理的现代化开发体验常量提取让动画参数集中管理便于调优掌握这两种动画技术后你可以轻松实现加载动画、通知弹出、页面切换等丰富的UI交互效果。