卡片式 UI 设计——从今日学习卡到单词翻转卡
一、引言
卡片式 UI 是移动应用中最主流的视觉设计模式,它将信息以独立卡片的形式组织,在有限的屏幕空间内清晰呈现多层次的内容。一个优秀的卡片设计需要在视觉层次、交互反馈和性能之间取得平衡。
本文以英语学习 App 中的三类典型卡片为例,系统讲解 HarmonyOS 中卡片式 UI 的实现思路:从简单的「今日学习卡」到数据驱动的「学习进度卡」,再到交互复杂的「单词翻转卡」,逐步深入卡片式设计的核心技巧。
二、基础卡片:今日学习卡
2.1 卡片布局分析
首页的「今日学习卡」是最简单的卡片形态——圆角矩形背景 + 内部文本 + 进度条。它使用@Builder构建,作为主页面的一个子模块:
// features/homePage/src/main/ets/pages/MainPage.ets@BuildertodayStudyBuilder(){Column(){// 标题行:今日学习 + 坚持 7 天Row(){Text('今日学习').fontSize(16).fontWeight(FontWeight.Medium);Text('坚持 7 天').fontSize(12).fontColor($r('sys.color.font_secondary')).layoutWeight(1).textAlign(TextAlign.End);}.width('100%').margin({bottom:8});// 数据行:已学 15 词 / 目标 30 词 + 50%Row(){Text('已学 15 词 / 目标 30 词').fontSize(13).fontColor($r('sys.color.font_secondary')).layoutWeight(1);Text('50%').fontSize(20).fontWeight(FontWeight.Bold).fontColor($r('sys.color.comp_background_emphasize'));}.width('100%').margin({bottom:8});// 线性进度条Progress({value:15,total:30,type:ProgressType.Linear}).color($r('sys.color.comp_background_emphasize')).height(8).width('100%');}.padding(16).borderRadius(16).backgroundColor($r('sys.color.background_primary'));}2.2 卡片设计要点
核心样式参数:
| 参数 | 值 | 视觉效果 |
|---|---|---|
borderRadius(16) | 16vp 圆角 | 柔和边缘,增加亲和力 |
padding(16) | 16vp 内边距 | 内容和边界留白 |
backgroundColor(primary) | 白色背景 | 与浅灰底色区隔 |
视觉层次构建:
卡片的标题行使用layoutWeight(1)+textAlign(TextAlign.End)让 “坚持 7 天” 右对齐,形成"主标题 + 副标签"的视觉结构。进度数据通过字号(13 和 20)和颜色(次要色和强调色)区分主次,引导用户视线聚焦在 50% 的完成率上。
进度条组件使用ProgressType.Linear配合8vp高度,形成清晰可见的进度指示,其颜色comp_background_emphasize在主题切换时自动适配。
三、功能卡片组:ReusableFlowItem
3.1 统一卡片容器
在「练习模式」区域,四个功能卡片(单词记忆、听力训练、阅读训练、语法练习)使用统一的ReusableFlowItem组件。该组件通过stateStyles实现按压反馈:
@ComponentV2struct ReusableFlowItem{@Paramitem:PracticeView=newPracticeView($r('app.media.ic_home'),'顺序练习','1、3256');build(){Row(){Column(){Text(this.item.name).textOverflow({overflow:TextOverflow.Ellipsis}).maxLines(1).fontWeight(FontWeight.Bold).fontSize($r('sys.float.Body_S')).fontColor($r('sys.color.font_primary'));Text(this.item.describe).textOverflow({overflow:TextOverflow.Ellipsis}).maxLines(1).fontSize($r('sys.float.Caption_M')).fontColor($r('sys.color.font_secondary')).margin({top:$r('app.float.vp_2')});}.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Start).layoutWeight(1);Image(this.item.imageUri).interpolation(ImageInterpolation.High).objectFit(ImageFit.Fill).width(40).height(40).clip(true).margin({left:$r('app.float.vp_4'),right:$r('app.float.vp_4')});}.stateStyles({pressed:{.scale({x:0.97,y:0.97})}}).backgroundColor($r('sys.color.background_secondary')).justifyContent(FlexAlign.SpaceBetween).height(100).width('100%').padding({left:$r('app.float.vp_12'),right:$r('app.float.vp_16')}).borderRadius($r('app.float.vp_12'));}}3.2 按压反馈实现
stateStyles是 ArkTS 中处理交互状态的最佳方式。当用户按下卡片时,组件以动画方式缩放到 0.97 倍:
.stateStyles({pressed:{.scale({x:0.97,y:0.97})}})这种微缩放效果模拟了真实世界中按下物体的反应,提升了交互的真实感。实现时需要注意:
- 缩放值不宜过小:0.95~0.98 是合适的范围,过小会显得突兀
- 避免在缩放组件中包含复杂布局:嵌套结构可能在动画时产生锯齿
- 与点击事件配合:
stateStyles只管理视觉反馈,业务逻辑仍需写在onClick中
四、高级卡片:3D 翻转单词卡
4.1 正反面结构设计
单词翻转卡是项目中交互最复杂的卡片,使用Stack容器叠加正反两个面。核心思路是通过rotate和opacity的组合实现 3D 翻转动画:
// features/topicPage/src/main/ets/views/CourseHomePage.ets@Entry@ComponentV2exportstruct CourseHomePage{@LocalcurrentIndex:number=0;@LocalisFlipped:boolean=false;@LocalrotateY:number=0;@LocalfrontOpacity:number=1;@LocalbackOpacity:number=0;// ...其他状态...build(){// ...Stack(){// 正面:单词展示Column(){Row(){Text(this.currentWord().partOfSpeech).fontSize(12).fontColor('#165DFF').backgroundColor('#EAF2FF').padding({left:10,right:10,top:4,bottom:4}).borderRadius(12);Blank();Text(this.isFavorite?'★':'☆').fontSize(24).fontColor('#FFB020').onClick(()=>{this.isFavorite=!this.isFavorite;});}.width('100%').padding({left:18,right:18,top:14});Column(){Text(this.currentWord().word).fontSize(34).fontWeight(FontWeight.Bold).fontColor('#165DFF').margin({bottom:8});Row({space:8}){Text(this.currentWord().phonetic).fontSize(15).fontColor($r('sys.color.font_secondary'));Text('▶').fontSize(12).fontColor('#FFFFFF').width(26).height(26).textAlign(TextAlign.Center).borderRadius(13).backgroundColor('#165DFF');}.alignItems(VerticalAlign.Center).margin({bottom:16});Text(this.currentWord().translation).fontSize(20).fontWeight(FontWeight.Medium).fontColor($r('sys.color.font_primary')).textAlign(TextAlign.Center).margin({bottom:14});Text(this.currentWord().example).fontSize(14).fontColor($r('sys.color.font_secondary')).textAlign(TextAlign.Center).maxLines(2).padding({left:18,right:18});}.layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).width('100%');Text('点击卡片查看释义').fontSize(11).fontColor($r('sys.color.font_tertiary')).margin({bottom:12});}.width('100%').height(280).backgroundColor($r('sys.color.background_primary')).borderRadius(20).shadow(ShadowStyle.OUTER_DEFAULT_MD)// 3D 投影效果.rotate({x:0,y:this.rotateY,z:0,centerX:'50%',centerY:'50%',perspective:1200}).opacity(this.frontOpacity).onClick(()=>this.flipCard());// 反面:详细释义Column(){Text('详细释义').fontSize(13).fontColor($r('sys.color.font_secondary')).margin({bottom:8});Text(this.currentWord().translation).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#165DFF').textAlign(TextAlign.Center).margin({bottom:14});Text(this.currentWord().example).fontSize(14).fontColor($r('sys.color.font_primary')).padding({left:20,right:20}).textAlign(TextAlign.Center).maxLines(2).margin({bottom:8});Text(this.currentWord().exampleTranslation).fontSize(13).fontColor($r('sys.color.font_secondary')).padding({left:20,right:20}).textAlign(TextAlign.Center).maxLines(2).margin({bottom:14});Row({space:6}){Text(this.getCategoryLabel()).fontSize(10).fontColor('#165DFF').backgroundColor('#EAF2FF').padding({left:8,right:8,top:3,bottom:3}).borderRadius(8);ForEach(this.currentWord().tags,(tag:string)=>{Text(tag).fontSize(10).fontColor('#165DFF').backgroundColor('#EAF2FF').padding({left:8,right:8,top:3,bottom:3}).borderRadius(8);},(tag:string)=>tag);}.justifyContent(FlexAlign.Center).margin({bottom:10});Text('点击返回正面').fontSize(11).fontColor($r('sys.color.font_tertiary'));}.width('100%').height(280).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).backgroundColor('#F8FAFC')// 浅灰背景区分正反面.borderRadius(20).shadow(ShadowStyle.OUTER_DEFAULT_MD).rotate({x:0,y:this.rotateY-180,z:0,centerX:'50%',centerY:'50%',perspective:1200}).opacity(this.backOpacity).onClick(()=>this.flipCard());}.width('90%').height(280);}}4.2 翻转动画实现
翻转动画通过animateTo驱动,在 400ms 内完成 Y 轴旋转 180 度:
privateflipCard():void{animateTo({duration:400,curve:Curve.EaseInOut},()=>{if(this.isFlipped){this.rotateY=0;// 回到正面this.frontOpacity=1;this.backOpacity=0;}else{this.rotateY=180;// 翻到反面this.frontOpacity=0;this.backOpacity=1;}this.isFlipped=!this.isFlipped;});}关键动画参数:
| 参数 | 值 | 说明 |
|---|---|---|
| duration | 400ms | 翻转时长,适中节奏 |
| curve | Curve.EaseInOut | 缓入缓出,模拟真实惯性 |
| rotate.y | 0 → 180 | Y 轴旋转角度 |
| perspective | 1200 | 透视距离,值越小 3D 效果越强 |
正反面同步技巧:
反面卡片的rotate比正面多 180 度(this.rotateY - 180),确保卡片翻转过程中始终保持"某一面正对用户"的视觉效果。透明度(frontOpacity/backOpacity)配合旋转角度,在翻转过程中平滑过渡,避免背面透过正面显示。
4.3 卡片颜色与布局差异
正反面的视觉差异通过颜色和布局来体现:
| 维度 | 正面 | 反面 |
|---|---|---|
| 背景色 | background_primary(白色) | #F8FAFC(浅灰) |
| 主色调 | 单词 34px Bold#165DFF | 释义 22px Bold#165DFF |
| 内容重点 | 词性徽章、发音按钮、收藏 | 例句翻译、分类标签 |
| 底部提示 | “点击卡片查看释义” | “点击返回正面” |
这种对比明确了"正面浏览单词 — 反面查看详解"的功能划分,用户一目了然。
五、卡片设计的通用原则
5.1 视觉规范
| 原则 | 实现方式 |
|---|---|
| 圆角统一 | 所有卡片使用 16~20vp borderRadius |
| 阴影层次 | ShadowStyle.OUTER_DEFAULT_MD提供 3D 层次感 |
| 留白充足 | 卡片内统一 16vp padding |
| 颜色系统 | 背景色使用sys.color变量,自动适配主题 |
5.2 交互反馈
// 三种反馈示例.stateStyles({pressed:{.scale({x:0.97,y:0.97})}})// 1. 按压缩放.onClick(()=>this.flipCard())// 2. 点击切换.gesture(SwipeGesture().onAction(...))// 3. 手势滑动六、总结
从简单的今日学习卡到复杂的 3D 翻转单词卡,卡片式 UI 设计贯穿了英语学习 App 的各个页面。通过borderRadius、shadow、stateStyles等技术手段的组合使用,我们构建了视觉统一、交互反馈丰富的卡片体系。单词翻转卡更是实现了正反面独立布局 + 3D 翻转动画,在有限屏幕空间内展示了单词、音标、释义、例句、分类标签等多维信息,大幅提升了学习效率。
在卡片组件设计时,建议遵循"先统一后差异化"的原则,先用基础容器确定卡片模板,再针对不同业务场景定制内容和交互。