鸿蒙原生应用实战:明信片制作页的实时预览卡与背景选择
App 43「校园电子明信片」制作页(Func1Tab),主题色
#00B894绿色(green),4 个 Tab 分别为首页(📮)、制作(🎨)、发送(✉️)、我的(👤)。制作页采用"Header + 预览 + 背景 + 祝福语 + 字体 + 按钮"六区布局——白色单行 Header("制作明信片"20 号加粗)+明信片实时预览卡(200 高绿色渐变 + 64 号背景 emoji + 祝福语文字实时渲染,空时显示占位"在此预览你的祝福语...")+背景选择卡(🎓🎂🌸🏖️🍁 5 个 54×54 方块,选中绿色描边 + 浅绿底)+祝福语卡(90 高浅底 TextArea"写下你的祝福...")+字体样式卡(手写体/楷体/圆体/宋体 4 等分胶囊)+全宽绿色"下一步 · 发送"按钮(Toast"前往发送")。本篇基于43-e-postcard/entry/src/main/ets/pages/Func1Tab.ets(共 104 行)逐段拆解,附 4 张实机截图。
一、整体结构:六区"Header + 预览 + 背景 + 祝福 + 字体 + 按钮"布局
制作页是"品牌头部 + 实时预览 + 四项定制 + 下一步"的六区布局:
build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.Preview() this.BgCard() this.MsgCard() this.FontCard() Button('下一步 · 发送').fontSize(15).fontColor('#FFFFFF').backgroundColor(C.primary) .width('100%').height(48).borderRadius(D.rMd) .onClick(() => { promptAction.showToast({ message: '前往发送' }); }) } .width('100%') .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width('100%').height('100%').backgroundColor(C.bg) }6 块结构:
- Header— 单行品牌头("制作明信片")
- Preview— 明信片实时预览,
@State message驱动祝福语即时渲染 - BgCard— 5 个背景 emoji 选择(
@State activeBg) - MsgCard— 祝福语 TextArea(
@State message) - FontCard— 4 种字体选择(
@State activeFont) - 下一步按钮— 全宽绿色主按钮,Toast"前往发送"
三个@State(message/activeBg/activeFont)贯穿全页——预览卡同时消费 message 与 activeBg,实现"改背景即时变预览、打字即时变预览"的所见即所得,这是制作页的核心交互模式。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、Preview:明信片实时预览
Preview() { Stack({ alignContent: Alignment.BottomStart }) { Column() { Text(this.bgs[this.activeBg]).fontSize(64) } .width('100%').height(200).borderRadius(D.rLg).justifyContent(FlexAlign.Center) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) Text(this.message.length > 0 ? this.message : '在此预览你的祝福语...') .fontSize(16).fontColor('#FFFFFF').padding(16).maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') }技术拆解:
- 背景层:绿色渐变(
#00B894 → #4BC9A8,135°)200 高圆角卡 + 64 号背景 emoji 居中——this.bgs[this.activeBg]随背景选择实时变化,选中 🎂 预览即显示 🎂。 - 祝福语层:
Stack({ alignContent: Alignment.BottomStart })把文字左下对齐叠在渐变上(明信片祝福语居下的版式)——this.message.length > 0 ? this.message : '在此预览你的祝福语...'三元切换:有内容显示真实祝福语、无内容显示灰色占位提示。 - 文字样式:16 号白色、
maxLines(2)+ 省略号防溢出(长祝福语截断)、padding(16)与边缘留距。 - 所见即所得:
@State message与@State activeBg双驱动——在祝福语框打字、在背景区选 emoji,预览卡即时更新,用户不需要"预览"按钮,所见即所得是本页最大的交互亮点。
三、BgCard:5 背景 emoji 选择
BgCard() { Column({ space: 10 }) { Text('选择背景').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') Row({ space: 10 }) { ForEach(this.bgs, (b: string, idx: number) => { Column() { Text(b).fontSize(26) } .width(54).height(54).justifyContent(FlexAlign.Center) .backgroundColor(this.activeBg === idx ? C.primarySoft : C.cardSoft).borderRadius(D.rSm) .border({ width: this.activeBg === idx ? 2 : 0, color: C.primary }) .onClick(() => { this.activeBg = idx; }) }, (b: string, idx: number) => b + idx) }.width('100%') } .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }5 个背景(15 行):['🎓', '🎂', '🌸', '🏖️', '🍁']——与首页模板的毕业季/生日快乐/春日物语/夏日海滩/秋日私语对应(去掉圣诞),制作页背景即首页模板 emoji 的复用。
技术要点:
- 选中态双重表达:选中背景浅绿底(
primarySoft)+2 宽绿色描边(border({ width: 2, color: C.primary })),未选中浅灰底无描边——底色 + 描边双重强化,比单一生效更醒目(App 42 心情卡用大小/透明度、本页用底色/描边,选中态表达手段丰富)。 - 方块尺寸:54×54 固定方块,26 号 emoji 居中,
borderRadius(D.rSm)小圆角。 - key 防冲突:
(b: string, idx: number) => b + idx——emoji 数组可能重复时用索引拼接保证唯一(系列标准写法)。 - 间距:
Row({ space: 10 })横向 10 间距,5 个方块恰好一行排布。
四、MsgCard:祝福语 TextArea
MsgCard() { Column({ space: 10 }) { Text('祝福语').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') TextArea({ placeholder: '写下你的祝福...' }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(90) .onChange((v: string) => { this.message = v; }) } .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }- 浅底圆角输入:
C.cardSoft浅灰底 + 圆角 10 + 90 高——与 App 41/42 的"透明沉浸式"不同,本页用"可见输入框"(浅底圆角),因为祝福语是"填表"性质(短文本 + 有预览联动),不需要沉浸感,明确的输入框边界反而提示"这里填祝福"。 - 占位文案:"写下你的祝福..."直接、无歧义。
- 实时联动:
onChange写入@State message,Preview 的祝福语层即时更新——打字即预览。
五、FontCard:4 字体选择
FontCard() { Column({ space: 10 }) { Text('字体样式').fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%') Row({ space: 10 }) { ForEach(this.fonts, (f: string, idx: number) => { Text(f).fontSize(13) .fontColor(this.activeFont === idx ? '#FFFFFF' : C.textSub) .textAlign(TextAlign.Center).layoutWeight(1).padding({ top: 9, bottom: 9 }) .backgroundColor(this.activeFont === idx ? C.primary : C.cardSoft).borderRadius(D.rSm) .onClick(() => { this.activeFont = idx; }) }, (f: string) => f) }.width('100%') } .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }4 种字体(16 行):['手写体', '楷体', '圆体', '宋体']——中文场景的四类字体风格。
技术要点:
- 等分胶囊:每个 Text
layoutWeight(1)四等分整行,textAlign(TextAlign.Center)居中,选中绿底白字、未选浅底灰字——与背景选择"方块"不同,字体选择是"胶囊条",两种选择器形态在同页并存(方块 = 视觉选择、胶囊 = 文本选择),选型贴合内容类型。 - Demo 简化:当前切换仅高亮、预览文字字体未实际变化——真实产品应给 Preview 的祝福语 Text 应用
fontFamily属性(手写体/楷体等),Demo 保留了选择结构、字体渲染需系统字体支持。 - key:
(f: string) => f(字体名唯一)。
六、下一步按钮
Button('下一步 · 发送').fontSize(15).fontColor('#FFFFFF').backgroundColor(C.primary) .width('100%').height(48).borderRadius(D.rMd) .onClick(() => { promptAction.showToast({ message: '前往发送' }); })- 文案"下一步 · 发送":明确"这是向导的下一步",比"完成"更引导流程——制作页与发送页构成"两步向导",按钮衔接两页。
- Toast"前往发送":Demo 占位,产品化后
router跳转发送页(发送页卡片预览展示"毕业季明信片 · 已完成制作"承接制作成果)。 - 无校验:祝福语可空(预览显示占位)——明信片允许只发背景图,设计上不强制填字。
七、跨页数据自洽:制作页承上启下
制作页是"选模板 → 制作 → 发送"链路的中间环节:
- 承接首页:首页点"制作: 毕业季"应跳转本页并预选 🎓 背景——
bgs[0] = '🎓'与首页毕业季模板 emoji 一致,数据同源。 - 衔接发送页:制作成果(背景 + 祝福语 + 字体)随"下一步 · 发送"传递到发送页——发送页卡片预览"毕业季明信片 · 已完成制作"正是制作页默认成果的展示。
- 背景与首页模板:5 个背景 emoji(🎓🎂🌸🏖️🍁)= 首页 6 模板中的 5 个 emoji(去 🎄)——制作页背景即模板的"定制层"。
- 我的页"我的作品"菜单↔ 制作页成果——制作完成的明信片保存在"我的作品"中可复用/再编辑。
八、实机截图与交互演示
本节结合 4 张实机截图,逐张还原制作页的交互链路。
1. 制作页首屏:预览卡 + 背景选择
第一张截图是制作页默认首屏:顶部"制作明信片"标题;下方 200 高绿色渐变预览卡——64 号 🎓 居中、左下角白色占位文字"在此预览你的祝福语...";再往下是"选择背景"卡——5 个 54×54 方块(🎓🎂🌸🏖️🍁),第一个 🎓 为浅绿底 + 绿色描边选中态;"祝福语"卡(90 高浅底输入框)与"字体样式"卡(手写体/楷体/圆体/宋体,第一个选中)部分可见。所见即所得的预览卡是整页视觉焦点。
2. 点击"🎂"背景:预览 + 选中切换
第二张截图是点击背景选择第二格"🎂"后的状态:预览卡中央的 emoji 从 🎓 变为 🎂(64 号即时更新),背景方块的选择态从第一格切换到第二格(浅绿底 + 绿描边)——@State activeBg驱动"预览 emoji + 选择框"双处同步更新,所见即所得的核心交互得到验证。
3. 点击"楷体":字体选中切换
第三张截图是点击"字体样式"第二格"楷体"后的状态:绿色选中态从"手写体"切换到"楷体"——@State activeFont由 0 变 1。四个字体胶囊等分整行(各约 1/4 屏宽),选中绿底白字与未选浅底灰字对比清晰。
4. 点击"下一步 · 发送":流程衔接 Toast
第四张截图是滚动到底部、点击全宽绿色"下一步 · 发送"按钮后的反馈:弹出"前往发送"Toast。按钮位于页面底部(祝福语卡与字体卡之后),"下一步"的向导语义明确——产品化后此处跳转发送页,承接制作成果。
九、扩展思考:所见即所得的编辑器设计
制作页的"预览实时联动"是编辑器类页面的核心模式,值得展开:
- 状态单一来源:
@State message/activeBg/activeFont三个状态是编辑器的"文档模型",预览卡只是它们的"渲染投影"——任何一处修改,预览自动更新,无需手动"刷新预览"按钮。这是 MVVM 思想的最小实践。 - 预览的性能:每次打字(onChange 高频触发)都重绘预览——Demo 内容轻量无压力;若未来预览含大图/动画,需防抖(300ms)合并渲染。
- 字体的真实渲染:Demo 只切换高亮,产品化后 Preview 的祝福语
fontFamily应跟随activeFont——需系统安装对应字体(手写体/楷体等),或使用内置字体资源。 - 背景的扩展:背景从 5 emoji 扩展为"渐变库 + 图片库 + 贴纸库"——
activeBg从索引升级为对象({ type: 'emoji'|'gradient'|'image', value: ... }),预览层按 type 分支渲染。 - 撤销重做:编辑器标配 Ctrl+Z 撤销——把三个 @State 收进"历史栈"(每次修改 push 快照),是编辑器产品化的进阶能力。
十、开发者视角:制作页的调试与验证技巧
- 预览与状态的一致性:改背景/打字后,用 dumpLayout 确认预览卡的 emoji 与文字确实更新——"状态变了但预览没变"通常是 onChange 未正确写入 @State(检查赋值)。
- maxLines 的预览截断:祝福语超过 2 行时预览省略——验证长文本的省略位置与"占位 → 真实文字"的切换边界(空内容显示占位、非空显示内容)。
- 背景选中态的描边:
border({ width: 2, color: C.primary })只在选中时存在——切换时描边出现/消失会引起 1-2px 的视觉跳动(方块大小变化),若介意可改为"始终透明描边 + 选中变色"。 - emoji key 的稳定性:
b + idx的 key 在静态数组下正确;若未来支持增删背景,改用 id 防重排重建。 - 键盘遮挡:点击 TextArea 弹键盘压缩可视区,"下一步 · 发送"按钮被顶出——
expandSafeArea或滚动适配(系列高频坑),真机验证键盘弹出场景。
十一、系列横向对比:编辑器类页面的四层架构
制作页是"编辑器"类页面,其架构可作为系列乃至所有编辑器产品的参考:
| 层级 | 本页实现 | 产品化演进 |
|---|---|---|
| 画布(预览) | Preview 渐变卡 | 可缩放画布/多模板 |
| 属性面板 | Bg/Font 选择卡 | 颜色/贴纸/滤镜库 |
| 输入层 | MsgCard TextArea | 富文本/表情/模板句 |
| 动作层 | 下一步按钮 | 保存草稿/导出图片/分享 |
编辑器的核心是"状态即文档":本页三个 @State 就是明信片文档的全部字段(背景/祝福/字体),预览卡是文档的渲染——任何编辑器(哪怕是 Photoshop)的本质都是"操作状态 → 渲染预览",本页用 100 行代码演示了这个最小模型。产品化时把三个 @State 收拢为@Observed class Postcard对象(含 id/模板/背景/祝福/字体/创建时间),即可支撑"草稿保存、作品列表、继续编辑"等完整功能——制作页是编辑器的最小可行示例。
与系列其他表单页的对比:App 41/42 的表单是"填完提交"(一次性),制作页是"边填边看"(实时预览)——编辑器的差别在于"预览层"的存在。若 App 41 的发布页也加一个"帖子预览卡"(所见即所得),即可升级为"编辑器型发布",这是 UGC 产品提升体验的常见路径。
十二、开发者视角:制作页的调试与验证技巧
- 预览联动验证:改背景后确认预览 emoji 更新、打字后确认预览文字更新——用 dumpLayout 检查两个状态是否同时反映;若预览不更新,第一排查
onChange是否写入 @State(检查赋值与引用)。 - 占位/真实文字的边界:
message.length > 0切换占位与真实文字——测试空、1 字、超 2 行三种情况,确认省略号与占位消失的边界正确。 - 选中描边的视觉跳动:
border({ width: 2 })选中出现/未选消失会引起 1-2px 尺寸变化——若视觉敏感,改为"始终 2 宽透明描边 + 选中变色"(尺寸恒定)。 - emoji key:
b + idx静态数组下正确;若未来支持用户自定义背景(增删数组),改用 id 防重排重建。 - 字体渲染的真实性:Demo 只切高亮未应用字体——产品化时确认系统字体资源(手写体/楷体等)是否可用,不可用则用内置字体文件,避免预览与实际不符。
十三、扩展思考:明信片制作的情感化与模板句
制作页的祝福语输入可进一步情感化:
- 模板句库:提供常用祝福句一键插入("祝你生日快乐!""毕业快乐,前程似锦!")——降低"不知写什么"的门槛,模板句是明信片产品的情感效率工具。
- 祝福语的字数体验:当前 TextArea 无字数上限——明信片讲究"短而真挚",产品化可设 100 字上限 + 计数(参考 App 42 树洞的 0/1000),引导克制表达。
- 字体与情绪的匹配:手写体适合温情(祝福)、楷体适合正式(师恩)、圆体适合可爱(朋友)、宋体适合雅致(风景)——字体选择与情感场景的匹配建议在产品中给出"场景推荐"。
- 背景与祝福的联动:选 🎂 背景时推荐生日祝福模板句、选 🎓 推荐毕业祝福——背景 emoji 已隐含场景,联动推荐降低选择成本。
- 预览的情感反馈:明信片制作完成后可加"寄出前的心动确认"(预览 + "就它了"),强化"心意已就绪"的仪式感——与发送页"已完成制作"绿色状态衔接。
十四、FAQ 与一句话总结
Q1:为什么祝福语不强制填写?明信片允许"只发背景 + emoji"的极简形式(预览占位提示而非拦截),降低发送门槛;App 41/42 的"请输入内容"拦截是针对"内容为主体"的 UGC,明信片的主体是模板+心意,填不填字是用户自由。
Q2:预览卡的字体为什么没变?Demo 的字体选择只切高亮,fontFamily未实际应用(需系统字体支持)。产品化后给预览 Text 绑定字体即可,选择结构已就绪。
Q3:背景选择为什么是方块而不是胶囊?背景是"视觉内容"(emoji 图案),方块(54×54)像"色卡/图案块"更贴合视觉选择;字体是"文本选项",胶囊(等分条)更像按钮。选择器形态贴合选项的感知类型,是本页的设计细节。
Q4:制作页和首页模板什么关系?首页是"选模板"(6 个渐变模板),制作页是"做定制"(在模板基础上换背景/写祝福/选字体)——首页点"制作"跳转本页并预选对应背景,制作成果流向发送页。三页构成"选 → 做 → 发"的完整向导。
Q5:为什么按钮叫"下一步 · 发送"而不叫"完成"?制作只是流程的第一步,后面还有发送页——"下一步"明确指示流程方向(向导语义),比"完成"更准确,引导用户继续而非结束。
一句话总结:制作页以"实时预览 + 背景选择 + 祝福语输入 + 字体选择"完成了明信片定制的最小闭环。所见即所得的预览联动(@State双驱动)、背景的"底色 + 描边"双重选中态、字体/背景两种选择器形态的差异化设计,都是编辑器类页面的标准手法——104 行代码,一个"改什么都能即时看到"的明信片制作台,也是系列中"实时预览"体验最完整的页面。
最后从验收视角确认:四张实机截图覆盖了首屏(预览卡+背景选择)、背景切换(🎂 预览即时更新)、字体切换(楷体选中)、下一步按钮(前往发送 Toast)四个状态,所见即所得的预览联动(背景 emoji 与预览同步、祝福语实时渲染)、背景"底色+描边"双重选中态、字体胶囊等分选择均验证正常。跨页数据自洽通过核对——制作页 5 背景 emoji(🎓🎂🌸🏖️🍁)与首页 6 模板中的 5 个 emoji 一致,默认背景 🎓 对应首页毕业季模板与发送页成果卡,制作成果(背景+祝福语+字体)随"下一步 · 发送"流向发送页。整页 104 行代码完成"实时预览 + 四项定制"的编辑器最小闭环,验收通过,可作为所见即所得编辑器的参考范本。
补充开发问答:Q1 预览卡为什么要放在最上方?——编辑器"先看结果再改参数"的心智模型,预览置顶让用户每次修改都即时看到效果;若参数在预览上方,改完要滚动回顶部看效果,体验割裂。Q2 背景选择的选中态为什么用描边?——描边(2 宽绿边)在浅色方块上醒目且不改变底色语义(primarySoft 浅绿底 + 绿描边双重提示),比纯底色更清晰。Q3 祝福语最多显示几行?——预览maxLines(2)两行省略,长祝福语截断但完整内容在明信片中仍完整(预览是"效果示意"非"内容截断")。Q4 下一步按钮为什么不做校验?——明信片允许只发背景(祝福可空),预览占位已提示"在此预览你的祝福语",无需强制;接收人校验在发送页负责(发送页"请填写接收人")。Q5 字体选择产品化后如何真正生效?——给预览 Text 绑定fontFamily并在资源目录放置四种字体文件(手写体/楷体/圆体/宋体),选择即切换,当前结构已预留。所见即所得的核心,就是状态与渲染的即时绑定。
最后以一句收尾:制作者的点滴定制,都在预览中即时绽放。 回望整页,从实时预览到背景/祝福/字体三项定制,从状态驱动的所见即所得到下一步的流程衔接,每个设计都在让"把心意做出来"变得简单而愉悦——这就是制作页的全部价值,也是编辑器类页面的最小范本。
每一次定制都是心意的具象化,每一张预览都是温暖的彩排。