鸿蒙实战:FormCardsPreview 桌面卡片预览与 EmptyPage 空状态设计
2026/7/12 5:08:08 网站建设 项目流程

鸿蒙实战:FormCardsPreview 桌面卡片预览与 EmptyPage 空状态设计

前言

图:鸿蒙实战第98篇:FormCardsPreview 桌面卡片预览与 EmptyPage 空状态设计 运行效果截图(HarmonyOS NEXT)

本篇覆盖两个在业务链路外但对用户体验非常重要的页面:FormCardsPreview(桌面 Widget 三种尺寸预览)和EmptyPage(空状态页面)。这两个页面都大量使用 Canvas 绘制手绘风格插图,且遵循完全不同的设计目标——前者展示功能,后者营造情绪。

重点拆解:

  1. Canvas 六层绘制系统(EmptyPage 的叠纸 + 羽毛笔插画)
  2. 三种卡片尺寸规范(2×2 / 2×4 / 4×4 HarmonyOS Widget)
  3. drawMiniTrend迷你折线图(110×65 Canvas + 渐变填充)
  4. 三卡片交错入场动画

系列导航:97 UnclassifiedArchivePage · 99 设计模式回顾

相关文档:Canvas API · FormExtensionAbility · Button API


一、EmptyPage Canvas 六层插画

1.1 绘制层次全景

第1层:底部柔光椭圆阴影
ellipse(90,148,68,6) rgba#EAE3D7 0.6

第2层:后面一张纸 (-8°)
roundRect 虚线信纸 setLineDash([3,4])

第3层:前面一张纸 (+6°)
roundRect 三道波浪线 棕/暖粉/灰绿

第4层:右上角落叶
bezierCurveTo 灰绿 rgba 0.75

第5层:斜放羽毛笔 (35°)
roundRect笔杆 + 三角笔尖 + quadraticCurveTo羽毛

第6层:三色装饰气泡
arc 棕/灰绿/玫瑰 三小圆

1.2 纸张倾斜 save/restore 模式

每一层纸张的旋转都用save/translate/rotate/translate/restore五步标准模式:

// 后面一张纸(-8°旋转)ctx.save()ctx.translate(90,80)// ① 移到旋转中心ctx.rotate(-8*Math.PI/180)// ② 旋转 -8°ctx.translate(-90,-80)// ③ 移回坐标原点ctx.beginPath()ctx.roundRect(36,34,100,100,8)ctx.fillStyle='#FFFCF7'ctx.fill()ctx.strokeStyle='#D9CDB7'ctx.lineWidth=1.5ctx.stroke()// 虚线信纸横线ctx.setLineDash([3,4])// ← 虚线:3px线段 + 4px间隙ctx.strokeStyle='#EAE3D7'ctx.beginPath()for(letyof[58,74,90]){ctx.moveTo(50,y)ctx.lineTo(112,y)ctx.stroke()}ctx.restore()// ④ 恢复变换矩阵(必须)

1.3 羽毛笔三子图形

羽毛笔由三个子图形组成:

ctx.save()ctx.translate(130,110)ctx.rotate(35*Math.PI/180)// 35° 斜放// 子图形①:笔杆(圆角矩形)ctx.beginPath()ctx.roundRect(-3,-28,6,56,3)ctx.fillStyle='#A8907A'ctx.fill()// 子图形②:笔尖(等腰三角形)ctx.beginPath()ctx.moveTo(-3,28);ctx.lineTo(3,28);ctx.lineTo(0,38)ctx.closePath()ctx.fillStyle='#3D352E'// 深棕:墨水色ctx.fill()// 子图形③:羽毛(双段二次贝塞尔轮廓)ctx.beginPath()ctx.moveTo(3,-26)ctx.quadraticCurveTo(20,-32,27,-46)// 外弧ctx.quadraticCurveTo(21,-18,3,-14)// 内弧ctx.closePath()ctx.fillStyle='rgba(217,166,137,0.85)'ctx.fill()ctx.restore()
子图形API颜色特点
笔杆roundRect#A8907A暖棕圆角 3px,整体感
笔尖三角形路径#3D352E深棕暗示墨水/笔尖
羽毛双段贝塞尔暖棕 85% 透明略透明,轻盈感

二、三种 Widget 卡片规范

2.1 HarmonyOS Widget 尺寸标准

尺寸网格像素(参考)信息密度场景
2×22 列 × 2 行约 180×180最低单一核心数据
2×42 列 × 4 行约 180×380趋势 + 数值
4×44 列 × 4 行约 380×380完整雷达 + 详情

2.2 2×2 小卡:今日情绪

// 2×2 卡片 1:今日情绪能量Stack(){// 背景装饰圆(渐变,右上角)Column().width(60).height(60).borderRadius(30).linearGradient({angle:0,colors:[['rgba(217,166,137,0.3)',0],['transparent',0.7]]}).position({x:'60%',y:0})// 内容Column({space:4}){Text('今日情绪').fontSize(11).fontColor(AppColors.TEXT_2)Text('62').fontSize(36).fontFamily(AppFonts.SERIF).fontWeight(AppFonts.W_MEDIUM).fontColor(AppColors.TEXT)Text('💛 温和偏暖').fontSize(10).fontColor(AppColors.WARM)Button('📷 拍一张').type(ButtonType.Capsule).fontSize(10).height(24).onClick(()=>router.pushUrl({url:'pages/CapturePage'}))}.alignItems(HorizontalAlign.Start).padding(14)}

2.3 2×4 中卡:7 天趋势

Row(){// 左侧:数字区Column({space:6}){Text('近 7 天情绪').fontSize(11).fontColor(AppColors.TEXT_2)Text('在变得舒展').fontSize(13).fontColor(AppColors.TEXT)Text('62/100').fontSize(12).fontColor(AppColors.TEXT_2)// 进度条Column().height(4).width('62%').backgroundColor(AppColors.PRIMARY).borderRadius(2)}.layoutWeight(1).alignItems(HorizontalAlign.Start)// 右侧:迷你折线图Column(){Text('趋势 ↗').fontSize(10).fontColor(AppColors.QUIET)Canvas(this.trendCtx).width(110).height(65).onReady(()=>this.drawMiniTrend())}}

2.4 drawMiniTrend:迷你趋势图(110×65)

privatedrawMiniTrend():void{constctx=this.trendCtxconstw=110,h=65constdata=this.mockTrendData// 7 个数据点// ① 渐变填充区域constgradient=ctx.createLinearGradient(0,0,0,h)gradient.addColorStop(0,'rgba(168,144,122,0.3)')gradient.addColorStop(1,'rgba(168,144,122,0)')ctx.fillStyle=gradient ctx.beginPath()ctx.moveTo(0,h)data.forEach((val,i)=>{constx=(i/(data.length-1))*wconsty=h-(val/100)*(h-10)-5if(i===0)ctx.lineTo(x,y)elsectx.lineTo(x,y)})ctx.lineTo(w,h)ctx.closePath()ctx.fill()// ② 折线ctx.strokeStyle='#A8907A'ctx.lineWidth=1.5ctx.beginPath()data.forEach((val,i)=>{constx=(i/(data.length-1))*wconsty=h-(val/100)*(h-10)-5if(i===0)ctx.moveTo(x,y)elsectx.lineTo(x,y)})ctx.stroke()// ③ 最后一个端点高亮constlastX=wconstlastY=h-(data[data.length-1]/100)*(h-10)-5ctx.fillStyle='#A8907A'ctx.beginPath()ctx.arc(lastX,lastY,3,0,Math.PI*2)ctx.fill()}

三、三卡片交错入场动画

privatestartEntranceAnimation():void{// 2×2 两张卡片:每 150ms 交错for(leti=0;i<2;i++){setTimeout(()=>{animateTo({duration:500,curve:Curve.FastOutSlowIn},()=>{this.card2x2Opacity[i]=1this.card2x2Offset[i]=0})},i*150)}// 2×4 中卡:400ms 后入场setTimeout(()=>{animateTo({duration:500,curve:Curve.FastOutSlowIn},()=>{this.card2x4Opacity=1this.card2x4Offset=0})},400)// 4×4 大卡:600ms 后入场setTimeout(()=>{animateTo({duration:600,curve:Curve.FastOutSlowIn},()=>{this.card4x4Opacity=1this.card4x4Offset=0})},600)}
卡片入场延迟特点
2×2(第一张)0ms最先出现,吸引注意
2×2(第二张)150ms错开 150ms
2×4400ms中等延迟,叙事节奏
4×4600ms压轴登场,大尺寸

四、FormCardsPreview vs 真实 Widget

重要说明FormCardsPreview是一个纯预览页,用于开发阶段预览卡片设计。真实的桌面 Widget(负一屏)需要通过以下额外配置实现:

  1. FormExtensionAbility:实现onUpdateForm生命周期,提供卡片数据
  2. form_config.json:声明卡片名称、尺寸、刷新间隔
  3. module.json5中注册extensionAbilities
  4. 卡片.ets文件:使用@Entry @Form struct标记(而非@Entry @Component

详见官方文档:HarmonyOS Widget 开发指南


九、注意事项与常见问题

9.1 开发注意事项

在实际开发过程中,需特别注意以下几点:

  • API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
  • 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
  • 生命周期:合理使用aboutToAppear()aboutToDisappear()管理资源初始化与释放
  • 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用@State/@Prop/@Link装饰器

9.2 常见错误与解决方案

常见问题快速排查表:

问题类型排查方向参考方法
应用崩溃查看 hilog 错误日志hilog.error(TAG, "...", e.message)
状态丢失检查 AppStorage 键名拼写统一使用常量管理键名
动画不流畅避免在 animateTo 回调中执行 I/O动画与数据操作分离

十、最佳实践与性能优化

10.1 代码组织规范

以下规范可显著提升代码可读性与可维护性:

  • 分层解耦:UI 逻辑(pages/)与数据操作(DAO/Service)严格分离,不在 build() 中调用数据库
  • 组件拆分:单个@Component保持 100~200 行以内,大型组件拆分为@Builder子函数
  • 令牌约束:颜色、字号、间距统一引用AppColors/AppFonts/AppAnimations,禁止魔数
  • 错误处理:异步方法统一返回Promise<T | null>,异常时hilog.error记录并返回null

10.2 性能优化要点

以下是常见性能优化措施的对比分析:

优化维度优化前优化后效果
列表渲染ForEach全量渲染LazyForEach按需渲染内存降低 40-60%
图片加载不指定尺寸指定width/height减少布局重计算
数据库查询每次重新查询合理缓存 + 精确 Predicates查询速度提升 5-10x
动画实现逐帧手动绘制animateTo属性动画稳定 60fps
状态更新全局刷新@State精细化作用域减少无效渲染

总结

本篇涵盖了两个视觉设计重点页面的技术实现:

  1. 六层 Canvas 插画:save/rotate/restore 矩阵变换,零图片资源实现叠纸+羽毛笔插画
  2. 三种 Widget 尺寸:2×2/2×4/4×4 信息密度递增,符合 HarmonyOS Widget 设计规范
  3. drawMiniTrend迷你图:110×65 Canvas 中渐变填充 + 折线 + 端点,空间极致利用
  4. 压轴交错入场:四张卡片 0/150/400/600ms 错开,形成"依次登场"的视觉叙事
  5. FormCardsPreview定位:开发调试用预览页,真实 Widget 需配合FormExtensionAbility

下一篇预告:第99篇 页面汇总与 7 大共享设计模式回顾

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

  • Canvas CanvasRenderingContext2D
  • HarmonyOS Widget 桌面卡片开发指南
  • FormExtensionAbility API
  • animateTo 显式动画
  • 第44篇 Canvas 组件基础
  • 第50篇 Canvas 动画与绘制进度驱动
  • 第89篇 LoginPage Canvas 插画
  • 第59篇 入场动画分段式设计

七、桌面卡片与空状态的设计精要

7.1 Widget 三种尺寸的差异化设计

2×2(小): 只显示最新情绪分数 + 今日推荐语 2×4(中): 情绪分数 + 最近3条记录列表 4×4(大): 完整的情绪雷达图 + 今日状态 + 快捷入口

三个尺寸遵循渐进信息展示原则:小→大,信息量递增,核心数据始终可见。

7.2 FormExtensionAbility 与主应用数据共享

Widget 数据来自数据库,但 FormExtensionAbility 运行在独立进程:

// 方案:通过 SharedPreferences 共享轻量数据// 主 App 写入constprefs=awaitpreferences.getPreferences(context,'widget_data');awaitprefs.put('latest_score',latestScore);awaitprefs.put('personality_type',personalityType);awaitprefs.flush();// FormExtensionAbility 读取constprefs=awaitpreferences.getPreferences(formContext,'widget_data');constscore=prefs.getSync('latest_score',0)asnumber;

7.3 EmptyPage 的情绪化设计

EmptyPage 不只是「没有数据」的提示,而是一个温暖的引导页:

场景图片文案风格
首次使用Canvas 绘制的小鹿插画「从这里开始,记录你的笔迹故事」
搜索无结果放大镜+星星「没找到,换个关键词试试?」
档案为空空信封插画「还没有记录,拍一张开始吧~」
网络断开失联的WiFi信号「好像断网了,检查一下网络吧」

每种场景的配图风格统一(手绘线条 + 米色调),确保空状态页面「在其位,不失格」。


八、FormCards 与 EmptyPage 技术进阶

8.1 桌面卡片数据刷新机制

FormExtensionAbility 通过FormProvider.updateForm主动推送数据:

import{formProvider,formBindingData}from'@kit.FormKit';// 在 UIAbility 中触发卡片更新asyncrefreshWidget(formId:string):Promise<void>{constreport=awaitReportDao.getLatest();constdata=formBindingData.createFormBindingData({dominantTrait:report.dominantTrait,matchScore:report.matchScore,emotionLabel:report.emotionTags[0]??'平静'});awaitformProvider.updateForm(formId,data);}

8.2 三种卡片尺寸适配

卡片尺寸格子数展示内容
1×2(小)2主人格图标 + 得分
2×2(中)4人格 + 情绪标签 + 日期
2×4(大)8完整摘要 + 匹配度圆环
// 根据卡片尺寸动态渲染@BuilderrenderByDimension(dimension:formInfo.FormDimension){if(dimension===formInfo.FormDimension.Dimension_1_2){this.SmallCard()}elseif(dimension===formInfo.FormDimension.Dimension_2_2){this.MediumCard()}else{this.LargeCard()}}

8.3 EmptyState 动画增强

空状态插画配合入场动画效果更佳:

@StateillustrationScale:number=0.8;@StateillustrationOpacity:number=0;aboutToAppear(){animateTo({duration:600,curve:Curve.EaseOut},()=>{this.illustrationScale=1.0;this.illustrationOpacity=1;});}

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询