当前位置: 首页 > news >正文

HarmonyOS《柚兔学伴》项目实战09-待办列表 UI——List+ForEach+滑动删除

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')})}

初始化流程:

  1. 初始化数据库——await this.todoDb.initialize()
  2. 判断首次启动——通过PreferencesUtil读取标记。
  3. 首次启动:调用initDemoTodoList()添加示例数据。
  4. 非首次启动:从数据库加载所有待办事项。
  5. 注册楷体字体——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滑动操作。
  • scrollerForListScroller控制器,支持编程式滚动。

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})}

删除逻辑说明:

  1. 判断数据来源:如果item.date为空,说明是示例数据(未入库),直接从数组移除。
  2. 数据库中的数据:先调用todoDb.deleteTodo()删除,成功后再从数组移除。
  3. 空列表检测:删除后如果数组为空,切换到空状态视图。

5. CustomImageToggle:完成状态切换

CustomImageToggle({isOn:item.isCompleted,onToggleChange:(value)=>{letcurrentBonus=PreferencesUtil.getNumberSync(AgentConstant.SP_BONUS,0)if(!value&&currentBonus===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)系统:

  1. 获取当前积分:从PreferencesUtil读取SP_BONUS
  2. 取消完成的保护逻辑:如果积分为 0,不允许取消完成状态(防止积分变为负数)。
  3. 更新积分:完成 +1,取消完成 -1,通过PreferencesUtil.putSync持久化。
  4. 更新数据库:调用todoDb.updateTodo()保存完成状态。
  5. 全完成检测:如果所有待办都已完成,显示庆祝弹窗(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:键盘弹出时的避让模式。

添加待办的表单包含任务名称、开始时间和结束时间,通过IBestFieldIBestCell组件实现输入和时间选择。

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.playTTS 诗词朗读
IconText功能入口组件,带图标、文字和配色
Scroll+EdgeEffect.Spring弹性滚动效果
空状态切换条件渲染:空列表 vs 数据列表
数据刷新模式清空数组 → 重新查询赋值,确保 @State 触发更新

待办列表看似是一个简单的 CRUD 页面,实则涉及列表渲染、手势交互、状态管理、数据持久化、自定义字体、计时器、TTS 朗读等多个技术点的综合运用。理解这些模式的组合方式,是构建复杂交互页面的基础。

http://www.jsqmd.com/news/1183693/

相关文章:

  • 微信投票到底该怎么做?2026三大平台对比+人人微投票保姆级图文教程
  • Octotree 8.3.0 与 Gitako 4.9.0 深度对比:3项核心功能与2种代码浏览体验
  • HTSeq 0.5.4p1源码包:含htseq-count、质控脚本与GTF解析功能的Python生物信息工具
  • 全球首发!五一视界定制物理AI卫星ECS-1剑指万亿赛道
  • 积家全国售后服务中心专业手表维修保养权威公示(2026年7月最新) - 积家官方售后服务中心
  • ODI备案为什么会被卡?广州企业最常见的几个卡点 - 欢欢在创业
  • 2026年职称论文写作加速器:AI工具如何帮你高效完成职称论文
  • 2026年 实木板材供货商实力解析:生态、免漆、直拼、环保板材采购决策指南 - 甄选服务推荐
  • 雅典中国官方售后服务中心维修地址及服务热线实地考察报告多信源验证(2026年7月最新) - 亨得利官方服务中心
  • VC++高效处理CSV文件实战:编码解析、大文件流式读写与性能优化
  • 【小程序毕业设计】SpringBoot 架构下的高校校车排班与订座系统的设计与实现 基于移动端的高校校车出行预订服务系统(源码+文档+远程调试,全bao定制等)
  • DeepSeek提示词效果暴跌预警:当LLM置信度低于0.62时,必须启用的4层动态重写机制
  • 免费解锁WeMod专业版:Wand-Enhancer终极本地增强指南
  • SAP QM 检验点(Inspection Point) 与 PP 集成:03/04类型对比及5个生产质检场景解析
  • AI Agent 系统架构:Planner、Executor、Memory、Tools 如何协作
  • 办ODI备案,企业大概要准备哪些材料?广州老板提前心里有数 - 欢欢在创业
  • 明末渊虚之羽下载2026最新免费+修改器
  • Tomcat 9 JDWP 远程调试原理解析:Attach 与 Listen 模式 2 种场景选择指南
  • 亲身探访上海宝玑官方售后服务中心|全新官方服务电话与地址(2026年7月最新) - 亨得利钟表维修中心
  • 江诗丹顿官方服务项目及价格查询|热线与地址权威信息通知(2026年7月最新) - 江诗丹顿官方服务中心
  • Perplexity vs ChatGPT vs Claude:实测127组复杂查询任务,谁才是真正可靠的“事实型AI助手”?
  • YARA规则性能优化:3种字符串策略对比与10万文件扫描实测
  • openEuler可重复构建度量看板使用指南:实时监控构建一致性
  • C++观察者模式实战:构建松耦合事件驱动系统的核心技巧
  • Anaconda3 2024.10 安装与配置:Ubuntu 22.04 虚拟机 5 步避坑指南
  • 好酷AI制作漫剧软件
  • SpringAI AlibabaRAGMilvus 笔记(一):课程导读与阿里云百炼云模型调试
  • 听懂CPython隐藏交响乐:内存、字节码、GIL与C扩展四重优化
  • 模型记忆泄露、提示注入、代理链投毒——AI Agent三大隐匿攻击面全拆解,附MITRE ATLAS映射表
  • 2026年论文格式排版自动化:毕业之家ai一键排版vs手动排版效率对比