鸿蒙开发之:服务卡片开发实战

4 阅读10分钟

本文字数:约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中创建服务卡片:

右键点击项目 → New → Service Widget

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

配置卡片信息:

卡片名称: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": "22", "supportDimensions": ["22", "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 { 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:weatherwidgetdesc","src":"./ets/widget/pages/WeatherWidget/WeatherWidget.ets","window":"designWidth":200,"autoDesignWidth":false,"colorMode":"auto","isDefault":true,"updateEnabled":true,"scheduledUpdateTime":"08:00","updateDuration":1,"defaultDimension":"22","supportDimensions":["12","22","24","44"],"landscapeLayouts":["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

运行

<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>
七、卡片测试与调试 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": "22", "supportDimensions": ["22", "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 本文要点回顾 卡片基础:服务卡片的概念、优势和使用场景

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

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

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

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

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

10.2 下期预告:《鸿蒙开发之:多端协同与流转》 下篇文章将深入讲解:

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

设备发现和连接管理

跨设备数据同步

应用流转和协同工作

实战:构建多端协同应用

动手挑战 任务1:创建日历卡片 要求:

显示当前月份和日期

支持查看前一天/后一天

显示日程安排

支持添加新日程

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

显示当前播放歌曲信息

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

显示播放进度条

支持音量控制

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

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

支持远程控制设备

显示能耗统计

支持场景模式切换

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

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

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

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

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

加入班级可学习及考试鸿蒙开发者认证啦!班级链接:developer.huawei.com/consumer/cn… ————————————————