Harmony學習之多設備適配

一、場景引入

小明開發的新聞閲讀應用需要在手機、平板、智能手錶等多種設備上運行,但不同設備的屏幕尺寸、交互方式、硬件能力差異很大。如何讓應用在不同設備上都能提供良好的用户體驗,成為開發中的重要挑戰。HarmonyOS提供了完善的多設備適配方案,幫助開發者構建一次開發、多端部署的應用。

二、多設備適配核心概念

1. 設備類型與特性

設備類型

屏幕尺寸

交互方式

典型使用場景

手機

5-7英寸

觸摸、手勢

移動閲讀、快速瀏覽

平板

8-13英寸

觸摸、鍵盤

深度閲讀、多任務

智能手錶

1-2英寸

觸摸、旋鈕

通知提醒、快速查看

智慧屏

55-85英寸

遙控器、語音

家庭娛樂、大屏閲讀

車機

10-15英寸

觸摸、語音

車載信息、導航輔助

2. 適配策略

  • 響應式佈局:根據屏幕尺寸動態調整UI
  • 資源限定符:為不同設備提供不同的資源文件
  • 條件編譯:根據設備類型編譯不同的代碼邏輯
  • 能力檢測:運行時判斷設備能力,動態調整功能

三、響應式佈局實踐

1. 斷點系統

// entry/src/main/ets/utils/DeviceUtils.ets
import window from '@ohos.window';

export class DeviceUtils {
  // 設備斷點定義
  static readonly BREAKPOINTS = {
    SMALL: 600,    // 手機
    MEDIUM: 840,   // 小屏平板
    LARGE: 1200,   // 大屏平板/摺疊屏
    XLARGE: 1600   // 智慧屏
  };

  // 獲取屏幕寬度
  static async getScreenWidth(): Promise<number> {
    try {
      const windowClass = await window.getLastWindow(this.context);
      const windowSize = await windowClass.getWindowProperties();
      return windowSize.windowRect.width;
    } catch (error) {
      console.error('獲取屏幕寬度失敗:', error);
      return 360; // 默認手機寬度
    }
  }

  // 判斷設備類型
  static async getDeviceType(): Promise<string> {
    const width = await this.getScreenWidth();
    
    if (width < this.BREAKPOINTS.SMALL) {
      return 'phone';
    } else if (width < this.BREAKPOINTS.MEDIUM) {
      return 'smallTablet';
    } else if (width < this.BREAKPOINTS.LARGE) {
      return 'tablet';
    } else {
      return 'largeScreen';
    }
  }

  // 判斷是否為摺疊屏展開狀態
  static async isFoldableExpanded(): Promise<boolean> {
    const width = await this.getScreenWidth();
    return width >= this.BREAKPOINTS.MEDIUM;
  }

  // 獲取屏幕方向
  static async getOrientation(): Promise<'portrait' | 'landscape'> {
    try {
      const windowClass = await window.getLastWindow(this.context);
      const windowSize = await windowClass.getWindowProperties();
      return windowSize.windowRect.width > windowSize.windowRect.height ? 'landscape' : 'portrait';
    } catch (error) {
      console.error('獲取屏幕方向失敗:', error);
      return 'portrait';
    }
  }
}

2. 響應式組件

// entry/src/main/ets/components/ResponsiveLayout.ets
@Component
export struct ResponsiveLayout {
  @State private deviceType: string = 'phone';
  @State private orientation: string = 'portrait';

  aboutToAppear() {
    this.updateDeviceInfo();
    
    // 監聽屏幕變化
    window.on('windowSizeChange', () => {
      this.updateDeviceInfo();
    });
  }

  async updateDeviceInfo() {
    this.deviceType = await DeviceUtils.getDeviceType();
    this.orientation = await DeviceUtils.getOrientation();
  }

  @Builder
  buildContent() {
    if (this.deviceType === 'phone') {
      this.buildPhoneLayout();
    } else if (this.deviceType === 'tablet' || this.deviceType === 'largeScreen') {
      this.buildTabletLayout();
    } else {
      this.buildSmallTabletLayout();
    }
  }

  @Builder
  buildPhoneLayout() {
    Column() {
      // 手機佈局:單列
      this.buildNewsList();
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildTabletLayout() {
    Row() {
      // 平板佈局:雙欄
      Column() {
        this.buildCategoryList();
      }
      .width('30%')
      
      Column() {
        this.buildNewsList();
      }
      .width('70%')
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildSmallTabletLayout() {
    if (this.orientation === 'portrait') {
      this.buildPhoneLayout();
    } else {
      this.buildTabletLayout();
    }
  }

  build() {
    this.buildContent();
  }
}

四、資源限定符適配

1. 資源目錄結構

resources/
├── base/
│   ├── element/
│   ├── media/
│   └── profile/
├── phone/
│   ├── element/
│   └── media/
├── tablet/
│   ├── element/
│   └── media/
└── wearable/
    ├── element/
    └── media/

2. 資源文件配置

// resources/base/element/string.json
{
  "strings": [
    {
      "name": "app_name",
      "value": "新聞閲讀器"
    },
    {
      "name": "news_title",
      "value": "新聞列表"
    }
  ]
}

// resources/tablet/element/string.json
{
  "strings": [
    {
      "name": "news_title",
      "value": "新聞列表 - 平板版"
    }
  ]
}

// resources/wearable/element/string.json
{
  "strings": [
    {
      "name": "app_name",
      "value": "新聞"
    },
    {
      "name": "news_title",
      "value": "新聞"
    }
  ]
}

3. 佈局文件適配

// resources/base/element/float.json
{
  "float": [
    {
      "name": "news_item_width",
      "value": "100%"
    }
  ]
}

// resources/tablet/element/float.json
{
  "float": [
    {
      "name": "news_item_width",
      "value": "50%"
    }
  ]
}

// resources/wearable/element/float.json
{
  "float": [
    {
      "name": "news_item_width",
      "value": "100%"
    }
  ]
}

五、條件編譯與能力檢測

1. 條件編譯配置

// module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": [
      "default",
      "tablet",
      "wearable"
    ],
    "buildOption": {
      "buildMode": "release",
      "deviceType": "default"
    }
  }
}

2. 運行時能力檢測

// entry/src/main/ets/utils/CapabilityDetector.ets
import systemParameter from '@ohos.systemParameter';

export class CapabilityDetector {
  // 檢測設備類型
  static async getDeviceType(): Promise<string> {
    try {
      const deviceType = await systemParameter.getSync('const.product.ohos.device.type');
      return deviceType as string;
    } catch (error) {
      console.error('獲取設備類型失敗:', error);
      return 'phone';
    }
  }

  // 檢測是否支持特定功能
  static async hasCapability(capability: string): Promise<boolean> {
    try {
      const capabilities = await systemParameter.getSync('const.product.ohos.capabilities');
      return capabilities.includes(capability);
    } catch (error) {
      console.error('檢測能力失敗:', error);
      return false;
    }
  }

  // 檢測屏幕密度
  static async getScreenDensity(): Promise<number> {
    try {
      const density = await systemParameter.getSync('const.product.ohos.screen.density');
      return parseFloat(density as string);
    } catch (error) {
      console.error('獲取屏幕密度失敗:', error);
      return 2.0;
    }
  }

  // 檢測是否支持觸摸
  static async hasTouch(): Promise<boolean> {
    return await this.hasCapability('touch');
  }

  // 檢測是否支持語音輸入
  static async hasVoiceInput(): Promise<boolean> {
    return await this.hasCapability('voice_input');
  }

  // 檢測是否支持攝像頭
  static async hasCamera(): Promise<boolean> {
    return await this.hasCapability('camera');
  }
}

3. 動態功能適配

// entry/src/main/ets/pages/NewsDetailPage.ets
@Component
struct NewsDetailPage {
  @State private hasCamera: boolean = false;
  @State private hasVoiceInput: boolean = false;

  aboutToAppear() {
    this.checkCapabilities();
  }

  async checkCapabilities() {
    this.hasCamera = await CapabilityDetector.hasCamera();
    this.hasVoiceInput = await CapabilityDetector.hasVoiceInput();
  }

  build() {
    Column() {
      // 新聞內容
      this.buildNewsContent();
      
      // 根據能力顯示不同功能
      if (this.hasCamera) {
        this.buildCameraButton();
      }
      
      if (this.hasVoiceInput) {
        this.buildVoiceInputButton();
      }
    }
  }
}

六、多設備應用配置

1. 應用配置

// entry/src/main/resources/base/profile/main_pages.json
{
  "src": [
    "pages/HomePage",
    "pages/NewsDetailPage",
    "pages/SettingsPage"
  ]
}

// entry/src/main/resources/tablet/profile/main_pages.json
{
  "src": [
    "pages/HomePage",
    "pages/NewsDetailPage",
    "pages/SettingsPage",
    "pages/TabletHomePage" // 平板專用頁面
  ]
}

// entry/src/main/resources/wearable/profile/main_pages.json
{
  "src": [
    "pages/WearableHomePage", // 手錶專用頁面
    "pages/WearableDetailPage"
  ]
}

2. 能力配置

// entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": [
      "default",
      "tablet",
      "wearable"
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "launchType": "standard",
        "orientation": "unspecified",
        "supportWindowMode": ["fullscreen", "split", "floating"],
        "maxWindowRatio": 3.5,
        "minWindowRatio": 0.5
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "用於網絡請求",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

七、實戰案例:多設備新聞閲讀器

1. 手機端佈局

// entry/src/main/ets/pages/HomePage.ets
@Component
struct HomePage {
  @State private deviceType: string = 'phone';
  @State private orientation: string = 'portrait';

  aboutToAppear() {
    this.updateDeviceInfo();
  }

  async updateDeviceInfo() {
    this.deviceType = await DeviceUtils.getDeviceType();
    this.orientation = await DeviceUtils.getOrientation();
  }

  build() {
    if (this.deviceType === 'phone') {
      this.buildPhoneLayout();
    } else if (this.deviceType === 'tablet') {
      this.buildTabletLayout();
    } else if (this.deviceType === 'wearable') {
      this.buildWearableLayout();
    }
  }

  @Builder
  buildPhoneLayout() {
    Column() {
      // 頂部導航欄
      this.buildHeader();
      
      // 新聞列表
      List({ space: 10 }) {
        ForEach(this.newsList, (item: NewsItem) => {
          ListItem() {
            this.buildNewsItem(item);
          }
        })
      }
      .layoutWeight(1)
    }
  }

  @Builder
  buildTabletLayout() {
    Row() {
      // 左側分類導航
      Column() {
        this.buildCategoryList();
      }
      .width('30%')
      
      // 右側新聞列表
      Column() {
        this.buildNewsList();
      }
      .width('70%')
    }
  }

  @Builder
  buildWearableLayout() {
    Column() {
      // 手錶端簡化佈局
      Text('最新新聞')
        .fontSize(16)
        .margin({ bottom: 10 })
      
      ForEach(this.newsList.slice(0, 3), (item: NewsItem) => {
        this.buildWearableNewsItem(item);
      })
    }
  }
}

2. 摺疊屏適配

// entry/src/main/ets/components/FoldableLayout.ets
@Component
export struct FoldableLayout {
  @State private isExpanded: boolean = false;

  aboutToAppear() {
    this.checkFoldableState();
    
    // 監聽摺疊狀態變化
    window.on('foldStatusChange', (data) => {
      this.isExpanded = data.isFolded;
    });
  }

  async checkFoldableState() {
    this.isExpanded = await DeviceUtils.isFoldableExpanded();
  }

  build() {
    if (this.isExpanded) {
      this.buildExpandedLayout();
    } else {
      this.buildCompactLayout();
    }
  }

  @Builder
  buildExpandedLayout() {
    // 展開狀態:雙欄佈局
    Row() {
      Column() {
        this.buildCategoryList();
      }
      .width('30%')
      
      Column() {
        this.buildNewsList();
      }
      .width('70%')
    }
  }

  @Builder
  buildCompactLayout() {
    // 摺疊狀態:單欄佈局
    Column() {
      this.buildNewsList();
    }
  }
}

八、最佳實踐

1. 適配原則

  • 漸進增強:先保證基礎功能可用,再根據設備能力增強體驗
  • 優雅降級:在不支持某些功能的設備上提供替代方案
  • 一致性:保持不同設備上的操作邏輯一致
  • 性能優先:避免在低性能設備上加載複雜資源

2. 測試策略

  • 在不同設備類型上測試佈局
  • 測試屏幕旋轉和摺疊狀態切換
  • 驗證資源文件是否正確加載
  • 檢查條件編譯是否生效

九、總結

多設備適配是HarmonyOS應用開發的重要環節,通過響應式佈局、資源限定符、條件編譯和能力檢測等技術,可以實現一次開發、多端部署的目標。建議開發者在設計階段就考慮多設備適配,採用模塊化的架構設計,便於後續維護和擴展。

關鍵要點:

  1. 使用斷點系統實現響應式佈局
  2. 為不同設備提供差異化資源
  3. 運行時檢測設備能力,動態調整功能
  4. 針對摺疊屏等特殊設備做特殊適配
  5. 遵循漸進增強和優雅降級原則

通過合理的多設備適配策略,可以顯著提升應用的用户體驗和市場競爭力。