Harmony开发之服务卡片开发——解锁原子化服务
Harmony开发之服务卡片开发——解锁原子化服务
引入:桌面卡片的便捷交互
当我们使用手机时,经常会发现一些应用在桌面上提供了小巧精致的卡片,比如天气卡片显示实时温度、运动卡片展示今日步数、音乐卡片提供播放控制。这些就是HarmonyOS的服务卡片(Service Widget),它们无需打开完整应用就能提供核心信息并支持快捷操作,极大地提升了用户体验和操作效率。
一、服务卡片核心概念
1.1 什么是服务卡片?
服务卡片是HarmonyOS原子化服务的一种呈现形式,是界面展示的控件。它作为应用的重要入口,通过在外围提供快捷访问特定功能的能力,实现应用功能的原子化。
1.2 服务卡片的优势
- 免安装使用:用户无需安装完整应用即可使用核心功能
- 即用即走:轻量化设计,快速响应
- 多设备协同:卡片可在手机、平板、手表等多设备间流转
- 动态更新:支持定时更新或数据驱动更新
1.3 卡片类型与规格
HarmonyOS支持多种规格的服务卡片,常见的有2x2、2x4、4x4等不同尺寸,开发者需要根据功能需求选择合适的卡片规格。
二、卡片创建与配置
2.1 创建卡片工程
在DevEco Studio中创建服务卡片项目时,选择"Service Widget"模板:
// 文件:entry/src/main/ets/entryability/EntryAbility.ets
import { UIAbility } from '@ohos.app.ability.UIAbility';
import { widgetManager } from '@ohos.app.ability.widgetManager';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('EntryAbility onCreate');
}
// 注册卡片更新回调
onAddForm(want: Want): FormBindingData {
let formData: Record<string, Object> = {
'title': '健康步数',
'steps': '8,256',
'target': '10,000',
'progress': 82
};
return new FormBindingData(JSON.stringify(formData));
}
}
2.2 卡片配置文件
// 文件:entry/src/main/resources/base/profile/form_config.json
{
"forms": [
{
"name": "widget_steps",
"description": "健康步数卡片",
"src": "./ets/widgets/StepsWidget.ets",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:00",
"updateDuration": 1,
"defaultDimension": "2 * 2",
"supportDimensions": ["2 * 2", "2 * 4"]
}
]
}
// 文件:entry/src/main/module.json5
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"formsEnabled": true,
"forms": [
{
"name": "widget_steps",
"description": "健康步数卡片",
"src": "./ets/widgets/StepsWidget.ets",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:00",
"updateDuration": 1,
"defaultDimension": "2 * 2",
"supportDimensions": ["2 * 2", "2 * 4"]
}
]
}
]
}
}
三、卡片布局开发
3.1 基础卡片组件
// 文件:entry/src/main/ets/widgets/StepsWidget.ets
@Entry
@Component
struct StepsWidget {
@State steps: string = '0'
@State progress: number = 0
build() {
Column() {
// 标题栏
Row() {
Image($r('app.media.ic_footprint'))
.width(20)
.height(20)
.margin({ right: 8 })
Text('今日步数')
.fontSize(16)
.fontColor('#000000')
.fontWeight(FontWeight.Medium)
Blank()
Image($r('app.media.ic_refresh'))
.width(16)
.height(16)
.onClick(() => {
this.updateStepsData()
})
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
// 进度区域
Column() {
Text(this.steps)
.fontSize(24)
.fontColor('#007DFF')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 4 })
Text('目标: 10,000步')
.fontSize(12)
.fontColor('#99000000')
.margin({ bottom: 8 })
// 进度条
Stack() {
// 背景条
Row()
.width('100%')
.height(6)
.backgroundColor('#20007DFF')
.borderRadius(3)
// 进度条
Row()
.width(`${this.progress}%`)
.height(6)
.backgroundColor('#007DFF')
.borderRadius(3)
}
.width('100%')
.height(6)
Text(`${this.progress}%完成`)
.fontSize(10)
.fontColor('#99000000')
.margin({ top: 4 })
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 12 })
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
// 更新步数数据
updateStepsData() {
// 模拟数据更新
const newSteps = Math.floor(Math.random() * 10000).toLocaleString()
const newProgress = Math.floor(Math.random() * 100)
this.steps = newSteps
this.progress = newProgress
}
}
3.2 交互式卡片
// 文件:entry/src/main/ets/widgets/MusicWidget.ets
@Entry
@Component
struct MusicWidget {
@State isPlaying: boolean = false
@State currentSong: string = 'HarmonyOS主题曲'
@State artist: string = '华为音乐'
@State progress: number = 40
build() {
Column() {
// 歌曲信息
Row() {
Image($r('app.media.ic_music_cover'))
.width(40)
.height(40)
.borderRadius(8)
.margin({ right: 12 })
Column() {
Text(this.currentSong)
.fontSize(14)
.fontColor('#000000')
.fontWeight(FontWeight.Medium)
.margin({ bottom: 2 })
Text(this.artist)
.fontSize(12)
.fontColor('#99000000')
}
.alignItems(HorizontalAlign.Start)
Blank()
}
.width('100%')
.padding({ left: 12, right: 12, top: 12 })
// 控制按钮
Row() {
Image($r('app.media.ic_skip_previous'))
.width(24)
.height(24)
.onClick(() => this.previousSong())
Blank()
.width(20)
Image(this.isPlaying ?
$r('app.media.ic_pause') :
$r('app.media.ic_play'))
.width(32)
.height(32)
.onClick(() => this.togglePlay())
Blank()
.width(20)
Image($r('app.media.ic_skip_next'))
.width(24)
.height(24)
.onClick(() => this.nextSong())
}
.width('100%')
.padding({ bottom: 12 })
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
togglePlay() {
this.isPlaying = !this.isPlaying
// 实际开发中这里应该控制音乐播放
}
previousSong() {
// 上一首逻辑
}
nextSong() {
// 下一首逻辑
}
}
四、卡片更新机制
4.1 定时更新配置
{
"forms": [
{
"name": "weather_widget",
"updateEnabled": true,
"scheduledUpdateTime": "08:00",
"updateDuration": 1,
"supportDimensions": ["2 * 2"]
}
]
}
4.2 手动更新实现
// 文件:entry/src/main/ets/utils/WidgetUtils.ets
import { formHost } from '@ohos.app.ability.formHost';
export class WidgetUtils {
// 更新指定卡片
static async updateWidget(formId: string): Promise<void> {
try {
const formBindingData = {
'temperature': '25°C',
'weather': '晴朗',
'location': '深圳市',
'updateTime': new Date().toLocaleTimeString()
};
await formHost.updateForm(formId, {
data: JSON.stringify(formBindingData)
});
} catch (error) {
console.error('更新卡片失败:', error);
}
}
// 获取所有卡片ID
static async getAllFormIds(): Promise<string[]> {
try {
const formInfos = await formHost.getAllFormsInfo();
return formInfos.map(info => info.formId);
} catch (error) {
console.error('获取卡片信息失败:', error);
return [];
}
}
}
4.3 数据驱动更新
// 文件:entry/src/main/ets/widgets/WeatherWidget.ets
@Entry
@Component
struct WeatherWidget {
@State temperature: string = '--'
@State weather: string = '加载中...'
@State updateTime: string = ''
aboutToAppear() {
this.fetchWeatherData()
}
build() {
Column() {
Text(this.temperature)
.fontSize(28)
.fontColor('#007DFF')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 4 })
Text(this.weather)
.fontSize(14)
.fontColor('#666666')
.margin({ bottom: 8 })
Text(`更新: ${this.updateTime}`)
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
async fetchWeatherData() {
try {
// 模拟API调用
const response = await this.mockWeatherAPI()
this.temperature = response.temperature
this.weather = response.weather
this.updateTime = new Date().toLocaleTimeString()
} catch (error) {
console.error('获取天气数据失败:', error)
}
}
async mockWeatherAPI() {
return new Promise<{temperature: string, weather: string}>((resolve) => {
setTimeout(() => {
resolve({
temperature: '25°C',
weather: '晴朗'
})
}, 1000)
})
}
}
五、卡片跳转与交互
5.1 卡片跳转配置
// 文件:entry/src/main/ets/widgets/NewsWidget.ets
@Entry
@Component
struct NewsWidget {
@State newsItems: Array<{id: string, title: string, time: string}> = [
{id: '1', title: 'HarmonyOS 4.0发布新特性', time: '10:30'},
{id: '2', title: '开发者大会即将举行', time: '09:15'},
{id: '3', title: '新版本开发工具更新', time: '昨天'}
]
build() {
Column() {
Text('最新资讯')
.fontSize(16)
.fontColor('#000000')
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8, left: 12, top: 12 })
List({ space: 4 }) {
ForEach(this.newsItems, (item: {id: string, title: string, time: string}) => {
ListItem() {
Row() {
Column() {
Text(item.title)
.fontSize(12)
.fontColor('#000000')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.time)
.fontSize(10)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_arrow_right'))
.width(12)
.height(12)
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.onClick(() => {
this.navigateToDetail(item.id)
})
}
}, (item: {id: string, title: string, time: string}) => item.id)
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
navigateToDetail(newsId: string) {
// 跳转到详情页
let want = {
deviceId: '', // 默认设备
bundleName: 'com.example.newsapp',
abilityName: 'NewsDetailAbility',
parameters: {
newsId: newsId
}
};
// 使用featureAbility进行跳转
featureAbility.startAbility({ want: want })
.then(() => {
console.info('跳转成功');
})
.catch((error) => {
console.error('跳转失败:', error);
});
}
}
5.2 原子化服务配置
// 文件:entry/src/main/module.json5
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"formsEnabled": true,
"forms": [
{
"name": "news_widget",
"description": "资讯卡片",
"src": "./ets/widgets/NewsWidget.ets",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "09:00",
"defaultDimension": "2 * 4",
"supportDimensions": ["2 * 4"]
}
],
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
}
],
"distro": {
"deliveryWithInstall": true,
"moduleName": "entry",
"moduleType": "entry"
},
"reqCapabilities": []
}
}
六、多设备适配与流转
6.1 响应式布局设计
// 文件:entry/src/main/ets/widgets/UniversalWidget.ets
@Entry
@Component
struct UniversalWidget {
@State deviceType: string = 'phone'
aboutToAppear() {
this.detectDeviceType()
}
build() {
Column() {
if (this.deviceType === 'watch') {
this.buildWatchLayout()
} else if (this.deviceType === 'tablet') {
this.buildTabletLayout()
} else {
this.buildPhoneLayout()
}
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
@Builder
buildPhoneLayout() {
Column() {
Text('手机版卡片')
.fontSize(16)
.fontColor('#000000')
// 手机专用布局...
}
.padding(8)
}
@Builder
buildTabletLayout() {
Column() {
Text('平板版卡片')
.fontSize(20)
.fontColor('#000000')
// 平板专用布局...
}
.padding(16)
}
@Builder
buildWatchLayout() {
Column() {
Text('手表版卡片')
.fontSize(12)
.fontColor('#000000')
// 手表专用布局...
}
.padding(4)
}
detectDeviceType() {
// 根据屏幕尺寸判断设备类型
const screenWidth = vp2px(display.getDefaultDisplaySync().width);
if (screenWidth < 600) {
this.deviceType = 'watch';
} else if (screenWidth < 1200) {
this.deviceType = 'phone';
} else {
this.deviceType = 'tablet';
}
}
}
6.2 卡片流转处理
// 文件:entry/src/main/ets/entryability/EntryAbility.ets
import { distributedDeviceManager } from '@ohos.distributedDeviceManager';
export default class EntryAbility extends UIAbility {
onConnect(want: Want): formBindingData.FormBindingData {
// 处理卡片流转连接
console.info('卡片流转连接建立');
return this.handleFormRequest(want);
}
onDisconnect(want: Want): void {
// 处理卡片流转断开
console.info('卡片流转连接断开');
}
// 处理跨设备卡片请求
handleFormRequest(want: Want): formBindingData.FormBindingData {
const deviceId = want.parameters?.deviceId as string;
const formId = want.parameters?.formId as string;
console.info(`处理来自设备 ${deviceId} 的卡片请求`);
// 根据设备能力返回不同的卡片数据
return this.getAdaptiveFormData(deviceId);
}
getAdaptiveFormData(deviceId: string): formBindingData.FormBindingData {
// 获取设备信息并返回适配的数据
const deviceInfo = this.getDeviceInfo(deviceId);
let formData: Record<string, Object> = {};
if (deviceInfo.deviceType === 'watch') {
formData = this.getWatchFormData();
} else {
formData = this.getDefaultFormData();
}
return new formBindingData.FormBindingData(JSON.stringify(formData));
}
}
七、调试与优化
7.1 卡片调试技巧
// 文件:entry/src/main/ets/utils/DebugUtils.ets
export class DebugUtils {
// 卡片性能监控
static startPerformanceMonitor(formId: string): void {
const startTime = new Date().getTime();
// 监控卡片加载性能
setTimeout(() => {
const loadTime = new Date().getTime() - startTime;
console.info(`卡片 ${formId} 加载耗时: ${loadTime}ms`);
if (loadTime > 1000) {
console.warn('卡片加载时间过长,建议优化');
}
}, 0);
}
// 内存使用检查
static checkMemoryUsage(): void {
const memoryInfo = process.getMemoryInfo();
console.info(`内存使用情况: ${JSON.stringify(memoryInfo)}`);
if (memoryInfo.availMem < 100 * 1024 * 1024) { // 100MB
console.warn('可用内存不足,可能影响卡片性能');
}
}
}
7.2 性能优化建议
- 图片资源优化:使用适当尺寸的图片,避免过大资源
- 数据缓存:合理使用缓存减少网络请求
- 布局简化:避免过于复杂的布局层次
- 按需更新:只在必要时更新卡片内容
总结
服务卡片是HarmonyOS原子化服务的核心载体,通过提供轻量级、即用即走的用户体验,极大地增强了应用的便捷性和实用性。本文从卡片创建、布局开发、更新机制、交互跳转到多设备适配等方面全面介绍了服务卡片的开发流程。
行动建议:
- 根据功能需求合理选择卡片尺寸和更新策略
- 注重卡片的视觉设计和用户体验
- 实现多设备适配,确保在不同设备上都有良好表现
- 优化卡片性能,确保快速加载和流畅交互
- 充分利用原子化服务的优势,提供免安装使用体验
通过精心设计的服务卡片,可以为用户提供更加便捷高效的服务入口,增强应用的用户粘性和使用价值。

浙公网安备 33010602011771号