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 性能优化建议

  1. 图片资源优化:使用适当尺寸的图片,避免过大资源
  2. 数据缓存:合理使用缓存减少网络请求
  3. 布局简化:避免过于复杂的布局层次
  4. 按需更新:只在必要时更新卡片内容

总结

服务卡片是HarmonyOS原子化服务的核心载体,通过提供轻量级、即用即走的用户体验,极大地增强了应用的便捷性和实用性。本文从卡片创建、布局开发、更新机制、交互跳转到多设备适配等方面全面介绍了服务卡片的开发流程。

行动建议

  • 根据功能需求合理选择卡片尺寸和更新策略
  • 注重卡片的视觉设计和用户体验
  • 实现多设备适配,确保在不同设备上都有良好表现
  • 优化卡片性能,确保快速加载和流畅交互
  • 充分利用原子化服务的优势,提供免安装使用体验

通过精心设计的服务卡片,可以为用户提供更加便捷高效的服务入口,增强应用的用户粘性和使用价值。


posted @ 2025-12-24 10:38  wrystart  阅读(0)  评论(0)    收藏  举报