ArkTS 驱动鸿蒙元服务开发:界面布局与交互逻辑实战
基于 ArkTS 的鸿蒙元服务开发实践,涵盖项目目录结构、主页面声明式布局与安全区域适配。核心功能包括决策币翻转动画、幸运号码状态切换及转盘抽奖逻辑,并实现元服务卡片生命周期管理与界面设计。通过 @StorageProp 和 router 实现跨组件状态共享与导航,提供完整的功能模块代码示例与优化建议。

基于 ArkTS 的鸿蒙元服务开发实践,涵盖项目目录结构、主页面声明式布局与安全区域适配。核心功能包括决策币翻转动画、幸运号码状态切换及转盘抽奖逻辑,并实现元服务卡片生命周期管理与界面设计。通过 @StorageProp 和 router 实现跨组件状态共享与导航,提供完整的功能模块代码示例与优化建议。



entry/src/main/ets/
├── entryability/ # 应用程序入口能力
│ └── EntryAbility.ets # 主入口文件
├── entryformability/ # 卡片能力相关
├── images/ # 图片资源文件
├── pages/ # 页面组件
│ ├── Index.ets # 主页面
│ ├── FingertipTable.ets # 决策币功能
│ ├── LuckyNumber.ets # 幸运号码功能
│ └── TurnLuck.ets # 转出好运功能
└── widget/ # 小组件相关

声明式布局:Column + List
响应式状态管理:@StorageProp
统一路由跳转机制:router.pushUrl
交互动画多态样式:stateStyles + animation
页面外层采用 Column 实现垂直布局,内部通过 List 组件构建'决策币、幸运号码、转出好运'功能入口,使用 padding 动态适配顶部与底部安全区域。
Column(){
NoticeBar().margin({bottom:10})
List({space:40}){
// 子项列表
}.alignListItem(ListItemAlign.Center)
}.padding({top:this.topHeight +51,bottom:this.bottomHeight }).height('100%').width('100%')
每个 ListItem 作为独立功能入口,通过 stateStyles 实现按压缩放动画增强交互体验,以 router.pushUrl() 完成页面的跳转。
ListItem(){
Row(){
Text('决策币').fontSize(50).fontColor('#FFFFFF')
Image('images/shouzhi.svg').width(40)
}
}.backgroundColor('#6699FF').width('90%').height(170).borderRadius(20).stateStyles({
normal:{.scale({x:1,y:1})},
pressed:{.scale({x:0.95,y:0.95})}
}).animation({duration:200}).onClick(_=>{
router.pushUrl({url:'pages/FingertipTable'})
})
相同跳转逻辑复用:决策币、幸运号码、转出好运三个功能入口,通过统一的 router.pushUrl() 实现页面导航,保证路由逻辑一致。
router.pushUrl({url:'pages/FingertipTable'})
router.pushUrl({url:'pages/LuckyNumber'})
router.pushUrl({url:'pages/TurnLuck'})

应用启动时创建主窗口并设置全屏显示
const win = windowStage.getMainWindowSync()
win.setWindowLayoutFullScreen(true)
动态计算顶部与底部安全区高度并存入全局状态,实现多设备屏幕与系统栏的自适应显示
// 获取系统状态栏(如信号栏、时间栏)等区域的避让范围
const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect
// 将顶部安全区域的高度(单位像素)转换为 vp 并存储到全局状态,用于页面动态适配
AppStorage.setOrCreate<number>('topHeight',px2vp(top.height))
// 获取系统导航指示栏(如手势导航区域)的避让范围
const bottom = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect
// 将底部安全区域的高度转换为 vp 并存储到全局状态,用于底部留白或控件布局自适应
AppStorage.setOrCreate<number>('bottomHeight',px2vp(bottom.height))
窗口创建完成后加载主页面 Index.ets,正式进入应用界面渲染阶段
windowStage.loadContent('pages/Index',(err)=>{});

循环调用 animateToImmediately(),短时间内多次递增旋转角度,实现硬币连续旋转的视觉效果,基于状态驱动的动画更新,让 UI 与数据绑定紧密,不依赖复杂的帧渲染逻辑
.animateToImmediately({delay: i * totalAnimationDuration / maxAnimationSteps,duration:100,},()=>{
this.rotationAngle +=90;// 每次增加 90 度旋转
});
双阶段动画链式执行让动作更贴近真实物理效果第一段:硬币上抛,verticalOffset 为负值模拟上升第二段:硬币下落,verticalOffset 回归 0,恢复初始位置
animateToImmediately({duration: totalAnimationDuration /2,onFinish:()=>{
animateToImmediately({duration: totalAnimationDuration /2,onFinish:()=>{/* 落地后逻辑 */}},()=>{
this.verticalOffset =0;
});
}},()=>{
this.verticalOffset =-100*(1+ Math.floor(Math.random()*5));
});


@StorageProp 保存顶部与底部间距等布局数据在不同组件间共享
@State 控制组件内部动态状态通过 isUnit 判断显示个位数或十位数
@StorageProp('topHeight') topHeight: number =0
@StorageProp('bottomHeight') bottomHeight: number =0
@State isUnit: boolean =true
两个按钮分别对应个位数和十位数,点击任意一个都会切换 isUnit 状态,按钮颜色与文字样式随状态变化而更新,从而实现视觉与逻辑同步的动态切换效果
Row(){
Column(){
Text('切换个位数').fontSize(15).fontColor(this.isUnit ? Color.White :'#00000').fontWeight(900)
}.onClick(_=>{this.isUnit =!this.isUnit }).backgroundColor(this.isUnit ? Color.Red : Color.White).height(50).width('50%')
Column(){
Text('切换十位数').fontSize(15).fontColor(this.isUnit ?'#00000': Color.White).fontWeight(900)
}.onClick(_=>{this.isUnit =!this.isUnit }).backgroundColor(this.isUnit ? Color.White : Color.Red).height(50).width('50%')
}

数据驱动转盘的可视化和旋转逻辑
// 计算每个单元格在转盘上的角度和旋转信息
private calculateAngles(){
// 计算所有单元格比例的总和
const totalProportion =this.cells.reduce((sum, cell)=> sum + cell.proportion,0);
// 根据每个单元格的比例计算对应的扇形角度
this.cells.forEach(cell=>{
cell.angle =(cell.proportion *360)/ totalProportion;// 扇形角度 = 单元格比例占比 * 360°
});
let cumulativeAngle =0;// 用于累加每个单元格的角度,确定起始位置
// 遍历单元格,设置起始角度、结束角度及旋转角度
this.cells.forEach(cell=>{
cell.angleStart = cumulativeAngle;// 扇形起始角度
cumulativeAngle += cell.angle;// 更新累计角度
cell.angleEnd = cumulativeAngle;// 扇形结束角度
cell.rotate = cumulativeAngle -(cell.angle /2);// 扇形文本或元素旋转角度,使其居中显示
});
}
实现转盘动画和随机选择功能
// '开始'按钮点击事件:触发转盘旋转
Button('开始').onClick(()=>{
// 如果转盘正在旋转,直接返回,避免重复触发
if(this.isAnimating)return;
this.selectedName ="";// 清空当前选中名称
this.isAnimating =true;// 标记动画开始
// 调用动画函数进行旋转
animateTo({duration:5000,// 动画持续时间 5 秒
curve: Curve.EaseInOut,// 缓入缓出动画曲线
onFinish:()=>{
// 动画结束回调
this.currentAngle %=360;// 保持角度在 0~360° 范围内
// 判断当前角度落在哪个单元格
for(const cell ofthis.cells){
if(360-this.currentAngle >= cell.angleStart &&360-this.currentAngle <= cell.angleEnd){
this.selectedName = cell.title;// 设置选中单元格的标题
break;// 找到目标单元格后退出循环
}
}
this.isAnimating =false;// 动画结束,重置状态
},
},()=>{
// 动画进行中回调:更新当前角度,实现旋转效果
this.currentAngle +=(360*5+ Math.floor(Math.random()*360));// 随机旋转多圈
});
});
提供转盘单元格内容和比例的动态管理
// 遍历每个单元格,创建可编辑行
ForEach(this.cells,(item: Cell,index: number)=>{
Row(){
// 文本输入框:显示并编辑单元格标题
TextInput({text: item.title }).onChange(value=> item.title = value);
// 内容变化时更新单元格标题
// 计数器组件:调整单元格比例
CounterComponent({options:{numberOptions:{value: item.proportion,// 初始比例值
onChange:(v)=>{// 值变化回调
item.proportion = v;// 更新单元格比例
this.calculateAngles();// 重新计算转盘角度
}}
}});
// 删除按钮:移除当前单元格
Button('删除').onClick(()=>{
this.cells.splice(index,1);// 从数组中删除
this.calculateAngles();// 重新计算转盘角度
});
}});
// 添加新单元格按钮
Button('添加新内容').onClick(()=>{
// 新建单元格,分配颜色并添加到数组
this.cells.push(newCell(1,"新内容",this.colorPalette[this.colorIndex++%this.colorPalette.length]));
this.calculateAngles();// 更新转盘角度
});
卡片生命周期管理(EntryFormAbility.ets) :介绍卡片的创建、更新、删除等生命周期方法 实现卡片用户界面设计(WidgetCard.ets) :分析卡片的 UI 结构、组件使用和交互设计
import{ formBindingData, FormExtensionAbility, formInfo }from'@kit.FormKit';
import{ Want }from'@kit.AbilityKit';
// 表单扩展能力类
exportdefaultclassEntryFormAbilityextendsFormExtensionAbility{
// 当添加表单时调用,返回 FormBindingData 对象
onAddForm(want: Want){
let formData ='';
return formBindingData.createFormBindingData(formData);
}
// 当临时表单成功转换为普通表单时调用
onCastToNormalForm(formId: string){
// 可在此处理转换后的逻辑
}
// 通知表单提供者更新指定表单
onUpdateForm(formId: string){
// 可在此实现更新表单逻辑
}
// 指定表单触发事件时调用
onFormEvent(formId: string,message: string){
// 可在此处理表单事件逻辑
}
// 通知表单提供者指定表单已被销毁
onRemoveForm(formId: string){
// 可在此处理表单移除逻辑
}
// 获取表单状态时调用,返回 FormState 对象
onAcquireFormState(want: Want){
return formInfo.FormState.READY;// 表单准备就绪状态
}
}
@Entry @Component struct WidgetCard {
/* * 卡片标题文本 */ readonly TITLE: string ='开始决策 🫵';
/* * 点击行为类型,例如路由跳转 */ readonly ACTION_TYPE: string ='router';
/* * 目标能力或页面名称 */ readonly ABILITY_NAME: string ='EntryAbility';
/* * 跳转时传递的参数消息 */ readonly MESSAGE: string ='add detail';
/* * 卡片宽度设置 */ readonly FULL_WIDTH_PERCENT: string ='100%';
/* * 卡片高度设置 */ readonly FULL_HEIGHT_PERCENT: string ='100%';
build(){
// 可点击卡片,点击后触发路由或能力调用
FormLink({action:this.ACTION_TYPE,// 动作类型
abilityName:this.ABILITY_NAME,// 目标能力/页面
params:{message:this.MESSAGE}// 传递参数
}){
// 卡片内部布局
Row(){
Column(){
// 显示卡片标题
Text(this.TITLE).fontSize($r('app.float.font_size'))// 字体大小
.fontWeight(FontWeight.Medium)// 字体粗细
.fontColor($r('app.color.item_title_font'))// 字体颜色
}.width(this.FULL_WIDTH_PERCENT)// 列宽度填满容器
}.height(this.FULL_HEIGHT_PERCENT)// 行高度填满容器
}
}
}
文章以鸿蒙元服务的决策币、幸运号码、转盘抽奖功能为核心,展示 ArkTS 开发实践:声明式布局搭 UI,@State 等做响应式管理,animateToImmediately 与 router 实现动画和导航。覆盖应用入口适配、功能核心逻辑及元服务卡片设计,形成完整开发流程,为鸿蒙元服务开发提供可复用参考。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online