09![]()
数据持久化只是第一步,将数据以优雅的交互形式呈现给用户才是最终目标。本文将围绕TodoView.ets,讲解如何使用 ArkUI 的List+ForEach渲染待办列表、swipeAction实现滑动删除、CustomImageToggle处理完成状态,以及空状态、计时器、诗词阅读等辅助功能的集成。
1. TodoView 组件概览
@Componentexportstruct TodoView{privatepageContext:PageContext=AppStorage.get('pageContext')asPageContext;@StorageProp('GlobalInfoModel')globalInfoModel:GlobalInfoModel=AppStorage.get('GlobalInfoModel')!;@Statearr:TodoItem[]=[]privatetodoDb:TodoDatabase=newTodoDatabase(getContext());@StateisTodoEmpty:boolean=false// ... 其他状态}核心状态:
@State arr: TodoItem[]:待办事项数组,驱动列表渲染。@StorageProp('GlobalInfoModel'):全局设备信息,获取状态栏高度等参数。todoDb:数据库实例,负责 CRUD 操作。isTodoEmpty:控制空状态与列表状态的切换。
2. 初始化:数据库与示例数据
asyncaboutToAppear():Promise<void>{awaitthis.todoDb.initialize();letisFirst=PreferencesUtil.getBooleanSync('isFirst',true)if(isFirst){this.initDemoTodoList();}else{this.arr=awaitthis.todoDb.getAllTodos();}this.getPoem()font.registerFont({familyName:'kaiti',familySrc:$rawfile('font/kaiti.ttf')})}初始化流程:
- 初始化数据库——
await this.todoDb.initialize()。 - 判断首次启动——通过
PreferencesUtil读取标记。 - 首次启动:调用
initDemoTodoList()添加示例数据。 - 非首次启动:从数据库加载所有待办事项。
- 注册楷体字体——
font.registerFont注册自定义字体,供诗词显示使用。
2.1 示例数据
privateinitDemoTodoList(){this.arr.push({id:1,time:'08:00',content:'(示例)晨读',isCompleted:true,date:'',dayOfWeek:''});this.arr.push({id:2,time:'09:00',content:'(示例)吃早饭',isCompleted:false,date:'',dayOfWeek:''});this.arr.push({id:3,time:'10:00',content:'(示例)语文作业',isCompleted:false,date:'',dayOfWeek:''});}首次用户看到三条示例待办,帮助他们快速理解应用功能。
3. List + ForEach 渲染待办列表
3.1 空状态与列表状态切换
if(this.isTodoEmpty){this.buildEmptyList()}else{this.buildTodoList()}3.2 空状态展示
@BuilderbuildEmptyList(){Column({space:10}){Text('当前暂无待办事项').fontColor($r('app.color.color_sub_text')).fontSize(12)Button('添加').height(36).fontSize(14).backgroundColor($r('app.color.app_primary')).fontColor($r('app.color.color_card')).borderRadius(25).onClick(()=>{this.isShowTask=true;});}.backgroundColor($r('app.color.color_card')).layoutWeight(1).justifyContent(FlexAlign.Center).width('100%').margin({bottom:40}).borderRadius(12)}当列表为空时,显示提示文字和「添加」按钮,引导用户创建第一条待办。
3.3 列表渲染
@BuilderbuildTodoList(){List({space:12,scroller:this.scrollerForList}){ForEach(this.arr,(item:TodoItem,index:number)=>{ListItem(){Row(){Column(){Text(item.content).layoutWeight(1).fontWeight(FontWeight.Bold)Row({space:6}){Image($r('app.media.ic_time_icon')).width(18);Text(item.time).fontColor($r('app.color.color_text_light'));};}.layoutWeight(1).alignItems(HorizontalAlign.Start);CustomImageToggle({isOn:item.isCompleted,onToggleChange:(value)=>{// 完成状态切换逻辑}});}.width('100%').height('100%');}.swipeAction({end:{builder:()=>{this.itemEnd(index);},actionAreaDistance:40,}}).height(80).padding({left:12,top:5,bottom:5,right:12}).borderRadius(12).backgroundColor($r('app.color.color_card'));},(item:TodoItem)=>item.id?.toString());}.listDirection(Axis.Vertical).scrollBar(BarState.Off).friction(0.6).margin({bottom:40}).width('100%')}关键组件解析:
List:可滚动列表容器,space: 12设置列表项间距。ForEach:渲染列表数据,第三个参数是键值生成函数(item) => item.id?.toString(),用于高效 diff 更新。ListItem:列表项容器,支持swipeAction滑动操作。scrollerForList:Scroller控制器,支持编程式滚动。
ForEach 键值:必须为每个列表项提供唯一键值,否则可能导致列表更新异常。本项目使用
item.id?.toString()作为键值。
4. swipeAction:滑动删除
4.1 滑动操作配置
.swipeAction({end:{builder:()=>{this.itemEnd(index);},actionAreaDistance:40,}})end:向左滑动后显示的操作区域(尾部)。builder:滑动操作区域的 UI 构建器。actionAreaDistance: 40:滑动操作区域的宽度。
4.2 删除按钮构建
@BuilderitemEnd(index:number){Row(){Button('删除').borderRadius(5).backgroundColor($r('app.color.app_primary')).onClick(async()=>{if(!this.arr[index].date){this.arr.splice(index,1)}else{this.todoDb.deleteTodo(this.arr[index].id??0).then((isDelete:boolean)=>{if(isDelete){this.arr.splice(index,1)}})}if(this.arr.length===0){this.isTodoEmpty=true}})}.margin({left:7})}删除逻辑说明:
- 判断数据来源:如果
item.date为空,说明是示例数据(未入库),直接从数组移除。 - 数据库中的数据:先调用
todoDb.deleteTodo()删除,成功后再从数组移除。 - 空列表检测:删除后如果数组为空,切换到空状态视图。
5. CustomImageToggle:完成状态切换
CustomImageToggle({isOn:item.isCompleted,onToggleChange:(value)=>{letcurrentBonus=PreferencesUtil.getNumberSync(AgentConstant.SP_BONUS,0)if(!value&¤tBonus===0){item.isCompleted=true;return;}item.isCompleted=value;PreferencesUtil.putSync(AgentConstant.SP_BONUS,value?currentBonus+1:currentBonus-1)this.todoDb.updateTodo(item.id!!,item)this.finishVisible=this.arr.every(item=>item.isCompleted===true);}});完成状态切换涉及积分(bonus)系统:
- 获取当前积分:从
PreferencesUtil读取SP_BONUS。 - 取消完成的保护逻辑:如果积分为 0,不允许取消完成状态(防止积分变为负数)。
- 更新积分:完成 +1,取消完成 -1,通过
PreferencesUtil.putSync持久化。 - 更新数据库:调用
todoDb.updateTodo()保存完成状态。 - 全完成检测:如果所有待办都已完成,显示庆祝弹窗(
finishVisible = true)。
6. 添加待办:半模态弹窗
.bindSheet($$this.isShowTask,this.todoAddBuilder(),{blurStyle:BlurStyle.Thick,dragBar:true,title:this.DialogTitle('今日任务','添加',async()=>{if(StrUtil.isEmpty(this.value)){ToastUtil.showToast("任务内容不能为空")return}if(StrUtil.isEmpty(this.startTime)){ToastUtil.showToast("开始时间不能为空")return}if(StrUtil.isEmpty(this.stopTime)){ToastUtil.showToast("结束时间不能为空")return}constnewTodo:TodoItem={date:DateUtil.getTodayStr('yyyy-MM-dd'),dayOfWeek:'周'+DateUtil.getWeekDay(DateUtil.getTodayStr('HH-mm-ss')),time:`${this.startTime}-${this.stopTime}`,content:this.value,isCompleted:false};consttodoId=awaitthis.todoDb.addTodo(newTodo);PreferencesUtil.putSync('isFirst',false);this.arr=[]this.arr=awaitthis.todoDb.getAllTodos();this.startTime=this.stopTime=''this.isShowTask=falsethis.isTodoEmpty=false}),height:this.sheetHeight,keyboardAvoidMode:SheetKeyboardAvoidMode.TRANSLATE_AND_RESIZE})**半模态(bindSheet)**关键点:
$$this.isShowTask:双向绑定显示状态,$$语法允许框架内部修改该值(如用户下滑关闭)。blurStyle: BlurStyle.Thick:背景毛玻璃效果。dragBar: true:显示拖拽条,用户可下滑关闭。keyboardAvoidMode:键盘弹出时的避让模式。
添加待办的表单包含任务名称、开始时间和结束时间,通过IBestField和IBestCell组件实现输入和时间选择。
6.1 刷新数据的方式
this.arr=[]this.arr=awaitthis.todoDb.getAllTodos();先清空数组再重新赋值,确保@State能检测到变化并触发 UI 刷新。直接使用this.arr = await ...赋值也是可行的,但清空后赋值的模式在某些场景下更可靠。
7. 功能入口:IconText 组件
Flex({justifyContent:FlexAlign.SpaceBetween}){IconText({icon:$r('app.media.ic_func_stroke'),txt:'每日诗词',fontColor:$r('app.color.color_function_txt_green'),bgColor:$r('app.color.color_function_green'),onFuncClick:async()=>{this.userId=UserInfoManager.getUserInfo()?.unionID??''if(StrUtil.isBlank(this.userId)){this.login()return}awaitthis.getPoem();if(this.poemInfo){this.pageContext.openPage({param:this.poemInfo,routerName:'PoemPage',onReturn:async(data)=>{this.getPoem()}},true);}}}).width('45%')IconText({icon:$r('app.media.ic_func_speak'),txt:'练字字帖',fontColor:$r('app.color.color_function_txt_purple'),bgColor:$r('app.color.color_function_blue'),onFuncClick:()=>{this.pageContext.openPage({routerName:'CopyPage'},true);}}).width('45%')}.width('100%')IconText是项目封装的图标+文字入口组件,每个功能入口有独立的配色方案。点击「每日诗词」时需要登录验证,而「练字字帖」则直接跳转。
8. TimerComponent:番茄钟计时器
TimerComponent({totalTime:this.settingTime,onTimeUp:this.onTimerFinished,onTimeChange:this.onTimeChanged,controller:this.timerController}).onClick(()=>{showDialogTime({titleBarAttribute:{titleText:"请选择计时时间"},timeAttribute:{timeType:TimeDialogType.HMS,startHours:0,startMinutes:0,startSeconds:0,selectTime:"00-05-00",endHours:23,endMinutes:59,endSeconds:59,},timeConfirmClick:(date)=>{this.settingTime=(Number(date.getHours())||0)*3600+(Number(date.getMinutes())||0)*60+(Number(date.getSeconds())||0)},})})计时器默认 25 分钟(番茄钟),点击后弹出时间选择对话框,用户可自定义时长。计时结束时触发onTimerFinished播放提醒铃声。
9. 诗词阅读与字体注册
9.1 楷体字体注册
font.registerFont({familyName:'kaiti',familySrc:$rawfile('font/kaiti.ttf')})通过font.registerFont注册自定义字体文件,注册后可在Text组件中使用.fontFamily('kaiti')指定字体。
9.2 诗词显示与朗读
Column({space:12}){Text(this.poem).fontFamily('kaiti').fontWeight(FontWeight.Bold).textAlign(TextAlign.Center).lineSpacing(LengthMetrics.fp(18))}.mainCardStyle().onClick(()=>{PoemReader.play(this.poem);}).fontFamily('kaiti'):使用注册的楷体字体,营造传统文化氛围。PoemReader.play:点击诗词卡片触发朗读,基于 TTS(文本转语音)能力。
10. Scroll 容器配置
Scroll(this.scroller){Column({space:10}){// 标题行// 倒计时 + 诗词// 功能入口// 待办列表}}.height('100%').width('100%').friction(0.6).align(Alignment.Top).scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring)| 属性 | 说明 |
|---|---|
.friction(0.6) | 滚动摩擦系数,小于 1 更灵敏 |
.scrollBar(BarState.Off) | 隐藏滚动条 |
.edgeEffect(EdgeEffect.Spring) | 边界回弹效果,提升手感 |
11. 总结
| 知识点 | 说明 |
|---|---|
List+ForEach | 可滚动列表渲染,ForEach 需提供唯一键值 |
swipeAction | 滑动操作,end配置左滑删除 |
CustomImageToggle | 自定义开关组件,处理完成状态与积分联动 |
bindSheet | 半模态弹窗,$$双向绑定显示状态 |
PreferencesUtil | 轻量偏好存储,保存积分和首次启动标记 |
font.registerFont | 注册自定义字体,$rawfile引用 rawfile 资源 |
TimerComponent | 番茄钟计时器,时间选择对话框集成 |
PoemReader.play | TTS 诗词朗读 |
IconText | 功能入口组件,带图标、文字和配色 |
Scroll+EdgeEffect.Spring | 弹性滚动效果 |
| 空状态切换 | 条件渲染:空列表 vs 数据列表 |
| 数据刷新模式 | 清空数组 → 重新查询赋值,确保 @State 触发更新 |
待办列表看似是一个简单的 CRUD 页面,实则涉及列表渲染、手势交互、状态管理、数据持久化、自定义字体、计时器、TTS 朗读等多个技术点的综合运用。理解这些模式的组合方式,是构建复杂交互页面的基础。