前端成功转鸿蒙开发者真实案例,教大家如何开发鸿蒙APP--ArkTS 卡片页面开发核心能力
ArkTS 卡片通过Canvas组件开放自定义绘制能力,支持通过接口绘制图形、文字、路径等,适用于需要个性化视觉元素的场景(如自定义图表、品牌 Logo)。初始化对象,配置抗锯齿(在Canvas的onReady回调中获取画布实际宽高(确保绘制坐标准确);使用的 API 执行绘制逻辑(填充、描边、路径等)。在项目resources浅色主题资源"color": [{ "name": "card_bg"

大家好,我是陈杨,一名有着8 年前端开发经验、6 年技术写作积淀的鸿蒙开发者,也是鸿蒙生态里的一名极客。
曾因前端行业的危机感居安思危,果断放弃饱和的 iOS、安卓赛道,在鸿蒙 API9 发布时,凭着前端技术底子,三天吃透 ArkTS 框架,快速上手鸿蒙开发。三年深耕,我不仅打造了鸿蒙开源图表组件库「莓创图表」,闯进过创新赛、极客挑战赛总决赛,更带着团队实实在在做出了成果 —— 目前已成功上架11 款鸿蒙应用,涵盖工具、效率、创意等多个品类,包括JLPT、REFLEX PRO、国潮纸刻、Wss 直连、ZenithDocs Pro、圣诞相册、CSS 特效等,靠这些自研产品赚到了转型后的第一桶金。
从前端转型到鸿蒙掘金,靠的不是运气,而是选对赛道的眼光和快速落地的执行力。今天这篇文章,就接着上一篇的内容,和大家聊聊 [ArkTS 卡片页面开发核心能力]。
上一篇我们梳理了 ArkTS 卡片的创建、配置与生命周期管理,感兴趣的可以在我的主页去查看。我们这篇将聚焦卡片页面开发的核心能力—— 从动态交互的动效设计、个性化的自定义绘制,到适配系统的深浅色模式、品牌化的自定义字体,再到页面与应用的交互逻辑,通过实操代码与场景说明,帮助我们快速实现高体验的 ArkTS 卡片页面。
一、ArkTS 卡片动效能力:让交互更流畅
ArkTS 卡片在动态卡片中开放了属性动画、显式动画、组件内转场三种动效能力,可增强用户交互反馈,但需遵守严格的参数限制(静态卡片不支持任何动效)。
1.1 动效参数核心限制
所有动效类型均需遵守以下限制,超出限制会导致动效异常:
| 参数名称 | 作用 | 限制描述 |
|---|---|---|
duration |
动画播放时长(毫秒) | 最长 1 秒(1000ms),设置超过 1 秒仍按 1 秒执行;小于 0 按 0 处理 |
tempo |
动画播放速度 | 禁止手动设置,强制使用默认值 1(正常速度) |
delay |
动画延迟执行时长 | 禁止手动设置,强制使用默认值 0(立即执行) |
iterations |
动画播放次数 | 禁止手动设置,强制使用默认值 1(播放 1 次) |
1.2 三种动效类型实操示例
1.2.1 属性动画:组件属性变化触发
通过修改组件的属性(如旋转角度、透明度),结合animation接口自动触发动画,适用于简单的组件状态切换(如按钮点击旋转)。
typescript
运行
// 动效示例1:属性动画(按钮旋转)
@Entry
@Component
struct AttrAnimationCard {
// 定义旋转角度状态(初始0度)
@State rotateAngle: number = 0;
build() {
Column({ space: 20 }) {
Text("属性动画:按钮旋转")
.fontSize(16)
.fontWeight(FontWeight.Bold)
// 点击按钮切换旋转角度(0° ↔ 90°)
Button("点击旋转")
.width("80%")
.height(50)
.onClick(() => {
this.rotateAngle = this.rotateAngle === 0 ? 90 : 0;
})
// 设置旋转属性
.rotate({ angle: this.rotateAngle })
// 配置动画(仅允许设置curve和playMode)
.animation({
curve: Curve.EaseOut, // 动画曲线:先快后慢
playMode: PlayMode.Normal // 播放模式:正常(1次)
})
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
1.2.2 显式动画:通过animateTo主动触发
使用全局animateTo接口包裹状态变化逻辑,主动控制动画触发时机,适用于复杂的多属性同步变化(如视图缩放 + 透明度变化)。
typescript
运行
// 动效示例2:显式动画(视图缩放+透明度)
import { animateTo } from '@kit.ArkUI';
@Entry
@Component
struct ExplicitAnimationCard {
@State scale: number = 1;
@State opacity: number = 1;
build() {
Column({ space: 20 }) {
Text("显式动画:缩放+透明度")
.fontSize(16)
.fontWeight(FontWeight.Bold)
// 动画目标视图
Text("HarmonyOS Card")
.fontSize(20)
.scale({ x: this.scale, y: this.scale })
.opacity(this.opacity)
Button("触发动画")
.width("80%")
.height(50)
.onClick(() => {
// 显式动画:包裹状态变化逻辑
animateTo({
curve: Curve.EaseInOut, // 先加速后减速
// duration默认1000ms,无需手动设置
}, () => {
// 动画期间的状态变化
this.scale = this.scale === 1 ? 1.2 : 1;
this.opacity = this.opacity === 1 ? 0.6 : 1;
});
})
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
1.2.3 组件内转场:组件显示 / 消失时的过渡
通过transition接口定义组件 “出现” 和 “消失” 的过渡效果,适用于组件动态加载 / 卸载的场景(如弹窗、图片切换)。
typescript
运行
// 动效示例3:组件内转场(图片显示/消失)
@Entry
@Component
struct TransitionAnimationCard {
@State isShow: boolean = false;
build() {
Column({ space: 20 }) {
Text("组件内转场:图片显示/消失")
.fontSize(16)
.fontWeight(FontWeight.Bold)
// 条件渲染图片,配置转场效果
if (this.isShow) {
Image($r("app.media.test_img")) // 替换为你的图片资源
.width("60%")
.height(150)
.objectFit(ImageFit.Cover)
// 转场效果:透明度+旋转组合
.transition(
TransitionEffect.OPACITY.animation({ curve: Curve.Ease })
.combine(TransitionEffect.rotate({ z: 1, angle: 180 }))
)
}
Button(this.isShow ? "隐藏图片" : "显示图片")
.width("80%")
.height(50)
.onClick(() => {
this.isShow = !this.isShow;
})
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
}
1.3 动效开发注意事项
- 静态卡片不支持动效:仅动态卡片(
isDynamic: true)可使用上述动效能力; - 避免复杂动效:卡片内存资源有限,建议减少多组件同时动效,防止卡顿;
- 动画曲线选择:常用
Curve.EaseInOut(交互反馈)、Curve.Linear(匀速变化),根据场景选择合适曲线。
二、ArkTS 卡片自定义绘制:实现个性化图形
ArkTS 卡片通过Canvas组件开放自定义绘制能力,支持通过CanvasRenderingContext2D接口绘制图形、文字、路径等,适用于需要个性化视觉元素的场景(如自定义图表、品牌 Logo)。
2.1 自定义绘制核心流程
- 初始化
CanvasRenderingContext2D对象,配置抗锯齿(RenderingContextSettings(true)); - 在
Canvas的onReady回调中获取画布实际宽高(确保绘制坐标准确); - 使用
CanvasRenderingContext2D的 API 执行绘制逻辑(填充、描边、路径等)。
2.2 实操示例:绘制笑脸图形
以下代码实现 “中心笑脸” 的自定义绘制,每一步都有详细注释:
typescript
运行
// 自定义绘制示例:Canvas绘制笑脸
@Entry
@Component
struct CustomCanvasCard {
// 1. 初始化Canvas上下文(抗锯齿开启)
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
// 画布宽高(onReady中动态获取)
private canvasWidth: number = 0;
private canvasHeight: number = 0;
build() {
Column() {
Text("自定义绘制:笑脸")
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
// 2. 定义Canvas组件,占满父容器
Canvas(this.ctx)
.width("100%")
.height(300)
.backgroundColor("#EEF0FF")
// 3. 画布就绪后执行绘制逻辑
.onReady(() => {
this.canvasWidth = this.ctx.width; // 获取实际宽度
this.canvasHeight = this.ctx.height; // 获取实际高度
this.drawSmiley(); // 执行绘制函数
})
}
.width("100%")
.height("100%")
.padding(15)
}
// 4. 绘制笑脸的核心逻辑
private drawSmiley(): void {
const ctx = this.ctx;
const centerX = this.canvasWidth / 2; // 画布中心X坐标
const centerY = this.canvasHeight / 2; // 画布中心Y坐标
const faceRadius = Math.min(centerX, centerY) * 0.6; // 脸部半径(适配画布大小)
// 步骤1:绘制脸部(圆形填充)
ctx.beginPath(); // 开始新路径
ctx.arc(centerX, centerY, faceRadius, 0, 2 * Math.PI); // 画圆(360°)
ctx.fillStyle = "#5A5FFF"; // 填充颜色:蓝色
ctx.fill(); // 执行填充
// 步骤2:绘制左眼(圆形描边)
const eyeRadius = faceRadius / 13; // 眼睛半径
const eyeOffsetX = faceRadius / 2.3; // 眼睛X轴偏移
const eyeOffsetY = faceRadius / 4.5; // 眼睛Y轴偏移
ctx.beginPath();
ctx.arc(centerX - eyeOffsetX, centerY - eyeOffsetY, eyeRadius, 0, 2 * Math.PI);
ctx.strokeStyle = "#FFFFFF"; // 描边颜色:白色
ctx.lineWidth = 15; // 描边宽度
ctx.stroke(); // 执行描边
// 步骤3:绘制右眼(与左眼对称)
ctx.beginPath();
ctx.arc(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, 0, 2 * Math.PI);
ctx.strokeStyle = "#FFFFFF";
ctx.lineWidth = 15;
ctx.stroke();
// 步骤4:绘制鼻子(三角形描边)
ctx.beginPath();
ctx.moveTo(centerX, centerY - 20); // 起点:鼻梁顶部
ctx.lineTo(centerX - 8, centerY + 20); // 左底点
ctx.lineTo(centerX + 8, centerY + 20); // 右底点
ctx.closePath(); // 闭合路径(回到起点)
ctx.strokeStyle = "#FFFFFF";
ctx.lineWidth = 15;
ctx.lineCap = "round"; // 线条端点:圆形
ctx.lineJoin = "round"; // 线条交点:圆形
ctx.stroke();
// 步骤5:绘制嘴巴(圆弧描边)
const mouthRadius = faceRadius / 2; // 嘴巴半径
ctx.beginPath();
// 画圆弧(从126°到54°,顺时针)
ctx.arc(centerX, centerY + 10, mouthRadius, Math.PI / 1.4, Math.PI / 3.4, true);
ctx.strokeStyle = "#FFFFFF";
ctx.lineWidth = 15;
ctx.stroke();
}
}
2.3 常用 Canvas 绘制 API
| API 名称 | 作用 | 示例 |
|---|---|---|
fillRect(x, y, w, h) |
填充矩形 | 绘制背景矩形 |
arc(x, y, r, startAngle, endAngle) |
绘制圆弧 / 圆形 | 画脸部、眼睛 |
moveTo(x, y) / lineTo(x, y) |
绘制线段 | 画鼻子三角形 |
stroke() / fill() |
执行描边 / 填充 | 确定图形最终显示 |
strokeStyle / fillStyle |
设置描边 / 填充颜色 | ctx.fillStyle = "#5A5FFF" |
三、ArkTS 卡片深浅色模式适配:跟随系统视觉风格
为保证卡片与系统视觉一致性,ArkTS 卡片支持自动适配系统深浅色模式,核心是通过 “配置跟随系统”+“主题色资源隔离” 实现。
3.1 适配核心原理
- 在
form_config.json中设置colorMode: "auto"(默认值),卡片自动跟随系统主题; - 在资源文件中按 “浅色 / 深色” 分别定义颜色、图片等资源;
- 卡片 UI 中引用 “主题无关” 的资源 ID,系统会根据当前主题加载对应资源。
3.2 实操步骤
步骤 1:配置form_config.json(确保跟随系统)
无需额外修改,默认colorMode为auto,若已手动设置需确保:
json
{
"forms": [
{
"colorMode": "auto", // 关键配置:跟随系统深浅色
// 其他配置...
}
]
}
步骤 2:定义主题化资源
在项目resources目录下,按主题分类定义资源(以颜色为例):
- 浅色主题资源:
resources/base/element/color.json
{
"color": [
{ "name": "card_bg", "value": "#FFFFFF" }, // 卡片背景:白色
]
}
- 深色主题资源:
resources/dark/element/color.json(需手动创建dark目录)
{
"color": [
{ "name": "card_bg", "value": "#1E1E1E" }, // 卡片背景:深灰
{ "name": "card_text", "value": "#EEEEEE" } // 卡片文字:浅灰
]
}
步骤 3:UI 中引用主题资源
卡片页面中通过$r("app.color.xxx")引用资源,无需判断主题,系统自动切换:
// 深浅色适配示例
@Entry
@Component
struct DarkLightAdaptCard {
build() {
Column()
.width("100%")
.height("100%")
.backgroundColor($r("app.color.card_bg")) // 主题化背景色
.padding(15) {
Text("深浅色适配示例")
.fontSize(18)
.fontWeight(FontWeight.Bold)
.color($r("app.color.card_text")) // 主题化文字色
.margin({ bottom: 20 })
Text("当前系统主题:" + (this.getSystemTheme() ? "深色" : "浅色"))
.color($r("app.color.card_text"))
}
}
// 辅助函数:判断当前系统主题(仅作演示,实际无需手动判断)
private getSystemTheme(): boolean {
// 实际开发中无需此逻辑,资源引用会自动适配
const colorMode = getAppContext().colorMode;
return colorMode === ColorMode.DARK;
}
}
四、ArkTS 卡片自定义字体:实现品牌化文字样式
这里要告诉一个好消息给大家,从 API Version 22 开始,ArkTS 卡片支持加载本地自定义字体(.ttf 格式),满足品牌化文字设计需求,这个我提了大概三年了,早就想要这个功能,这个版本终于实现了。自定义字体的核心是通过FontCollection接口管理字体加载与卸载,大家赶快用起来吧。
4.1 核心限制与原理
- 内存限制:单个应用所有卡片共用字体实例,总加载字体大小不超过 20MB;
- 文件路径:字体文件需放在
rawfile目录(需手动创建); - 共用性:加载 / 卸载字体后,同一应用的所有卡片会同步更新字体样式。
4.2 实操步骤
步骤 1:添加字体文件
- 在项目
entry/src/main/resources/目录下创建rawfile文件夹; - 将自定义字体文件(如
MyBrandFont.ttf)放入rawfile目录。
步骤 2:页面中加载 / 卸载字体
通过FontCollection.getLocalInstance()获取字体实例,调用loadFontSync/unloadFontSync实现字体管理:
// 自定义字体示例(API Version 22+)
import { text } from '@kit.ArkGraphics2D';
@Entry
@Component
struct CustomFontCard {
// 1. 初始化本地字体集实例
private fontCollection: text.FontCollection = text.FontCollection.getLocalInstance();
// 2. 控制字体显示状态与文本内容
@State useCustomFont: boolean = false;
@State displayText: string = "默认字体样式";
build() {
Column({ space: 25 })
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
.padding(15) {
// 3. 显示文本(根据状态切换字体)
Text(this.displayText)
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontFamily(this.useCustomFont ? "MyBrandFont" : "") // 自定义字体名称
// 4. 加载自定义字体按钮
Button("加载自定义字体")
.width("80%")
.height(50)
.backgroundColor("#5A5FFF")
.fontColor("#FFFFFF")
.onClick(() => {
try {
// 加载字体:参数1=字体名称(自定义),参数2=字体文件路径
this.fontCollection.loadFontSync("MyBrandFont", $rawfile("MyBrandFont.ttf"));
this.useCustomFont = true;
this.displayText = "自定义字体样式";
} catch (error) {
console.error("字体加载失败:", error);
}
})
// 5. 卸载自定义字体按钮
Button("卸载自定义字体")
.width("80%")
.height(50)
.backgroundColor("#FF6B6B")
.fontColor("#FFFFFF")
.onClick(() => {
try {
// 卸载字体:参数=字体名称
this.fontCollection.unloadFontSync("MyBrandFont");
this.useCustomFont = false;
this.displayText = "默认字体样式";
} catch (error) {
console.error("字体卸载失败:", error);
}
})
}
}
}
五、卡片页面交互补充:动态与静态卡片的差异
卡片页面交互的核心是 “与应用通信”,动态卡片和静态卡片的实现方式不同,需针对性开发。
5.1 动态卡片:使用postCardAction
动态卡片通过postCardAction接口触发三种事件(router/message/call),需在EntryFormAbility.ets中处理回调。
示例 1:router事件(跳转应用 UIAbility)
// 动态卡片:跳转应用页面
Button("打开应用详情页")
.width("80%")
.height(50)
.onClick(() => {
postCardAction(this, {
action: "router", // 事件类型:跳转
abilityName: "EntryAbility", // 目标UIAbility名称
params: {
targetPage: "DetailPage", // 传递参数:目标页面
id: "123" // 业务参数
}
});
});
示例 2:message事件(触发onFormEvent)
// 动态卡片:发送消息给FormExtensionAbility
Button("刷新卡片数据")
.width("80%")
.height(50)
.onClick(() => {
postCardAction(this, {
action: "message", // 事件类型:消息
params: {
actionType: "refreshData" // 自定义消息参数
}
});
});
// 在EntryFormAbility.ets中处理消息
onFormEvent(formId: string, message: string): void {
const params = JSON.parse(message);
if (params.actionType === "refreshData") {
// 执行刷新逻辑(如更新卡片数据)
const newData = { title: "刷新后标题", content: "刷新后内容" };
formProvider.updateForm(formId, formBindingData.createFormBindingData(newData));
}
}
5.2 静态卡片:使用FormLink
静态卡片仅支持FormLink组件实现交互,用法与postCardAction类似,但需包裹交互元素:
// 静态卡片:使用FormLink跳转
FormLink({
action: "router",
abilityName: "EntryAbility",
params: { targetPage: "HomePage" }
}) {
Text("打开应用首页")
.fontSize(16)
.color("#5A5FFF")
.underline(true)
}
六、总结
这篇聚焦 ArkTS 卡片页面开发的 “核心能力层”,从动效设计(提升交互反馈)、自定义绘制(实现个性化视觉)、深浅色适配(保障系统一致性)、自定义字体(强化品牌识别)到页面交互(打通卡片与应用),提供了完整的实操方案。大家有什么疑问都可以在评论区评论,也可以私聊我,也有交流群,欢迎大家。
开发建议:
- 优先使用动态卡片实现复杂交互,但需控制内存开销;
- 动效设计以 “轻量” 为原则,避免多组件同时动画;
- 自定义字体和绘制需注意资源大小,避免超出系统限制;
- 深浅色适配需提前规划资源,确保两种主题下的视觉体验一致。
更多推荐



所有评论(0)