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應用開發的重要環節,通過響應式佈局、資源限定符、條件編譯和能力檢測等技術,可以實現一次開發、多端部署的目標。建議開發者在設計階段就考慮多設備適配,採用模塊化的架構設計,便於後續維護和擴展。
關鍵要點:
- 使用斷點系統實現響應式佈局
- 為不同設備提供差異化資源
- 運行時檢測設備能力,動態調整功能
- 針對摺疊屏等特殊設備做特殊適配
- 遵循漸進增強和優雅降級原則
通過合理的多設備適配策略,可以顯著提升應用的用户體驗和市場競爭力。