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原子化服務的核心載體,通過提供輕量級、即用即走的用户體驗,極大地增強了應用的便捷性和實用性。本文從卡片創建、佈局開發、更新機制、交互跳轉到多設備適配等方面全面介紹了服務卡片的開發流程。
行動建議:
- 根據功能需求合理選擇卡片尺寸和更新策略
- 注重卡片的視覺設計和用户體驗
- 實現多設備適配,確保在不同設備上都有良好表現
- 優化卡片性能,確保快速加載和流暢交互
- 充分利用原子化服務的優勢,提供免安裝使用體驗
通過精心設計的服務卡片,可以為用户提供更加便捷高效的服務入口,增強應用的用户粘性和使用價值。