本文字数:约3200字 | 预计阅读时间:13分钟

前置知识:建议先学习本系列前六篇,特别是UI组件和数据存储

实战价值:掌握服务卡片开发,让应用信息直达桌面,提升用户体验

系列导航:本文是《鸿蒙开发系列》第7篇,下篇将讲解多端协同与流转

一、服务卡片概述:桌面上的应用窗口

服务卡片(Service Widget)是鸿蒙应用的一种重要形态,它允许用户在不打开应用的情况下,直接在桌面上查看应用的关键信息和快速操作。服务卡片可以展示天气、待办事项、新闻摘要等信息,并支持交互操作。

1.1 服务卡片的优势

  • 信息直达:关键信息直接展示在桌面

  • 快速操作:无需打开应用即可执行常用功能

  • 提升活跃度:增加用户与应用交互的机会

  • 个性展示:支持多种尺寸和样式

1.2 卡片尺寸与类型

卡片类型 尺寸 (dp) 适用场景
1×2卡片 100×200 显示简单信息,如天气、时间
2×2卡片 200×200 展示图文结合的信息
2×4卡片 200×400 显示详细信息和列表
4×4卡片 400×400 展示复杂信息或多功能操作

二、创建第一个服务卡片

2.1 创建卡片工程

在DevEco Studio中创建服务卡片:

  1. 右键点击项目 → New → Service Widget

  2. 选择卡片模板(推荐:Grid pattern)

  3. 配置卡片信息:

    • 卡片名称:WeatherCard

    • 卡片类型:Java/JS

    • 支持的设备:Phone

    • 卡片尺寸:2×2

2.2 卡片目录结构

text

复制

下载

entry/src/main/
├── ets/
│   ├── entryability/
│   └── pages/
│       └── index.ets
├── resources/
│   └── base/
│       ├── element/
│       │   └── string.json          # 字符串资源
│       ├── graphic/
│       │   └── background_card.xml   # 卡片背景
│       ├── layout/
│       │   └── weather_card.xml      # 卡片布局
│       └── profile/
│           └── weather_card.json     # 卡片配置文件
└── module.json5

2.3 卡片配置文件

json

复制

下载

// resources/base/profile/weather_card.json
{
  "forms": [
    {
      "name": "WeatherCard",
      "description": "天气卡片",
      "src": "./ets/widget/pages/WeatherCard/WeatherCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 200,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "2*2",
      "supportDimensions": ["2*2", "2*4"],
      "formConfigAbility": "EntryAbility"
    }
  ]
}

2.4 卡片页面代码

typescript

复制

下载

// ets/widget/pages/WeatherCard/WeatherCard.ets
@Entry
@Component
struct WeatherCard {
  @State temperature: number = 25;
  @State weather: string = '晴';
  @State city: string = '北京';
  @State forecastList: Array<{day: string, temp: number, icon: string}> = [
    {day: '今天', temp: 25, icon: 'sunny'},
    {day: '明天', temp: 26, icon: 'cloudy'},
    {day: '后天', temp: 24, icon: 'rain'}
  ];

  build() {
    Column({ space: 8 }) {
      // 顶部:城市和刷新按钮
      Row({ space: 8 }) {
        Text(this.city)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Image($r('app.media.refresh'))
          .width(20)
          .height(20)
          .onClick(() => {
            this.updateWeather();
          })
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 8 })

      // 中部:当前天气
      Column({ space: 4 }) {
        Row() {
          Text(`${this.temperature}`)
            .fontSize(48)
            .fontWeight(FontWeight.Bold)

          Text('°C')
            .fontSize(16)
            .margin({ top: 8 })
        }

        Text(this.weather)
          .fontSize(14)
          .fontColor('#666666')
      }
      .height(120)
      .width('100%')
      .justifyContent(FlexAlign.Center)

      // 底部:天气预报
      Row({ space: 12 }) {
        ForEach(this.forecastList, (item) => {
          Column({ space: 4 }) {
            Text(item.day)
              .fontSize(12)
              .fontColor('#666666')

            Image($r(`app.media.${item.icon}`))
              .width(24)
              .height(24)

            Text(`${item.temp}°`)
              .fontSize(14)
          }
          .width('33%')
        })
      }
      .width('100%')
      .padding({ left: 12, right: 12, bottom: 12 })
    }
    .height('100%')
    .backgroundImage($r('app.media.card_bg'), ImageRepeat.NoRepeat)
    .backgroundImageSize(ImageSize.Cover)
    .borderRadius(16)
  }

  // 更新天气数据
  updateWeather() {
    // 这里可以调用天气API
    console.log('更新天气数据');
  }
}

三、卡片数据管理与更新

3.1 卡片数据持久化

typescript

// ets/utils/CardDataManager.ts
import dataPreferences from '@ohos.data.preferences';

class CardDataManager {
  private static instance: CardDataManager;
  private preferences: dataPreferences.Preferences | null = null;

  private constructor() {}

  static getInstance(): CardDataManager {
    if (!CardDataManager.instance) {
      CardDataManager.instance = new CardDataManager();
    }
    return CardDataManager.instance;
  }

  async init(context: any) {
    try {
      this.preferences = await dataPreferences.getPreferences(context, 'card_data');
    } catch (error) {
      console.error('卡片数据初始化失败:', error);
    }
  }

  // 保存卡片数据
  async saveCardData(formId: string, data: any) {
    if (!this.preferences) return false;

    try {
      await this.preferences.put(formId, JSON.stringify(data));
      await this.preferences.flush();
      return true;
    } catch (error) {
      console.error('保存卡片数据失败:', error);
      return false;
    }
  }

  // 获取卡片数据
  async getCardData(formId: string): Promise<any> {
    if (!this.preferences) return null;

    try {
      const data = await this.preferences.get(formId, '{}');
      return JSON.parse(data as string);
    } catch (error) {
      console.error('获取卡片数据失败:', error);
      return null;
    }
  }
}

export default CardDataManager;

3.2 卡片更新管理

typescript

// ets/utils/CardUpdateManager.ts
import formBindingData from '@ohos.app.form.formBindingData';
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';

class CardUpdateManager {
  // 更新卡片数据
  static async updateCard(formId: string, data: any) {
    try {
      const formData = {
        temperature: data.temperature,
        weather: data.weather,
        city: data.city,
        forecastList: data.forecastList
      };

      const formBinding = formBindingData.createFormBindingData(formData);
      await FormExtensionAbility.updateForm(formId, formBinding);
      
      console.log('卡片更新成功:', formId);
      return true;
    } catch (error) {
      console.error('卡片更新失败:', error);
      return false;
    }
  }

  // 定时更新卡片
  static async scheduleCardUpdate(formId: string, interval: number = 30) {
    try {
      await FormExtensionAbility.setFormNextRefreshTime(formId, interval * 60);
      return true;
    } catch (error) {
      console.error('设置定时更新失败:', error);
      return false;
    }
  }

  // 手动触发更新
  static async triggerCardUpdate(formId: string) {
    try {
      await FormExtensionAbility.requestForm(formId);
      return true;
    } catch (error) {
      console.error('手动更新失败:', error);
      return false;
    }
  }
}

export default CardUpdateManager;

3.3 卡片Ability开发

typescript

// ets/entryability/FormAbility.ts
import FormExtension from '@ohos.app.form.FormExtensionAbility';
import CardDataManager from '../utils/CardDataManager';

export default class FormAbility extends FormExtension {
  private cardDataManager = CardDataManager.getInstance();

  onAddForm(want) {
    console.log('FormAbility onAddForm');
    
    // 初始化卡片数据管理器
    this.cardDataManager.init(this.context);

    // 创建卡片数据
    const formData = {
      temperature: 25,
      weather: '晴',
      city: '北京',
      forecastList: [
        {day: '今天', temp: 25, icon: 'sunny'},
        {day: '明天', temp: 26, icon: 'cloudy'},
        {day: '后天', temp: 24, icon: 'rain'}
      ]
    };

    // 保存卡片数据
    this.cardDataManager.saveCardData(want.parameters.formId, formData);

    return formData;
  }

  onCastToNormalForm(formId) {
    console.log('FormAbility onCastToNormalForm');
  }

  onUpdateForm(formId) {
    console.log('FormAbility onUpdateForm');
    
    // 获取最新的卡片数据
    this.cardDataManager.getCardData(formId).then(data => {
      if (data) {
        // 这里可以更新数据,比如从网络获取最新天气
        data.temperature = Math.floor(Math.random() * 10) + 20;
        
        // 保存更新后的数据
        this.cardDataManager.saveCardData(formId, data);
        
        // 更新卡片显示
        this.updateForm(formId, data);
      }
    });
  }

  onChangeFormVisibility(newStatus) {
    console.log('FormAbility onChangeFormVisibility');
  }

  onFormEvent(formId, message) {
    console.log('FormAbility onFormEvent:', message);
    
    // 处理卡片事件
    if (message === 'refresh') {
      this.onUpdateForm(formId);
    }
  }

  onRemoveForm(formId) {
    console.log('FormAbility onRemoveForm');
  }

  onConfigurationUpdate(config) {
    console.log('FormAbility onConfigurationUpdate');
  }

  // 更新卡片
  private async updateForm(formId: string, data: any) {
    const formData = {
      temperature: data.temperature,
      weather: data.weather,
      city: data.city,
      forecastList: data.forecastList
    };

    const formBinding = formBindingData.createFormBindingData(formData);
    await this.updateForm(formId, formBinding);
  }
}

四、卡片交互与事件处理

4.1 卡片路由跳转

typescript

// ets/widget/pages/WeatherCard/WeatherCard.ets
@Entry
@Component
struct WeatherCard {
  @State temperature: number = 25;
  @State weather: string = '晴';
  
  // 获取FormExtensionContext
  private getFormExtensionContext() {
    // 在卡片中可以通过特定API获取
    return this.context as FormExtensionContext;
  }

  build() {
    Column({ space: 8 }) {
      // 天气信息区域(可点击跳转)
      Column({ space: 4 })
        .width('100%')
        .height(120)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .onClick(() => {
          this.openWeatherDetail();
        })
        .justifyContent(FlexAlign.Center) {
          Text(`${this.temperature}°C`)
            .fontSize(48)
            .fontWeight(FontWeight.Bold)

          Text(this.weather)
            .fontSize(16)
            .fontColor('#666666')
        }

      // 操作按钮区域
      Row({ space: 8 }) {
        Button('刷新')
          .width(80)
          .height(32)
          .fontSize(12)
          .onClick(() => {
            this.refreshWeather();
          })

        Button('设置')
          .width(80)
          .height(32)
          .fontSize(12)
          .backgroundColor('#F5F5F5')
          .fontColor('#333333')
          .onClick(() => {
            this.openSettings();
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ top: 12 })
    }
    .padding(16)
    .backgroundColor('#F8F8F8')
    .borderRadius(16)
  }

  // 打开天气详情页
  openWeatherDetail() {
    const formContext = this.getFormExtensionContext();
    if (formContext && formContext.startAbility) {
      const want = {
        bundleName: 'com.example.weatherapp',
        abilityName: 'WeatherDetailAbility',
        parameters: {
          city: '北京'
        }
      };
      formContext.startAbility(want).catch(console.error);
    }
  }

  // 刷新天气
  refreshWeather() {
    // 触发卡片更新事件
    const formContext = this.getFormExtensionContext();
    if (formContext && formContext.updateForm) {
      formContext.updateForm({
        temperature: Math.floor(Math.random() * 10) + 20,
        weather: ['晴', '多云', '阴', '小雨'][Math.floor(Math.random() * 4)]
      });
    }
  }

  // 打开设置
  openSettings() {
    const formContext = this.getFormExtensionContext();
    if (formContext && formContext.startAbility) {
      const want = {
        bundleName: 'com.example.weatherapp',
        abilityName: 'SettingsAbility'
      };
      formContext.startAbility(want).catch(console.error);
    }
  }
}

4.2 卡片动态交互

typescript

// ets/widget/pages/InteractiveCard/InteractiveCard.ets
@Entry
@Component
struct InteractiveCard {
  @State taskList: Array<{id: number, title: string, completed: boolean}> = [
    {id: 1, title: '完成鸿蒙卡片开发', completed: false},
    {id: 2, title: '学习ArkUI布局', completed: true},
    {id: 3, title: '写技术博客', completed: false}
  ];
  
  @State showCompleted: boolean = false;

  build() {
    Column({ space: 12 }) {
      // 标题和切换按钮
      Row({ space: 8 }) {
        Text('待办事项')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Text(this.showCompleted ? '隐藏已完成' : '显示已完成')
          .fontSize(12)
          .fontColor('#007DFF')
          .onClick(() => {
            this.showCompleted = !this.showCompleted;
          })
      }
      .width('100%')

      // 待办列表
      Column({ space: 8 }) {
        ForEach(this.getFilteredTasks(), (task) => {
          Row({ space: 8 }) {
            // 复选框
            Column()
              .width(20)
              .height(20)
              .border({ width: 1, color: task.completed ? '#34C759' : '#CCCCCC' })
              .backgroundColor(task.completed ? '#34C759' : 'transparent')
              .borderRadius(4)
              .onClick(() => {
                this.toggleTask(task.id);
              })

            // 任务标题
            Text(task.title)
              .fontSize(14)
              .fontColor(task.completed ? '#999999' : '#333333')
              .textDecoration(task.completed ? { type: TextDecorationType.LineThrough } : null)
              .layoutWeight(1)

            // 删除按钮
            Image($r('app.media.delete'))
              .width(16)
              .height(16)
              .onClick(() => {
                this.deleteTask(task.id);
              })
          }
          .width('100%')
          .padding(8)
          .backgroundColor('#FFFFFF')
          .borderRadius(8)
        })
      }

      // 添加新任务
      Row({ space: 8 }) {
        TextInput({ placeholder: '添加新任务...' })
          .width('70%')
          .height(36)
          .id('newTaskInput')

        Button('添加')
          .width('30%')
          .height(36)
          .fontSize(12)
          .onClick(() => {
            this.addTask();
          })
      }
      .width('100%')
    }
    .padding(16)
    .backgroundColor('#F5F5F5')
    .borderRadius(16)
  }

  // 获取过滤后的任务
  getFilteredTasks() {
    return this.taskList.filter(task => this.showCompleted || !task.completed);
  }

  // 切换任务状态
  toggleTask(id: number) {
    this.taskList = this.taskList.map(task => 
      task.id === id ? {...task, completed: !task.completed} : task
    );
  }

  // 删除任务
  deleteTask(id: number) {
    this.taskList = this.taskList.filter(task => task.id !== id);
  }

  // 添加新任务
  addTask() {
    const input = this.$refs.newTaskInput as TextInput;
    const title = input.value?.trim();
    
    if (title) {
      const newTask = {
        id: Date.now(),
        title: title,
        completed: false
      };
      
      this.taskList = [...this.taskList, newTask];
      input.value = '';
    }
  }
}

五、卡片样式与动画

5.1 卡片样式优化

typescript

// ets/widget/pages/StyledCard/StyledCard.ets
@Entry
@Component
struct StyledCard {
  @State time: string = this.getCurrentTime();
  @State battery: number = 85;
  @State steps: number = 8524;
  @State heartRate: number = 72;
  
  private timer: number | null = null;

  aboutToAppear() {
    // 每分钟更新一次时间
    this.timer = setInterval(() => {
      this.time = this.getCurrentTime();
    }, 60000);
  }

  aboutToDisappear() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }

  getCurrentTime(): string {
    const now = new Date();
    return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  }

  build() {
    Column({ space: 16 }) {
      // 时间显示
      Text(this.time)
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .textShadow({ radius: 4, color: '#00000040', offsetX: 2, offsetY: 2 })

      // 健康数据网格
      Grid() {
        GridItem() {
          this.buildHealthItem('电量', `${this.battery}%`, $r('app.media.battery'))
        }

        GridItem() {
          this.buildHealthItem('步数', this.steps.toString(), $r('app.media.steps'))
        }

        GridItem() {
          this.buildHealthItem('心率', `${this.heartRate} BPM`, $r('app.media.heart'))
        }

        GridItem() {
          this.buildHealthItem('卡路里', '426', $r('app.media.calorie'))
        }
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .width('100%')
    }
    .padding(24)
    .backgroundImage($r('app.media.health_bg'))
    .backgroundImageSize(ImageSize.Cover)
    .borderRadius(24)
    .shadow({ 
      radius: 20, 
      color: '#00000020', 
      offsetX: 0, 
      offsetY: 4 
    })
  }

  @Builder
  buildHealthItem(label: string, value: string, icon: Resource) {
    Column({ space: 8 }) {
      Row({ space: 6 }) {
        Image(icon)
          .width(16)
          .height(16)

        Text(label)
          .fontSize(12)
          .fontColor('#FFFFFF99')
      }

      Text(value)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
    }
    .padding(16)
    .backgroundColor('#FFFFFF20')
    .backdropBlur(10)
    .borderRadius(16)
    .border({ width: 1, color: '#FFFFFF30' })
  }
}

5.2 卡片动画效果

typescript

复制

下载

// ets/widget/pages/AnimatedCard/AnimatedCard.ets
import Curves from '@ohos.curves';

@Entry
@Component
struct AnimatedCard {
  @State isExpanded: boolean = false;
  @State progress: number = 0.75;
  @State rotation: number = 0;
  
  private animationTimer: number | null = null;

  aboutToAppear() {
    // 启动旋转动画
    this.animationTimer = setInterval(() => {
      this.rotation = (this.rotation + 1) % 360;
    }, 50);
  }

  aboutToDisappear() {
    if (this.animationTimer) {
      clearInterval(this.animationTimer);
    }
  }

  build() {
    Column({ space: 16 }) {
      // 标题和展开按钮
      Row({ space: 8 }) {
        Text('系统监控')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Image($r('app.media.arrow_down'))
          .width(16)
          .height(16)
          .rotate({ x: 0, y: 0, z: 1, angle: this.isExpanded ? 180 : 0 })
          .animation({
            duration: 300,
            curve: Curves.EaseInOut
          })
          .onClick(() => {
            this.isExpanded = !this.isExpanded;
          })
      }
      .width('100%')

      // CPU使用率
      Column({ space: 8 }) {
        Row({ space: 8 }) {
          Text('CPU使用率')
            .fontSize(14)
            .layoutWeight(1)

          Text(`${Math.floor(this.progress * 100)}%`)
            .fontSize(14)
            .fontColor('#007DFF')
        }

        // 进度条
        Column()
          .width('100%')
          .height(8)
          .backgroundColor('#EEEEEE')
          .borderRadius(4)
          .clip(true) {
            Column()
              .width(`${this.progress * 100}%`)
              .height('100%')
              .backgroundColor('#007DFF')
              .borderRadius(4)
              .animation({
                duration: 500,
                curve: Curves.EaseOut
              })
          }
      }

      // 展开的内容
      if (this.isExpanded) {
        Column({ space: 12 }) {
          // 内存使用
          this.buildMetricItem('内存', '4.2/8GB', 0.52)
          
          // 网络速度
          this.buildMetricItem('网络', '256 Kbps', 0.32)
          
          // 磁盘空间
          this.buildMetricItem('磁盘', '128/256GB', 0.5)
          
          // 旋转的风扇图标
          Row()
            .justifyContent(FlexAlign.Center)
            .width('100%')
            .margin({ top: 16 }) {
              Image($r('app.media.fan'))
                .width(40)
                .height(40)
                .rotate({ x: 0, y: 0, z: 1, angle: this.rotation })
            }
        }
        .transition({
          type: TransitionType.Insert,
          opacity: 0,
          translate: { y: -20 }
        })
        .transition({
          type: TransitionType.Delete,
          opacity: 0,
          translate: { y: -20 }
        })
      }
    }
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
    .shadow({ radius: 12, color: '#00000010' })
  }

  @Builder
  buildMetricItem(label: string, value: string, progress: number) {
    Column({ space: 6 }) {
      Row({ space: 8 }) {
        Text(label)
          .fontSize(13)
          .fontColor('#666666')
          .layoutWeight(1)

        Text(value)
          .fontSize(13)
          .fontColor('#333333')
      }

      // 迷你进度条
      Row()
        .width('100%')
        .height(4)
        .backgroundColor('#F0F0F0')
        .borderRadius(2) {
          Row()
            .width(`${progress * 100}%`)
            .height('100%')
            .backgroundColor(this.getProgressColor(progress))
            .borderRadius(2)
        }
    }
  }

  getProgressColor(progress: number): string {
    if (progress > 0.8) return '#FF3B30';
    if (progress > 0.6) return '#FF9500';
    return '#34C759';
  }
}

六、卡片配置与部署

6.1 配置卡片信息

json

// resources/base/profile/form_config.json
{
  "forms": [
    {
      "name": "WeatherWidget",
      "description": "$string:weather_widget_desc",
      "src": "./ets/widget/pages/WeatherWidget/WeatherWidget.ets",
      "window": {
        "designWidth": 200,
        "autoDesignWidth": false
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "08:00",
      "updateDuration": 1,
      "defaultDimension": "2*2",
      "supportDimensions": ["1*2", "2*2", "2*4", "4*4"],
      "landscapeLayouts": ["$layout:weather_widget_land"],
      "portraitLayouts": ["$layout:weather_widget_port"],
      "formVisibleNotify": true,
      "formConfigAbility": "EntryAbility",
      "metaData": {
        "customizeData": [
          {
            "name": "widgetCategory",
            "value": "weather"
          },
          {
            "name": "widgetPriority",
            "value": "high"
          }
        ]
      }
    }
  ]
}

6.2 卡片字符串资源

json

// resources/base/element/string.json
{
  "string": [
    {
      "name": "weather_widget_desc",
      "value": "实时天气信息卡片"
    },
    {
      "name": "weather_widget_title",
      "value": "天气卡片"
    },
    {
      "name": "refresh_button",
      "value": "刷新"
    },
    {
      "name": "city_label",
      "value": "城市"
    }
  ]
}

6.3 卡片布局资源

xml

运行

<!-- resources/base/layout/weather_widget_land.xml -->
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:width="match_parent"
    ohos:height="match_parent"
    ohos:orientation="horizontal"
    ohos:padding="16dp">

    <Image
        ohos:id="$+id:weather_icon"
        ohos:width="48dp"
        ohos:height="48dp"
        ohos:image_src="$media:sunny"
        ohos:layout_alignment="center"/>

    <DirectionalLayout
        ohos:width="0dp"
        ohos:height="match_parent"
        ohos:orientation="vertical"
        ohos:weight="1"
        ohos:margin_left="12dp"
        ohos:alignment="vertical_center">

        <Text
            ohos:id="$+id:temperature"
            ohos:width="match_content"
            ohos:height="match_content"
            ohos:text="25°C"
            ohos:text_size="28fp"
            ohos:text_color="#333333"/>

        <Text
            ohos:id="$+id:weather_desc"
            ohos:width="match_content"
            ohos:height="match_content"
            ohos:text="晴"
            ohos:text_size="14fp"
            ohos:text_color="#666666"
            ohos:margin_top="4dp"/>
    </DirectionalLayout>

</DirectionalLayout>

七、卡片测试与调试

7.1 卡片调试技巧

typescript

// ets/utils/CardDebugger.ts
class CardDebugger {
  // 启用卡片调试模式
  static enableDebugMode() {
    console.info('卡片调试模式已启用');
    
    // 监听卡片生命周期事件
    this.listenToLifecycleEvents();
    
    // 添加调试工具
    this.addDebugTools();
  }

  private static listenToLifecycleEvents() {
    const originalOnAddForm = FormExtension.onAddForm;
    FormExtension.onAddForm = function(want) {
      console.debug('卡片添加:', want);
      return originalOnAddForm.call(this, want);
    };

    const originalOnUpdateForm = FormExtension.onUpdateForm;
    FormExtension.onUpdateForm = function(formId) {
      console.debug('卡片更新:', formId);
      return originalOnUpdateForm.call(this, formId);
    };

    const originalOnRemoveForm = FormExtension.onRemoveForm;
    FormExtension.onRemoveForm = function(formId) {
      console.debug('卡片移除:', formId);
      return originalOnRemoveForm.call(this, formId);
    };
  }

  private static addDebugTools() {
    // 添加调试按钮到卡片
    if (typeof window !== 'undefined') {
      const debugButton = document.createElement('button');
      debugButton.textContent = '调试';
      debugButton.style.position = 'fixed';
      debugButton.style.top = '10px';
      debugButton.style.right = '10px';
      debugButton.style.zIndex = '9999';
      debugButton.onclick = () => {
        this.showDebugPanel();
      };
      document.body.appendChild(debugButton);
    }
  }

  private static showDebugPanel() {
    console.group('卡片调试信息');
    console.log('当前时间:', new Date().toLocaleString());
    console.log('卡片尺寸:', window.innerWidth, 'x', window.innerHeight);
    console.log('设备像素比:', window.devicePixelRatio);
    console.groupEnd();
  }
}

export default CardDebugger;

7.2 卡片性能监控

typescript

// ets/utils/CardPerformanceMonitor.ts
class CardPerformanceMonitor {
  private static metrics: Map<string, number> = new Map();
  private static startTimes: Map<string, number> = new Map();

  // 开始测量
  static startMeasure(name: string) {
    this.startTimes.set(name, performance.now());
  }

  // 结束测量
  static endMeasure(name: string) {
    const startTime = this.startTimes.get(name);
    if (startTime) {
      const duration = performance.now() - startTime;
      this.metrics.set(name, duration);
      console.log(`性能测量 [${name}]: ${duration.toFixed(2)}ms`);
    }
  }

  // 获取测量结果
  static getMetrics(): Record<string, number> {
    const result: Record<string, number> = {};
    this.metrics.forEach((value, key) => {
      result[key] = value;
    });
    return result;
  }

  // 监控卡片渲染性能
  static monitorCardRender(cardName: string) {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach((entry) => {
        console.log(`卡片渲染性能 [${cardName}]:`, {
          name: entry.name,
          duration: entry.duration,
          startTime: entry.startTime
        });
      });
    });

    observer.observe({ entryTypes: ['measure'] });
  }
}

export default CardPerformanceMonitor;

八、实战:新闻卡片应用

typescript

// ets/widget/pages/NewsCard/NewsCard.ets
@Entry
@Component
struct NewsCard {
  @State newsList: Array<{
    id: number;
    title: string;
    summary: string;
    source: string;
    time: string;
    image: string;
    read: boolean;
  }> = [];

  @State currentIndex: number = 0;
  @State loading: boolean = true;

  aboutToAppear() {
    this.loadNews();
    
    // 自动轮播
    setInterval(() => {
      this.nextNews();
    }, 10000);
  }

  async loadNews() {
    this.loading = true;
    
    try {
      // 模拟API请求
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      this.newsList = [
        {
          id: 1,
          title: '鸿蒙4.0正式发布,全面升级',
          summary: '华为正式发布鸿蒙4.0操作系统,带来全新体验...',
          source: '华为官方',
          time: '2小时前',
          image: 'news1.jpg',
          read: false
        },
        // ... 更多新闻
      ];
    } catch (error) {
      console.error('加载新闻失败:', error);
    } finally {
      this.loading = false;
    }
  }

  nextNews() {
    if (this.newsList.length > 0) {
      this.currentIndex = (this.currentIndex + 1) % this.newsList.length;
    }
  }

  prevNews() {
    if (this.newsList.length > 0) {
      this.currentIndex = (this.currentIndex - 1 + this.newsList.length) % this.newsList.length;
    }
  }

  markAsRead(id: number) {
    this.newsList = this.newsList.map(news =>
      news.id === id ? { ...news, read: true } : news
    );
  }

  build() {
    Column({ space: 16 }) {
      // 标题和指示器
      Row({ space: 8 }) {
        Text('新闻快讯')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        if (this.newsList.length > 0) {
          Text(`${this.currentIndex + 1}/${this.newsList.length}`)
            .fontSize(12)
            .fontColor('#666666')
        }
      }
      .width('100%')

      // 新闻内容
      if (this.loading) {
        LoadingProgress()
          .width(40)
          .height(40)
      } else if (this.newsList.length === 0) {
        Text('暂无新闻')
          .fontSize(14)
          .fontColor('#999999')
      } else {
        const news = this.newsList[this.currentIndex];
        
        Column({ space: 12 }) {
          // 新闻标题
          Text(news.title)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor(news.read ? '#666666' : '#333333')
            .maxLines(2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          // 新闻摘要
          Text(news.summary)
            .fontSize(14)
            .fontColor('#666666')
            .maxLines(2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          // 新闻元信息
          Row({ space: 16 }) {
            Text(news.source)
              .fontSize(12)
              .fontColor('#999999')

            Text(news.time)
              .fontSize(12)
              .fontColor('#999999')

            Blank()

            Text(news.read ? '已读' : '未读')
              .fontSize(12)
              .fontColor(news.read ? '#34C759' : '#FF9500')
          }
          .width('100%')

          // 导航按钮
          Row({ space: 12 }) {
            Button('上一篇')
              .width('50%')
              .height(36)
              .fontSize(12)
              .backgroundColor('#F5F5F5')
              .fontColor('#333333')
              .enabled(this.currentIndex > 0)
              .onClick(() => {
                this.prevNews();
              })

            Button('下一篇')
              .width('50%')
              .height(36)
              .fontSize(12)
              .backgroundColor('#007DFF')
              .fontColor('#FFFFFF')
              .onClick(() => {
                this.nextNews();
                this.markAsRead(news.id);
              })
          }
          .width('100%')
        }
        .width('100%')
      }
    }
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
    .shadow({ radius: 12, color: '#00000010' })
    .onClick(() => {
      if (this.newsList.length > 0) {
        const news = this.newsList[this.currentIndex];
        this.openNewsDetail(news.id);
      }
    })
  }

  openNewsDetail(id: number) {
    // 打开新闻详情
    console.log('打开新闻详情:', id);
  }
}

九、卡片发布与配置

9.1 卡片发布配置

json

// entry/build-profile.json5
{
  "apiType": 'stageMode',
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": ""
    }
  },
  "targets": [
    {
      "name": "default",
      "runtimeOS": "HarmonyOS"
    }
  ],
  "products": [
    {
      "name": "default",
      "signingConfig": "default",
      "compatibleSdkVersion": "4.0.0(11)",
      "runtimeOS": "HarmonyOS",
      "formConfigs": [
        {
          "name": "WeatherCard",
          "description": "天气服务卡片",
          "src": "./ets/widget/pages/WeatherCard/WeatherCard.ets",
          "window": {
            "designWidth": 200
          },
          "colorMode": "auto",
          "isDefault": true,
          "updateEnabled": true,
          "scheduledUpdateTime": "10:30",
          "updateDuration": 1,
          "defaultDimension": "2*2",
          "supportDimensions": ["2*2", "2*4"]
        }
      ]
    }
  ]
}

9.2 卡片安装与部署

bash

# 1. 编译卡片应用
./gradlew assembleRelease

# 2. 安装应用到设备
hdc install entry-default-signed.hap

# 3. 查看卡片列表
hdc shell aa dump -a | grep forms

# 4. 强制刷新卡片
hdc shell aa force-stop com.example.weatherapp

十、总结与下期预告

10.1 本文要点回顾

  1. 卡片基础:服务卡片的概念、优势和使用场景

  2. 卡片创建:从零开始创建各种类型的服务卡片

  3. 数据管理:卡片数据的持久化和更新机制

  4. 交互设计:卡片的事件处理和用户交互

  5. 样式动画:卡片的样式美化和动画效果

  6. 实战案例:天气卡片、待办卡片、新闻卡片等完整实现

10.2 下期预告:《鸿蒙开发之:多端协同与流转》

下篇文章将深入讲解:

  • 分布式软总线的概念和原理

  • 设备发现和连接管理

  • 跨设备数据同步

  • 应用流转和协同工作

  • 实战:构建多端协同应用


动手挑战

任务1:创建日历卡片
要求:

  • 显示当前月份和日期

  • 支持查看前一天/后一天

  • 显示日程安排

  • 支持添加新日程

任务2:创建音乐控制卡片
要求:

  • 显示当前播放歌曲信息

  • 支持播放/暂停、上一首/下一首

  • 显示播放进度条

  • 支持音量控制

任务3:创建智能家居控制卡片
要求:

  • 显示设备状态(开关、温度等)

  • 支持远程控制设备

  • 显示能耗统计

  • 支持场景模式切换

将你的代码分享到评论区,我会挑选优秀实现进行详细点评!


常见问题解答

Q:卡片可以动态调整大小吗?
A:是的,可以在卡片配置文件中指定支持多种尺寸,用户可以在桌面上调整卡片大小。

Q:卡片更新频率有限制吗?
A:是的,为了避免耗电,卡片更新频率有限制。建议使用定时更新或事件驱动更新。

Q:卡片可以调用系统能力吗?
A:可以,但需要申请相应权限。卡片可以调用网络、存储、定位等系统能力。

Q:卡片支持深色模式吗?
A:支持,可以在卡片配置中设置colorMode为auto,系统会自动适配深色模式。

加入班级可学习及考试鸿蒙开发者认证啦!班级链接:https://developer.huawei.com/consumer/cn/training/classDetail/0a4ab47fc3ae4a25b6f8c69e08229470?type=1?ha_source=hmosclass&ha_sourceId=89000248


版权声明:本文为《鸿蒙开发系列》第7篇,原创文章,转载请注明出处。

标签:#HarmonyOS #鸿蒙开发 #服务卡片 #ArkUI #桌面组件 #华为开发者

Logo

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

更多推荐