鸿蒙开发之:服务卡片开发实战
服务卡片(Service Widget)是鸿蒙应用的一种重要形态,它允许用户在不打开应用的情况下,直接在桌面上查看应用的关键信息和快速操作。服务卡片可以展示天气、待办事项、新闻摘要等信息,并支持交互操作。卡片基础:服务卡片的概念、优势和使用场景卡片创建:从零开始创建各种类型的服务卡片数据管理:卡片数据的持久化和更新机制交互设计:卡片的事件处理和用户交互样式动画:卡片的样式美化和动画效果实战案例:天
本文字数:约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": "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 本文要点回顾
-
卡片基础:服务卡片的概念、优势和使用场景
-
卡片创建:从零开始创建各种类型的服务卡片
-
数据管理:卡片数据的持久化和更新机制
-
交互设计:卡片的事件处理和用户交互
-
样式动画:卡片的样式美化和动画效果
-
实战案例:天气卡片、待办卡片、新闻卡片等完整实现
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 #桌面组件 #华为开发者
更多推荐


所有评论(0)