本文字数:约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": "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… ————————————————