鸿蒙实战:FormCardsPreview 桌面卡片预览与 EmptyPage 空状态设计
鸿蒙实战FormCardsPreview 桌面卡片预览与 EmptyPage 空状态设计前言图鸿蒙实战第98篇FormCardsPreview 桌面卡片预览与 EmptyPage 空状态设计 运行效果截图HarmonyOS NEXT本篇覆盖两个在业务链路外但对用户体验非常重要的页面FormCardsPreview桌面 Widget 三种尺寸预览和EmptyPage空状态页面。这两个页面都大量使用 Canvas 绘制手绘风格插图且遵循完全不同的设计目标——前者展示功能后者营造情绪。重点拆解Canvas 六层绘制系统EmptyPage 的叠纸 羽毛笔插画三种卡片尺寸规范2×2 / 2×4 / 4×4 HarmonyOS WidgetdrawMiniTrend迷你折线图110×65 Canvas 渐变填充三卡片交错入场动画系列导航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#FFFCF7ctx.fill()ctx.strokeStyle#D9CDB7ctx.lineWidth1.5ctx.stroke()// 虚线信纸横线ctx.setLineDash([3,4])// ← 虚线3px线段 4px间隙ctx.strokeStyle#EAE3D7ctx.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#A8907Actx.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.fillStylergba(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×65privatedrawMiniTrend():void{constctxthis.trendCtxconstw110,h65constdatathis.mockTrendData// 7 个数据点// ① 渐变填充区域constgradientctx.createLinearGradient(0,0,0,h)gradient.addColorStop(0,rgba(168,144,122,0.3))gradient.addColorStop(1,rgba(168,144,122,0))ctx.fillStylegradient ctx.beginPath()ctx.moveTo(0,h)data.forEach((val,i){constx(i/(data.length-1))*wconstyh-(val/100)*(h-10)-5if(i0)ctx.lineTo(x,y)elsectx.lineTo(x,y)})ctx.lineTo(w,h)ctx.closePath()ctx.fill()// ② 折线ctx.strokeStyle#A8907Actx.lineWidth1.5ctx.beginPath()data.forEach((val,i){constx(i/(data.length-1))*wconstyh-(val/100)*(h-10)-5if(i0)ctx.moveTo(x,y)elsectx.lineTo(x,y)})ctx.stroke()// ③ 最后一个端点高亮constlastXwconstlastYh-(data[data.length-1]/100)*(h-10)-5ctx.fillStyle#A8907Actx.beginPath()ctx.arc(lastX,lastY,3,0,Math.PI*2)ctx.fill()}三、三卡片交错入场动画privatestartEntranceAnimation():void{// 2×2 两张卡片每 150ms 交错for(leti0;i2;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.card2x4Opacity1this.card2x4Offset0})},400)// 4×4 大卡600ms 后入场setTimeout((){animateTo({duration:600,curve:Curve.FastOutSlowIn},(){this.card4x4Opacity1this.card4x4Offset0})},600)}卡片入场延迟特点2×2第一张0ms最先出现吸引注意2×2第二张150ms错开 150ms2×4400ms中等延迟叙事节奏4×4600ms压轴登场大尺寸四、FormCardsPreview vs 真实 Widget重要说明FormCardsPreview是一个纯预览页用于开发阶段预览卡片设计。真实的桌面 Widget负一屏需要通过以下额外配置实现FormExtensionAbility实现onUpdateForm生命周期提供卡片数据form_config.json声明卡片名称、尺寸、刷新间隔module.json5中注册extensionAbilities卡片.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禁止魔数错误处理异步方法统一返回PromiseT | null异常时hilog.error记录并返回null10.2 性能优化要点以下是常见性能优化措施的对比分析优化维度优化前优化后效果列表渲染ForEach全量渲染LazyForEach按需渲染内存降低 40-60%图片加载不指定尺寸指定width/height减少布局重计算数据库查询每次重新查询合理缓存 精确 Predicates查询速度提升 5-10x动画实现逐帧手动绘制animateTo属性动画稳定 60fps状态更新全局刷新State精细化作用域减少无效渲染总结本篇涵盖了两个视觉设计重点页面的技术实现六层 Canvas 插画save/rotate/restore 矩阵变换零图片资源实现叠纸羽毛笔插画三种 Widget 尺寸2×2/2×4/4×4 信息密度递增符合 HarmonyOS Widget 设计规范drawMiniTrend迷你图110×65 Canvas 中渐变填充 折线 端点空间极致利用压轴交错入场四张卡片 0/150/400/600ms 错开形成依次登场的视觉叙事FormCardsPreview定位开发调试用预览页真实 Widget 需配合FormExtensionAbility下一篇预告第99篇 页面汇总与 7 大共享设计模式回顾如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源Canvas CanvasRenderingContext2DHarmonyOS Widget 桌面卡片开发指南FormExtensionAbility APIanimateTo 显式动画第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 写入constprefsawaitpreferences.getPreferences(context,widget_data);awaitprefs.put(latest_score,latestScore);awaitprefs.put(personality_type,personalityType);awaitprefs.flush();// FormExtensionAbility 读取constprefsawaitpreferences.getPreferences(formContext,widget_data);constscoreprefs.getSync(latest_score,0)asnumber;7.3 EmptyPage 的情绪化设计EmptyPage 不只是「没有数据」的提示而是一个温暖的引导页场景图片文案风格首次使用Canvas 绘制的小鹿插画「从这里开始记录你的笔迹故事」搜索无结果放大镜星星「没找到换个关键词试试」档案为空空信封插画「还没有记录拍一张开始吧」网络断开失联的WiFi信号「好像断网了检查一下网络吧」每种场景的配图风格统一手绘线条 米色调确保空状态页面「在其位不失格」。八、FormCards 与 EmptyPage 技术进阶8.1 桌面卡片数据刷新机制FormExtensionAbility 通过FormProvider.updateForm主动推送数据import{formProvider,formBindingData}fromkit.FormKit;// 在 UIAbility 中触发卡片更新asyncrefreshWidget(formId:string):Promisevoid{constreportawaitReportDao.getLatest();constdataformBindingData.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(dimensionformInfo.FormDimension.Dimension_1_2){this.SmallCard()}elseif(dimensionformInfo.FormDimension.Dimension_2_2){this.MediumCard()}else{this.LargeCard()}}8.3 EmptyState 动画增强空状态插画配合入场动画效果更佳StateillustrationScale:number0.8;StateillustrationOpacity:number0;aboutToAppear(){animateTo({duration:600,curve:Curve.EaseOut},(){this.illustrationScale1.0;this.illustrationOpacity1;});}如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力