大家好,我是陈杨,一名有着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 自定义绘制核心流程

  1. 初始化CanvasRenderingContext2D对象,配置抗锯齿(RenderingContextSettings(true));
  2. CanvasonReady回调中获取画布实际宽高(确保绘制坐标准确);
  3. 使用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 适配核心原理

  1. form_config.json中设置colorMode: "auto"(默认值),卡片自动跟随系统主题;
  2. 在资源文件中按 “浅色 / 深色” 分别定义颜色、图片等资源;
  3. 卡片 UI 中引用 “主题无关” 的资源 ID,系统会根据当前主题加载对应资源。

3.2 实操步骤

步骤 1:配置form_config.json(确保跟随系统)

无需额外修改,默认colorModeauto,若已手动设置需确保:

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:添加字体文件
  1. 在项目entry/src/main/resources/目录下创建rawfile文件夹;
  2. 将自定义字体文件(如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 卡片页面开发的 “核心能力层”,从动效设计(提升交互反馈)、自定义绘制(实现个性化视觉)、深浅色适配(保障系统一致性)、自定义字体(强化品牌识别)到页面交互(打通卡片与应用),提供了完整的实操方案。大家有什么疑问都可以在评论区评论,也可以私聊我,也有交流群,欢迎大家。

开发建议:

  1. 优先使用动态卡片实现复杂交互,但需控制内存开销;
  2. 动效设计以 “轻量” 为原则,避免多组件同时动画;
  3. 自定义字体和绘制需注意资源大小,避免超出系统限制;
  4. 深浅色适配需提前规划资源,确保两种主题下的视觉体验一致。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐