11  第10章:多端适配

11.1 10.1 多设备形态概述

11.1.1 10.1.1 HarmonyOS支持的设备类型

HarmonyOS作为分布式操作系统,支持多种设备形态,包括:

  • 智能手机:主流设备,屏幕尺寸6.0-7.2英寸,支持触摸交互
  • 平板电脑:大屏设备,屏幕尺寸8-12.9英寸,支持手写笔和键盘
  • 折叠屏设备:可折叠屏幕,支持展开/折叠两种形态,屏幕尺寸7.6-8英寸(展开状态)
  • 智慧屏(电视):超大屏幕,屏幕尺寸55-98英寸,遥控器交互
  • 穿戴设备:小屏幕设备,屏幕尺寸1.3-1.9英寸,圆形或方形屏幕
  • 车机设备:车载信息娱乐系统,屏幕尺寸8-17英寸,触摸和语音交互
  • 智慧屏音箱:中等屏幕,带摄像头和扬声器

11.1.2 10.1.2 设备能力差异分析

不同设备在以下方面存在显著差异:

屏幕尺寸与分辨率

// 设备屏幕信息获取
import device from '@ohos.deviceInfo';
import display from '@ohos.display';

// 获取默认显示信息
let defaultDisplay = display.getDefaultDisplaySync();
console.log(`屏幕宽度: ${defaultDisplay.width}`);
console.log(`屏幕高度: ${defaultDisplay.height}`);
console.log(`屏幕密度: ${defaultDisplay.densityDPI}`);

输入方式差异 - 手机/平板:触摸屏、手写笔(平板) - 智慧屏:遥控器、语音 - 穿戴:触摸屏、物理按键、语音 - 车机:触摸屏、语音、方向盘控制

性能约束差异 - 手机/平板:高性能处理器,充足内存 - 穿戴设备:低功耗处理器,有限内存(通常512MB-1GB) - 智慧屏:中等性能,注重图形渲染

11.1.3 10.1.3 自适应设计原则

原则1:内容优先 - 根据屏幕尺寸调整内容展示方式 - 小屏幕:单列布局,突出核心内容 - 大屏幕:多列布局,充分利用空间

原则2:交互自然 - 适配不同输入方式 - 触摸设备:大按钮、手势操作 - 遥控设备:方向键导航、确认键

原则3:性能优化 - 根据设备性能调整资源加载 - 低端设备:简化动画、降低图片质量 - 高端设备:丰富动效、高清资源

原则4:一致性体验 - 保持核心功能和品牌一致性 - 根据设备特点优化交互细节

11.1.4 10.1.4 一次开发多端部署策略

HarmonyOS提供多种策略实现一次开发多端部署:

策略1:响应式布局

// 使用GridRow/GridColumn实现响应式网格
@Entry
@Component
struct ResponsiveGrid {
  build() {
    GridRow({ columns: 12 }) {
      GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
        Text('内容1')
          .width('100%')
          .height(100)
          .backgroundColor('#FF6B6B')
      }
      GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
        Text('内容2')
          .width('100%')
          .height(100)
          .backgroundColor('#4ECDC4')
      }
      GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
        Text('内容3')
          .width('100%')
          .height(100)
          .backgroundColor('#45B7D1')
      }
      GridCol({ span: { xs: 12, sm: 6, md: 12, lg: 3 } }) {
        Text('内容4')
          .width('100%')
          .height(100)
          .backgroundColor('#FFA07A')
      }
    }
  }
}

策略2:资源限定词

// 资源目录结构
resources/
├── base/              # 默认资源
│   ├── element/
│   └── media/
├── en_US/             # 英文资源
├── zh_CN/             # 中文资源
├── phone/             # 手机资源
├── tablet/            # 平板资源
├── wearable/          # 穿戴设备资源
├── tv/                # 智慧屏资源
├── car/               # 车机资源
├── foldable/          # 折叠屏资源
└── dark/              # 深色模式资源

策略3:设备类型检测

import device from '@ohos.deviceInfo';

// 检测设备类型
function getDeviceType(): string {
  // 通过屏幕尺寸判断
  let display = display.getDefaultDisplaySync();
  let screenWidth = display.width / display.densityDPI;
  
  if (screenWidth < 3) {
    return 'wearable';
  } else if (screenWidth < 6) {
    return 'phone';
  } else if (screenWidth < 9) {
    return 'tablet';
  } else if (screenWidth < 12) {
    return 'foldable';
  } else {
    return 'tv';
  }
}

// 根据设备类型加载不同UI
@Entry
@Component
struct AdaptiveUI {
  @State deviceType: string = 'phone';
  
  aboutToAppear() {
    this.deviceType = getDeviceType();
  }
  
  build() {
    if (this.deviceType === 'wearable') {
      WearableUI()
    } else if (this.deviceType === 'phone') {
      PhoneUI()
    } else if (this.deviceType === 'tablet') {
      TabletUI()
    } else {
      TabletUI()
    }
  }
}

11.2 10.2 响应式布局

11.2.1 10.2.1 GridRow/GridColumn网格布局

GridRow和GridColumn是HarmonyOS提供的响应式网格布局组件,基于12列网格系统。

基础用法

@Entry
@Component
struct BasicGrid {
  build() {
    GridRow({ columns: 12, gutter: { x: 16, y: 16 } }) {
      GridCol({ span: 6 }) {
        Text('左侧内容')
          .width('100%')
          .height(100)
          .backgroundColor('#FF6B6B')
          .textAlign(TextAlign.Center)
      }
      GridCol({ span: 6 }) {
        Text('右侧内容')
          .width('100%')
          .height(100)
          .backgroundColor('#4ECDC4')
          .textAlign(TextAlign.Center)
      }
    }
    .width('100%')
    .padding(16)
  }
}

响应式断点

@Entry
@Component
struct ResponsiveGrid {
  build() {
    GridRow({ 
      columns: { sm: 4, md: 8, lg: 12 },  // 不同断点的列数
      gutter: { x: 16, y: 16 }
    }) {
      GridCol({ 
        span: { 
          sm: 4,   // 小屏:占满4列
          md: 4,   // 中屏:占4列
          lg: 3    // 大屏:占3列
        } 
      }) {
        Text('内容1')
          .width('100%')
          .height(100)
          .backgroundColor('#FF6B6B')
      }
      
      GridCol({ 
        span: { 
          sm: 4, 
          md: 4, 
          lg: 3 
        } 
      }) {
        Text('内容2')
          .width('100%')
          .height(100)
          .backgroundColor('#4ECDC4')
      }
      
      GridCol({ 
        span: { 
          sm: 4, 
          md: 8, 
          lg: 6 
        } 
      }) {
        Text('内容3')
          .width('100%')
          .height(100)
          .backgroundColor('#45B7D1')
      }
    }
  }
}

GridRow参数详解

GridRow({
  columns: number | Breakpoints,  // 列数或断点配置
  gutter: GutterOptions,          // 列间距
  debug: boolean                  // 是否显示网格线
})

// GutterOptions
interface GutterOptions {
  x?: Length;  // 水平间距
  y?: Length;  // 垂直间距
}

// Breakpoints
interface Breakpoints {
  sm?: number;  // 小屏列数(默认4)
  md?: number;  // 中屏列数(默认8)
  lg?: number;  // 大屏列数(默认12)
}

GridCol参数详解

GridCol({
  span: number | SpanOptions,     // 占用列数
  offset: number | OffsetOptions, // 偏移列数
  order: number | OrderOptions    // 排列顺序
})

// SpanOptions
interface SpanOptions {
  sm?: number;
  md?: number;
  lg?: number;
}

// OffsetOptions
interface OffsetOptions {
  sm?: number;
  md?: number;
  lg?: number;
}

// OrderOptions
interface OrderOptions {
  sm?: number;
  md?: number;
  lg?: number;
}

实际应用:卡片列表

@Entry
@Component
struct CardList {
  @State cards: string[] = ['卡片1', '卡片2', '卡片3', '卡片4', '卡片5', '卡片6'];
  
  build() {
    Scroll() {
      GridRow({ 
        columns: { sm: 1, md: 2, lg: 3 },
        gutter: { x: 16, y: 16 }
      }) {
        ForEach(this.cards, (item: string) => {
          GridCol({ span: 1 }) {
            Column() {
              Image($r('app.media.image'))
                .width('100%')
                .height(150)
                .objectFit(ImageFit.Cover)
              
              Text(item)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .margin({ top: 12 })
              
              Text('这是卡片描述内容')
                .fontSize(14)
                .fontColor('#666')
                .margin({ top: 8 })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .shadow({
              radius: 8,
              color: 'rgba(0,0,0,0.1)',
              offsetX: 0,
              offsetY: 2
            })
          }
        })
      }
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

11.2.2 10.2.2 媒体查询(MediaQuery)

MediaQuery允许根据设备特性(屏幕尺寸、方向、分辨率等)应用不同的样式。

基础用法

import mediaquery from '@ohos.mediaquery';

@Entry
@Component
struct MediaQueryExample {
  @State isLargeScreen: boolean = false;
  @State isLandscape: boolean = false;
  private largeScreenListener?: mediaquery.MediaQueryListener;
  private landscapeListener?: mediaquery.MediaQueryListener;
  
  aboutToAppear() {
    // 监听大屏(宽度>=600vp)
    this.largeScreenListener = mediaquery.matchMediaSync('(width>=600)');
    this.largeScreenListener.on('change', (result: mediaquery.MediaQueryResult) => {
      this.isLargeScreen = result.matches;
    });
    
    // 监听横屏
    this.landscapeListener = mediaquery.matchMediaSync('(orientation: landscape)');
    this.landscapeListener.on('change', (result: mediaquery.MediaQueryResult) => {
      this.isLandscape = result.matches;
    });
  }
  
  aboutToDisappear() {
    this.largeScreenListener?.off('change');
    this.landscapeListener?.off('change');
  }
  
  build() {
    Column() {
      if (this.isLargeScreen) {
        // 大屏布局
        Row() {
          Text('左侧导航')
            .width(200)
            .height('100%')
            .backgroundColor('#F0F0F0')
          
          Text('主内容区')
            .layoutWeight(1)
            .height('100%')
        }
      } else {
        // 小屏布局
        Column() {
          Text('主内容区')
            .width('100%')
            .layoutWeight(1)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

常用媒体查询条件

// 屏幕宽度
'(width>=600)'           // 宽度>=600vp
'(width>=600 and width<900)'  // 宽度在600-900vp之间

// 屏幕方向
'(orientation: portrait)'   // 竖屏
'(orientation: landscape)'  // 横屏

// 屏幕密度
'(resolution>=2)'      // 像素密度>=2

// 设备类型
'(device-type: phone)'     // 手机
'(device-type: tablet)'    // 平板
'(device-type: tv)'        // 智慧屏
'(device-type: wearable)'  // 穿戴设备
'(device-type: car)'       // 车机

// 深色模式
'(prefers-color-scheme: dark)'   // 深色模式
'(prefers-color-scheme: light)'  // 浅色模式

综合示例:响应式页面

import mediaquery from '@ohos.mediaquery';

@Entry
@Component
struct ResponsivePage {
  @State layout: 'mobile' | 'tablet' | 'desktop' = 'mobile';
  private mobileListener?: mediaquery.MediaQueryListener;
  private tabletListener?: mediaquery.MediaQueryListener;
  
  aboutToAppear() {
    // 移动端(<600vp)
    this.mobileListener = mediaquery.matchMediaSync('(width<600)');
    this.mobileListener.on('change', (result) => {
      if (result.matches) this.layout = 'mobile';
    });
    
    // 平板端(600-900vp)
    this.tabletListener = mediaquery.matchMediaSync('(width>=600 and width<900)');
    this.tabletListener.on('change', (result) => {
      if (result.matches) this.layout = 'tablet';
    });
    
    // 桌面端(>=900vp)
    let desktopListener = mediaquery.matchMediaSync('(width>=900)');
    desktopListener.on('change', (result) => {
      if (result.matches) this.layout = 'desktop';
    });
  }
  
  build() {
    if (this.layout === 'mobile') {
      this.MobileLayout()
    } else if (this.layout === 'tablet') {
      this.TabletLayout()
    } else {
      this.DesktopLayout()
    }
  }
  
  @Builder
  MobileLayout() {
    Column() {
      // 底部导航
      Tabs({ barPosition: BarPosition.End }) {
        TabContent() { Text('首页') }
        TabContent() { Text('发现') }
        TabContent() { Text('我的') }
      }
    }
  }
  
  @Builder
  TabletLayout() {
    Row() {
      // 左侧导航
      Column() {
        Text('首页')
        Text('发现')
        Text('我的')
      }
      .width(200)
      
      // 主内容
      Column() {
        Text('主内容区')
      }
      .layoutWeight(1)
    }
  }
  
  @Builder
  DesktopLayout() {
    Row() {
      // 左侧导航
      Column() {
        Text('Logo')
        Text('首页')
        Text('发现')
        Text('我的')
      }
      .width(250)
      .padding(20)
      
      // 中间内容
      Column() {
        Text('主内容区')
      }
      .layoutWeight(1)
      .padding(20)
      
      // 右侧边栏
      Column() {
        Text('推荐')
        Text('热门')
      }
      .width(300)
      .padding(20)
    }
  }
}

11.2.3 10.2.3 断点系统设计

断点系统是响应式设计的核心,用于定义不同屏幕尺寸的临界值。

定义断点常量

// Breakpoints.ets
export class Breakpoints {
  // 断点值定义
  static readonly SM = 0;      // 小屏(手机竖屏)
  static readonly MD = 600;    // 中屏(手机横屏/平板竖屏)
  static readonly LG = 840;    // 大屏(平板横屏/折叠屏)
  static readonly XL = 1200;   // 超大屏(桌面)
  
  // 断点名称
  static readonly SM_NAME = 'sm';
  static readonly MD_NAME = 'md';
  static readonly LG_NAME = 'lg';
  static readonly XL_NAME = 'xl';
}

// 断点工具类
export class BreakpointSystem {
  // 根据宽度获取断点
  static getBreakpoint(width: number): string {
    if (width < Breakpoints.MD) {
      return Breakpoints.SM_NAME;
    } else if (width < Breakpoints.LG) {
      return Breakpoints.MD_NAME;
    } else if (width < Breakpoints.XL) {
      return Breakpoints.LG_NAME;
    } else {
      return Breakpoints.XL_NAME;
    }
  }
  
  // 获取列数
  static getColumns(breakpoint: string): number {
    switch (breakpoint) {
      case Breakpoints.SM_NAME: return 4;
      case Breakpoints.MD_NAME: return 8;
      case Breakpoints.LG_NAME: return 12;
      case Breakpoints.XL_NAME: return 12;
      default: return 12;
    }
  }
  
  // 获取边距
  static getMargin(breakpoint: string): number {
    switch (breakpoint) {
      case Breakpoints.SM_NAME: return 16;
      case Breakpoints.MD_NAME: return 24;
      case Breakpoints.LG_NAME: return 32;
      case Breakpoints.XL_NAME: return 40;
      default: return 16;
    }
  }
}

断点监听器封装

import mediaquery from '@ohos.mediaquery';
import display from '@ohos.display';

@Observed
export class BreakpointState {
  current: string = 'sm';
  width: number = 0;
  columns: number = 4;
  margin: number = 16;
}

export class BreakpointListener {
  private state: BreakpointState;
  private listeners: mediaquery.MediaQueryListener[] = [];
  
  constructor(state: BreakpointState) {
    this.state = state;
    this.initListeners();
  }
  
  private initListeners() {
    // 小屏监听
    let smListener = mediaquery.matchMediaSync('(width<600)');
    smListener.on('change', (result) => {
      if (result.matches) {
        this.updateBreakpoint('sm');
      }
    });
    this.listeners.push(smListener);
    
    // 中屏监听
    let mdListener = mediaquery.matchMediaSync('(width>=600 and width<840)');
    mdListener.on('change', (result) => {
      if (result.matches) {
        this.updateBreakpoint('md');
      }
    });
    this.listeners.push(mdListener);
    
    // 大屏监听
    let lgListener = mediaquery.matchMediaSync('(width>=840 and width<1200)');
    lgListener.on('change', (result) => {
      if (result.matches) {
        this.updateBreakpoint('lg');
      }
    });
    this.listeners.push(lgListener);
    
    // 超大屏监听
    let xlListener = mediaquery.matchMediaSync('(width>=1200)');
    xlListener.on('change', (result) => {
      if (result.matches) {
        this.updateBreakpoint('xl');
      }
    });
    this.listeners.push(xlListener);
  }
  
  private updateBreakpoint(breakpoint: string) {
    this.state.current = breakpoint;
    this.state.columns = BreakpointSystem.getColumns(breakpoint);
    this.state.margin = BreakpointSystem.getMargin(breakpoint);
    
    let displayInfo = display.getDefaultDisplaySync();
    this.state.width = displayInfo.width / displayInfo.densityDPI;
  }
  
  destroy() {
    this.listeners.forEach(listener => {
      listener.off('change');
    });
  }
}

使用断点系统

@Entry
@Component
struct BreakpointExample {
  @State breakpointState: BreakpointState = new BreakpointState();
  private breakpointListener?: BreakpointListener;
  
  aboutToAppear() {
    this.breakpointListener = new BreakpointListener(this.breakpointState);
  }
  
  aboutToDisappear() {
    this.breakpointListener?.destroy();
  }
  
  build() {
    Column() {
      Text(`当前断点: ${this.breakpointState.current}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      Text(`列数: ${this.breakpointState.columns}`)
        .fontSize(16)
      
      Text(`边距: ${this.breakpointState.margin}`)
        .fontSize(16)
      
      // 根据断点显示不同布局
      if (this.breakpointState.current === 'sm') {
        Text('手机布局')
      } else if (this.breakpointState.current === 'md') {
        Text('平板布局')
      } else {
        Text('大屏布局')
      }
    }
    .width('100%')
    .padding(this.breakpointState.margin)
  }
}

11.2.4 10.2.4 弹性布局(Flex)高级用法

Flex布局是响应式设计的重要组成部分,适用于需要灵活调整的场景。

Flex容器属性

Flex({
  direction: FlexDirection,      // 主轴方向
  wrap: FlexWrap,                // 换行方式
  justifyContent: FlexAlign,     // 主轴对齐
  alignItems: ItemAlign,         // 交叉轴对齐
  alignContent: FlexAlign        // 多行对齐
})

响应式导航栏

@Entry
@Component
struct ResponsiveNav {
  @State isExpanded: boolean = true;
  
  build() {
    Flex({ 
      direction: FlexDirection.Row,
      wrap: FlexWrap.NoWrap,
      justifyContent: FlexAlign.SpaceBetween,
      alignItems: ItemAlign.Center
    }) {
      // Logo
      Text('Logo')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      // 导航菜单(大屏显示,小屏隐藏)
      if (this.isExpanded) {
        Flex({ 
          direction: FlexDirection.Row,
          gap: 24
        }) {
          Text('首页')
          Text('产品')
          Text('关于')
          Text('联系')
        }
      }
      
      // 菜单按钮(小屏显示)
      if (!this.isExpanded) {
        Button('☰')
          .onClick(() => {
            // 打开侧边菜单
          })
      }
    }
    .width('100%')
    .height(60)
    .padding({ left: 20, right: 20 })
    .backgroundColor(Color.White)
  }
}

自适应卡片网格

@Entry
@Component
struct AdaptiveCards {
  @State items: number[] = Array.from({ length: 20 }, (_, i) => i + 1);
  
  build() {
    Scroll() {
      Flex({ 
        direction: FlexDirection.Row,
        wrap: FlexWrap.Wrap,
        gap: 16
      }) {
        ForEach(this.items, (item: number) => {
          Column() {
            Text(`卡片 ${item}`)
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
          }
          .width('calc(33.33% - 11px)')  // 3列布局
          .height(150)
          .backgroundColor('#FF6B6B')
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
        })
      }
      .padding(16)
    }
  }
}

Flex布局对齐方式

@Entry
@Component
struct FlexAlignExample {
  build() {
    Column({ space: 30 }) {
      // justifyContent示例
      Text('justifyContent示例')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      
      Flex({ 
        direction: FlexDirection.Row,
        justifyContent: FlexAlign.SpaceBetween,
        width: '100%'
      }) {
        Text('A').width(50).height(50).backgroundColor('#FF6B6B')
        Text('B').width(50).height(50).backgroundColor('#4ECDC4')
        Text('C').width(50).height(50).backgroundColor('#45B7D1')
      }
      
      // alignItems示例
      Text('alignItems示例')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      
      Flex({ 
        direction: FlexDirection.Row,
        alignItems: ItemAlign.Center,
        height: 150,
        width: '100%'
      }) {
        Text('A').width(50).height(50).backgroundColor('#FF6B6B')
        Text('B').width(50).height(100).backgroundColor('#4ECDC4')
        Text('C').width(50).height(50).backgroundColor('#45B7D1')
      }
    }
    .padding(20)
  }
}

11.2.5 10.2.5 百分比与相对单位

百分比单位

@Entry
@Component
struct PercentageExample {
  build() {
    Column() {
      // 宽度百分比
      Row() {
        Text('左侧')
          .width('30%')
          .height(100)
          .backgroundColor('#FF6B6B')
        
        Text('右侧')
          .width('70%')
          .height(100)
          .backgroundColor('#4ECDC4')
      }
      .width('100%')
      
      // 高度百分比
      Row() {
        Text('内容')
          .width('100%')
          .height('50%')
          .backgroundColor('#45B7D1')
      }
      .width('100%')
      .height(200)
    }
  }
}

相对单位vp/fp

// vp: 虚拟像素,自动适配屏幕密度
// fp: 字体像素,用于字体大小

@Entry
@Component
struct UnitExample {
  build() {
    Column() {
      Text('文字大小使用fp')
        .fontSize(16)  // 16fp
      
      Text('大文字')
        .fontSize(24)  // 24fp
      
      Row() {
        Text('宽度使用vp')
          .width(100)  // 100vp
          .height(50)  // 50vp
      }
    }
  }
}

11.3 10.3 折叠屏适配

11.3.1 10.3.1 折叠/展开状态检测

折叠屏设备可以在折叠和展开状态之间切换,需要实时检测状态变化。

获取折叠屏状态

import foldDisplayMode from '@ohos.foldDisplayMode';

@Entry
@Component
struct FoldableExample {
  @State foldStatus: string = 'unknown';
  @State foldDisplayMode: foldDisplayMode.FoldDisplayMode = 
    foldDisplayMode.FoldDisplayMode.FULL;
  
  aboutToAppear() {
    // 获取当前折叠状态
    this.updateFoldStatus();
    
    // 监听折叠状态变化
    foldDisplayMode.on('foldDisplayModeChange', (mode: foldDisplayMode.FoldDisplayMode) => {
      this.foldDisplayMode = mode;
      this.updateFoldStatus();
    });
  }
  
  updateFoldStatus() {
    let mode = foldDisplayMode.getFoldDisplayMode();
    switch (mode) {
      case foldDisplayMode.FoldDisplayMode.FULL:
        this.foldStatus = '展开状态';
        break;
      case foldDisplayMode.FoldDisplayMode.HALF:
        this.foldStatus = '半折叠状态';
        break;
      case foldDisplayMode.FoldDisplayMode.FOLDED:
        this.foldStatus = '折叠状态';
        break;
      default:
        this.foldStatus = '未知状态';
    }
  }
  
  aboutToDisappear() {
    foldDisplayMode.off('foldDisplayModeChange');
  }
  
  build() {
    Column() {
      Text(`折叠状态: ${this.foldStatus}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      if (this.foldDisplayMode === foldDisplayMode.FoldDisplayMode.FULL) {
        this.ExpandedLayout()
      } else {
        this.FoldedLayout()
      }
    }
  }
  
  @Builder
  ExpandedLayout() {
    Row() {
      // 左侧导航
      Column() {
        Text('导航1')
        Text('导航2')
        Text('导航3')
      }
      .width(300)
      .height('100%')
      .backgroundColor('#F0F0F0')
      
      // 主内容
      Column() {
        Text('主内容区')
      }
      .layoutWeight(1)
      .height('100%')
    }
  }
  
  @Builder
  FoldedLayout() {
    Column() {
      Text('主内容区')
    }
    .width('100%')
    .height('100%')
  }
}

11.3.2 10.3.2 双屏模式处理

折叠屏在半折叠状态下可以使用双屏模式,上下屏显示不同内容。

双屏布局

import foldDisplayMode from '@ohos.foldDisplayMode';
import display from '@ohos.display';

@Entry
@Component
struct DualScreenExample {
  @State isHalfFolded: boolean = false;
  
  aboutToAppear() {
    foldDisplayMode.on('foldDisplayModeChange', (mode) => {
      this.isHalfFolded = mode === foldDisplayMode.FoldDisplayMode.HALF;
    });
  }
  
  build() {
    if (this.isHalfFolded) {
      this.DualScreenLayout()
    } else {
      this.SingleScreenLayout()
    }
  }
  
  @Builder
  DualScreenLayout() {
    Column() {
      // 上屏:视频播放
      Column() {
        Video({
          src: $r('app.media.video'),
          controller: new VideoController()
        })
          .width('100%')
          .height('100%')
      }
      .layoutWeight(1)
      .backgroundColor(Color.Black)
      
      // 下屏:控制界面
      Column() {
        Text('视频控制')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 })
        
        Row({ space: 20 }) {
          Button('播放')
          Button('暂停')
          Button('停止')
        }
        
        Slider({
          min: 0,
          max: 100,
          value: 50
        })
          .margin({ top: 20 })
      }
      .layoutWeight(1)
      .padding(20)
      .backgroundColor('#F5F5F5')
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  SingleScreenLayout() {
    Column() {
      Video({
        src: $r('app.media.video'),
        controller: new VideoController()
      })
        .width('100%')
        .height(300)
      
      Column() {
        Text('视频控制')
        // 控制按钮...
      }
      .layoutWeight(1)
    }
  }
}

11.3.3 10.3.3 屏幕尺寸变化响应

折叠屏展开/折叠时,屏幕尺寸会发生显著变化,需要响应式调整UI。

监听屏幕尺寸变化

import display from '@ohos.display';
import mediaquery from '@ohos.mediaquery';

@Entry
@Component
struct ScreenSizeChange {
  @State screenWidth: number = 0;
  @State screenHeight: number = 0;
  private sizeListener?: mediaquery.MediaQueryListener;
  
  aboutToAppear() {
    this.updateScreenSize();
    
    // 监听屏幕宽度变化
    this.sizeListener = mediaquery.matchMediaSync('(width>=600)');
    this.sizeListener.on('change', () => {
      this.updateScreenSize();
    });
  }
  
  updateScreenSize() {
    let displayInfo = display.getDefaultDisplaySync();
    this.screenWidth = displayInfo.width / displayInfo.densityDPI;
    this.screenHeight = displayInfo.height / displayInfo.densityDPI;
  }
  
  build() {
    Column() {
      Text(`屏幕宽度: ${this.screenWidth.toFixed(0)}vp`)
      Text(`屏幕高度: ${this.screenHeight.toFixed(0)}vp`)
      
      if (this.screenWidth >= 600) {
        // 大屏布局
        Row() {
          Column() {
            Text('左侧')
          }
          .width('30%')
          
          Column() {
            Text('右侧')
          }
          .layoutWeight(1)
        }
      } else {
        // 小屏布局
        Column() {
          Text('单列布局')
        }
      }
    }
  }
}

11.3.4 10.3.4 自适应UI切换

根据折叠状态动态切换UI组件和布局。

自适应内容展示

import foldDisplayMode from '@ohos.foldDisplayMode';

@Entry
@Component
struct AdaptiveFoldableUI {
  @State foldMode: foldDisplayMode.FoldDisplayMode = 
    foldDisplayMode.FoldDisplayMode.FOLDED;
  @State articles: Article[] = [];
  
  aboutToAppear() {
    foldDisplayMode.on('foldDisplayModeChange', (mode) => {
      this.foldMode = mode;
    });
  }
  
  build() {
    if (this.foldMode === foldDisplayMode.FoldDisplayMode.FULL) {
      this.ExpandedView()
    } else {
      this.FoldedView()
    }
  }
  
  @Builder
  ExpandedView() {
    Row() {
      // 左侧文章列表
      List() {
        ForEach(this.articles, (article: Article) => {
          ListItem() {
            ArticleItem({ article: article })
          }
        })
      }
      .width(350)
      .height('100%')
      .backgroundColor('#F9F9F9')
      
      // 右侧文章内容
      Column() {
        ArticleDetail({ article: this.articles[0] })
      }
      .layoutWeight(1)
      .height('100%')
      .padding(20)
    }
  }
  
  @Builder
  FoldedView() {
    Navigation() {
      List() {
        ForEach(this.articles, (article: Article) => {
          ListItem() {
            ArticleItem({ article: article })
          }
        })
      }
    }
    .navDestination(this.ArticlePage())
  }
  
  @Builder
  ArticlePage() {
    NavDestination() {
      ArticleDetail({ article: this.articles[0] })
    }
  }
}

interface Article {
  id: number;
  title: string;
  content: string;
}

@Component
struct ArticleItem {
  article: Article = { id: 0, title: '', content: '' };
  
  build() {
    Column() {
      Text(this.article.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
    }
    .padding(16)
    .width('100%')
  }
}

@Component
struct ArticleDetail {
  article: Article = { id: 0, title: '', content: '' };
  
  build() {
    Column() {
      Text(this.article.title)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      Text(this.article.content)
        .fontSize(16)
        .margin({ top: 20 })
    }
  }
}

11.4 10.4 平板适配

11.4.1 10.4.1 大屏布局策略

平板设备屏幕较大,需要充分利用屏幕空间,提供更好的信息展示和交互体验。

多列布局

@Entry
@Component
struct TabletLayout {
  @State categories: string[] = ['推荐', '热门', '科技', '娱乐', '体育'];
  @State articles: Article[] = [];
  
  build() {
    Row() {
      // 左侧分类导航
      Column() {
        Text('分类')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .padding({ left: 16, top: 16, bottom: 12 })
        
        ForEach(this.categories, (category: string) => {
          Text(category)
            .fontSize(16)
            .padding({ left: 16, top: 12, bottom: 12, right: 16 })
            .width('100%')
            .onClick(() => {
              // 切换分类
            })
        })
      }
      .width(200)
      .height('100%')
      .backgroundColor('#F5F5F5')
      
      // 中间文章列表
      Column() {
        List() {
          ForEach(this.articles, (article: Article) => {
            ListItem() {
              ArticleCard({ article: article })
            }
          })
        }
        .width('100%')
      }
      .layoutWeight(1)
      .height('100%')
      .padding(16)
      
      // 右侧推荐
      Column() {
        Text('热门推荐')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })
        
        ForEach(this.articles.slice(0, 5), (article: Article) => {
          Text(article.title)
            .fontSize(14)
            .padding({ top: 8, bottom: 8 })
        })
      }
      .width(300)
      .height('100%')
      .padding(16)
      .backgroundColor('#FAFAFA')
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct ArticleCard {
  article: Article = { id: 0, title: '', content: '' };
  
  build() {
    Row() {
      Image($r('app.media.article'))
        .width(120)
        .height(80)
        .objectFit(ImageFit.Cover)
        .borderRadius(8)
      
      Column() {
        Text(this.article.title)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Text(this.article.content)
          .fontSize(14)
          .fontColor('#666')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ top: 8 })
      }
      .layoutWeight(1)
      .margin({ left: 12 })
    }
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({
      radius: 4,
      color: 'rgba(0,0,0,0.1)',
      offsetX: 0,
      offsetY: 2
    })
  }
}

11.4.2 10.4.2 分栏导航(NavigationSplitView)

NavigationSplitView是专为大屏设计的分栏导航组件,适用于平板和折叠屏。

基础用法

@Entry
@Component
struct NavigationSplitViewExample {
  @State selectedCategory: string = '全部';
  @State selectedItem: string = '';
  categories: string[] = ['全部', '未读', '已读', '收藏'];
  items: string[] = ['文章1', '文章2', '文章3', '文章4', '文章5'];
  
  build() {
    NavigationSplitView({
      sidebar: this.Sidebar(),
      content: this.Content(),
      detail: this.Detail()
    })
  }
  
  @Builder
  Sidebar() {
    List() {
      ForEach(this.categories, (category: string) => {
        ListItem() {
          Text(category)
            .fontSize(16)
            .fontWeight(this.selectedCategory === category ? FontWeight.Bold : FontWeight.Normal)
            .padding({ left: 16, top: 12, bottom: 12 })
            .backgroundColor(this.selectedCategory === category ? '#E3F2FD' : Color.Transparent)
            .onClick(() => {
              this.selectedCategory = category;
            })
        }
      })
    }
    .width('100%')
  }
  
  @Builder
  Content() {
    List() {
      ForEach(this.items, (item: string) => {
        ListItem() {
          Text(item)
            .fontSize(16)
            .padding(16)
            .backgroundColor(this.selectedItem === item ? '#E3F2FD' : Color.White)
            .onClick(() => {
              this.selectedItem = item;
            })
        }
      })
    }
    .width('100%')
  }
  
  @Builder
  Detail() {
    Column() {
      if (this.selectedItem) {
        Text(this.selectedItem)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        
        Text('这里是详细内容区域')
          .fontSize(16)
          .margin({ top: 20 })
      } else {
        Text('请选择一个项目')
          .fontSize(18)
          .fontColor('#999')
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

三栏导航

@Entry
@Component
struct ThreeColumnNav {
  @State selectedFolder: string = '收件箱';
  @State selectedEmail: string = '';
  
  folders: string[] = ['收件箱', '已发送', '草稿', '垃圾邮件'];
  emails: Email[] = [
    { id: 1, from: '张三', subject: '会议通知', preview: '明天下午3点开会...' },
    { id: 2, from: '李四', subject: '项目进展', preview: '项目已完成80%...' },
    { id: 3, from: '王五', subject: '周末聚会', preview: '周六晚上一起吃饭...' }
  ];
  
  build() {
    NavigationSplitView({
      sidebar: this.FolderList(),
      content: this.EmailList(),
      detail: this.EmailDetail()
    })
  }
  
  @Builder
  FolderList() {
    Column() {
      Text('邮箱')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .padding(16)
      
      ForEach(this.folders, (folder: string) => {
        Row() {
          Text(folder)
            .fontSize(16)
            .layoutWeight(1)
        }
        .padding({ left: 16, top: 12, bottom: 12, right: 16 })
        .backgroundColor(this.selectedFolder === folder ? '#E3F2FD' : Color.Transparent)
        .onClick(() => {
          this.selectedFolder = folder;
        })
      })
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  EmailList() {
    Column() {
      Text(this.selectedFolder)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .padding(16)
      
      List() {
        ForEach(this.emails, (email: Email) => {
          ListItem() {
            Column() {
              Text(email.from)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
              
              Text(email.subject)
                .fontSize(14)
                .margin({ top: 4 })
              
              Text(email.preview)
                .fontSize(12)
                .fontColor('#666')
                .margin({ top: 4 })
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .padding(16)
            .width('100%')
            .backgroundColor(this.selectedEmail === email.from ? '#E3F2FD' : Color.White)
            .onClick(() => {
              this.selectedEmail = email.from;
            })
          }
        })
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  EmailDetail() {
    Column() {
      if (this.selectedEmail) {
        Text(this.selectedEmail)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        
        Divider()
          .margin({ top: 16, bottom: 16 })
        
        Text('邮件内容...')
          .fontSize(16)
      } else {
        Text('选择一封邮件查看详情')
          .fontSize(18)
          .fontColor('#999')
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

interface Email {
  id: number;
  from: string;
  subject: string;
  preview: string;
}

11.4.3 10.4.3 多窗口支持

平板支持多窗口功能,可以同时显示多个应用或同一应用的多个实例。

多窗口配置

// module.json5
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "supportWindowMode": [
          "fullscreen",      // 全屏模式
          "split",           // 分屏模式
          "floatingWindow"   // 浮动窗口
        ],
        "minWindowWidth": 400,   // 最小窗口宽度
        "minWindowHeight": 300   // 最小窗口高度
      }
    ]
  }
}

窗口模式监听

import window from '@ohos.window';

@Entry
@Component
struct MultiWindowExample {
  @State windowMode: string = 'fullscreen';
  
  aboutToAppear() {
    // 获取当前窗口上下文
    let windowClass = window.getLastWindow(getContext(this));
    
    // 监听窗口模式变化
    windowClass.on('windowModeChange', (mode: window.WindowMode) => {
      switch (mode) {
        case window.WindowMode.FULLSCREEN:
          this.windowMode = 'fullscreen';
          break;
        case window.WindowMode.PRIMARY:
          this.windowMode = 'split-primary';
          break;
        case window.WindowMode.SECONDARY:
          this.windowMode = 'split-secondary';
          break;
        case window.WindowMode.FLOATING:
          this.windowMode = 'floating';
          break;
      }
    });
  }
  
  build() {
    Column() {
      Text(`当前窗口模式: ${this.windowMode}`)
        .fontSize(20)
      
      if (this.windowMode === 'split-primary' || this.windowMode === 'split-secondary') {
        // 分屏模式:简化布局
        Text('分屏模式')
      } else if (this.windowMode === 'floating') {
        // 浮动窗口:紧凑布局
        Text('浮动窗口')
      } else {
        // 全屏模式:完整布局
        Text('全屏模式')
      }
    }
  }
}

11.4.4 10.4.4 手写笔与键盘适配

平板设备支持手写笔和键盘输入,需要适配相应的交互。

手写笔支持

@Entry
@Component
struct PenInputExample {
  @State strokes: Stroke[] = [];
  @State currentStroke: Stroke = new Stroke();
  
  build() {
    Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true)))
      .width('100%')
      .height('100%')
      .backgroundColor(Color.White)
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          this.currentStroke = new Stroke();
          let point = { x: event.touches[0].x, y: event.touches[0].y };
          this.currentStroke.points.push(point);
        } else if (event.type === TouchType.Move) {
          let point = { x: event.touches[0].x, y: event.touches[0].y };
          this.currentStroke.points.push(point);
          
          // 实时绘制
          this.drawStroke(this.currentStroke);
        } else if (event.type === TouchType.Up) {
          this.strokes.push(this.currentStroke);
          this.currentStroke = new Stroke();
        }
      })
  }
  
  drawStroke(stroke: Stroke) {
    // 绘制笔画逻辑
  }
}

class Stroke {
  points: Point[] = [];
}

interface Point {
  x: number;
  y: number;
}

键盘快捷键

@Entry
@Component
struct KeyboardShortcut {
  @State text: string = '';
  
  build() {
    Column() {
      TextArea({ text: this.text, placeholder: '输入文本...' })
        .width('100%')
        .height(200)
        .onKeyEvent((event: KeyEvent) => {
          // Ctrl+S 保存
          if (event.ctrlKey && event.key === 's') {
            this.saveDocument();
            event.stopPropagation();
          }
          
          // Ctrl+Z 撤销
          if (event.ctrlKey && event.key === 'z') {
            this.undo();
            event.stopPropagation();
          }
          
          // Ctrl+Y 重做
          if (event.ctrlKey && event.key === 'y') {
            this.redo();
            event.stopPropagation();
          }
        })
    }
  }
  
  saveDocument() {
    console.log('保存文档');
  }
  
  undo() {
    console.log('撤销');
  }
  
  redo() {
    console.log('重做');
  }
}

11.5 10.5 穿戴设备适配

11.5.1 10.5.1 小屏幕UI设计

穿戴设备屏幕较小(通常1.3-1.9英寸),需要精简UI元素,突出核心功能。

紧凑布局

@Entry
@Component
struct WearableUI {
  @State steps: number = 8562;
  @State heartRate: number = 72;
  @State calories: number = 320;
  
  build() {
    Column() {
      // 步数显示
      Column() {
        Text('步数')
          .fontSize(12)
          .fontColor('#999')
        
        Text(this.steps.toString())
          .fontSize(36)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 4 })
      }
      .margin({ top: 20 })
      
      // 心率和卡路里
      Row({ space: 20 }) {
        Column() {
          Text('心率')
            .fontSize(10)
            .fontColor('#999')
          
          Text(`${this.heartRate} bpm`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ top: 2 })
        }
        
        Column() {
          Text('卡路里')
            .fontSize(10)
            .fontColor('#999')
          
          Text(`${this.calories} kcal`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ top: 2 })
        }
      }
      .margin({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.Black)
    .fontColor(Color.White)
  }
}

11.5.2 10.5.2 圆形屏幕适配

部分穿戴设备采用圆形屏幕,需要适配圆形显示区域。

圆形屏幕布局

@Entry
@Component
struct CircularScreen {
  build() {
    Stack() {
      // 圆形背景
      Circle()
        .width('100%')
        .height('100%')
        .fill('#1A1A1A')
      
      // 内容区域(限制在圆形范围内)
      Column() {
        Text('12:30')
          .fontSize(48)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        
        Text('周一, 3月15日')
          .fontSize(14)
          .fontColor('#CCC')
          .margin({ top: 8 })
        
        Row({ space: 16 }) {
          Text('❤️ 72')
            .fontSize(12)
            .fontColor('#FF6B6B')
          
          Text('🔥 320')
            .fontSize(12)
            .fontColor('#FFA500')
        }
        .margin({ top: 16 })
      }
      .width('70%')  // 限制内容宽度
      .height('70%') // 限制内容高度
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

11.5.3 10.5.3 精简交互模式

穿戴设备交互方式有限,需要简化交互流程。

手势交互

@Entry
@Component
struct WearableGesture {
  @State currentPage: number = 0;
  
  build() {
    Swiper() {
      // 页面1:运动数据
      Column() {
        Text('今日运动')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        
        Text('8562步')
          .fontSize(32)
          .margin({ top: 12 })
      }
      
      // 页面2:心率
      Column() {
        Text('心率')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        
        Text('72 bpm')
          .fontSize(32)
          .margin({ top: 12 })
      }
      
      // 页面3:卡路里
      Column() {
        Text('卡路里')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        
        Text('320 kcal')
          .fontSize(32)
          .margin({ top: 12 })
      }
    }
    .index(this.currentPage)
    .indicator(false)  // 隐藏指示器
    .loop(false)
    .onChange((index: number) => {
      this.currentPage = index;
    })
    .width('100%')
    .height('100%')
  }
}

11.5.4 10.5.4 性能约束下的优化

穿戴设备性能有限,需要优化资源使用。

性能优化策略

@Entry
@Component
struct WearableOptimization {
  @State dataList: number[] = [];
  
  aboutToAppear() {
    // 延迟加载数据
    setTimeout(() => {
      this.dataList = [1, 2, 3, 4, 5];
    }, 100);
  }
  
  build() {
    List() {
      ForEach(this.dataList, (item: number) => {
        ListItem() {
          // 简化UI组件
          Text(`项目 ${item}`)
            .fontSize(14)
            .padding(12)
        }
      })
    }
    .width('100%')
    .cachedCount(2)  // 限制缓存数量
    .scrollBar(BarState.Off)  // 关闭滚动条
  }
}

资源优化

// 使用小尺寸图片
Image($r('app.media.icon_small'))  // 而不是 icon_large
  .width(48)
  .height(48)

// 避免复杂动画
Text('数据')
  .fontSize(16)
  // 不使用 .animation() 或减少动画时长

// 减少重绘
@Component
struct OptimizedComponent {
  @Prop data: string;  // 使用@Prop而不是@Link,减少更新
  
  build() {
    Text(this.data)
  }
}

11.6 10.6 多端适配最佳实践

11.6.1 10.6.1 资源限定词(Resource Qualifier)

资源限定词允许为不同设备配置提供不同的资源文件。

资源目录结构

resources/
├── base/                    # 默认资源
│   ├── element/
│   │   ├── color.json
│   │   └── string.json
│   └── media/
│       └── icon.png
├── phone/                   # 手机资源
│   └── media/
│       └── icon_phone.png
├── tablet/                  # 平板资源
│   └── media/
│       └── icon_tablet.png
├── wearable/                # 穿戴设备资源
│   └── media/
│       └── icon_wearable.png
├── tv/                      # 智慧屏资源
│   └── media/
│       └── icon_tv.png
├── foldable/                # 折叠屏资源
│   └── media/
│       └── icon_foldable.png
├── car/                     # 车机资源
│   └── media/
│       └── icon_car.png
├── dark/                    # 深色模式
│   └── element/
│       └── color.json
├── en_US/                   # 英文
│   └── element/
│       └── string.json
└── zh_CN/                   # 中文
    └── element/
        └── string.json

使用资源限定词

@Entry
@Component
struct ResourceQualifier {
  build() {
    Column() {
      // 自动根据设备类型加载对应图片
      Image($r('app.media.icon'))
        .width(100)
        .height(100)
      
      // 自动根据语言加载字符串
      Text($r('app.string.hello'))
        .fontSize(20)
      
      // 自动根据深色/浅色模式加载颜色
      Text('文本')
        .fontColor($r('app.color.text_primary'))
    }
  }
}

颜色资源定义

// base/element/color.json
{
  "color": [
    {
      "name": "text_primary",
      "value": "#000000"
    },
    {
      "name": "background",
      "value": "#FFFFFF"
    }
  ]
}

// dark/element/color.json
{
  "color": [
    {
      "name": "text_primary",
      "value": "#FFFFFF"
    },
    {
      "name": "background",
      "value": "#121212"
    }
  ]
}

11.6.2 10.6.2 动态资源加载

根据设备类型动态加载不同资源。

动态加载图片

import device from '@ohos.deviceInfo';

@Entry
@Component
struct DynamicResource {
  @State deviceType: string = 'phone';
  
  aboutToAppear() {
    this.deviceType = this.getDeviceType();
  }
  
  getDeviceType(): string {
    // 根据设备特征判断类型
    let displayInfo = display.getDefaultDisplaySync();
    let screenWidth = displayInfo.width / displayInfo.densityDPI;
    
    if (screenWidth < 3) return 'wearable';
    if (screenWidth < 6) return 'phone';
    if (screenWidth < 9) return 'tablet';
    return 'tv';
  }
  
  build() {
    Column() {
      if (this.deviceType === 'wearable') {
        Image($r('app.media.icon_small'))
          .width(48)
          .height(48)
      } else if (this.deviceType === 'phone') {
        Image($r('app.media.icon_medium'))
          .width(96)
          .height(96)
      } else {
        Image($r('app.media.icon_large'))
          .width(192)
          .height(192)
      }
    }
  }
}

11.6.3 10.6.3 设备类型检测

准确检测设备类型是适配的基础。

综合设备检测

import device from '@ohos.deviceInfo';
import display from '@ohos.display';
import mediaquery from '@ohos.mediaquery';

export class DeviceDetector {
  // 检测设备类型
  static getDeviceType(): 'phone' | 'tablet' | 'foldable' | 'tv' | 'wearable' | 'car' {
    let displayInfo = display.getDefaultDisplaySync();
    let screenWidthInch = displayInfo.width / displayInfo.densityDPI;
    let screenHeightInch = displayInfo.height / displayInfo.densityDPI;
    let diagonalInch = Math.sqrt(screenWidthInch * screenWidthInch + screenHeightInch * screenHeightInch);
    
    if (diagonalInch < 2) {
      return 'wearable';
    } else if (diagonalInch < 7) {
      return 'phone';
    } else if (diagonalInch < 11) {
      // 可能是平板或折叠屏
      return this.isFoldable() ? 'foldable' : 'tablet';
    } else if (diagonalInch < 20) {
      return 'car';
    } else {
      return 'tv';
    }
  }
  
  // 检测是否为折叠屏
  static isFoldable(): boolean {
    try {
      // 检查是否支持折叠屏API
      let foldMode = foldDisplayMode.getFoldDisplayMode();
      return foldMode !== undefined;
    } catch (e) {
      return false;
    }
  }
  
  // 检测屏幕方向
  static isLandscape(): boolean {
    let displayInfo = display.getDefaultDisplaySync();
    return displayInfo.width > displayInfo.height;
  }
  
  // 检测是否为深色模式
  static isDarkMode(): boolean {
    // 通过资源限定词或系统API检测
    return false;
  }
  
  // 获取屏幕尺寸信息
  static getScreenInfo() {
    let displayInfo = display.getDefaultDisplaySync();
    return {
      width: displayInfo.width,
      height: displayInfo.height,
      density: displayInfo.densityDPI,
      widthInch: displayInfo.width / displayInfo.densityDPI,
      heightInch: displayInfo.height / displayInfo.densityDPI
    };
  }
}

11.6.4 10.6.4 自适应组件封装

封装可复用的自适应组件。

自适应按钮

@Component
export struct AdaptiveButton {
  text: string = '';
  onClick: () => void = () => {};
  @Prop deviceType: string = 'phone';
  
  build() {
    Button(this.text)
      .fontSize(this.getFontSize())
      .height(this.getHeight())
      .padding(this.getPadding())
      .onClick(this.onClick)
  }
  
  getFontSize(): number {
    switch (this.deviceType) {
      case 'wearable': return 12;
      case 'phone': return 14;
      case 'tablet': return 16;
      case 'tv': return 20;
      default: return 14;
    }
  }
  
  getHeight(): number {
    switch (this.deviceType) {
      case 'wearable': return 32;
      case 'phone': return 40;
      case 'tablet': return 48;
      case 'tv': return 56;
      default: return 40;
    }
  }
  
  getPadding(): Padding {
    switch (this.deviceType) {
      case 'wearable': return { left: 12, right: 12 };
      case 'phone': return { left: 16, right: 16 };
      case 'tablet': return { left: 24, right: 24 };
      case 'tv': return { left: 32, right: 32 };
      default: return { left: 16, right: 16 };
    }
  }
}

自适应列表

@Component
export struct AdaptiveList<T> {
  items: T[] = [];
  @BuilderParam itemBuilder: (item: T) => void;
  @Prop deviceType: string = 'phone';
  
  build() {
    if (this.deviceType === 'wearable') {
      // 穿戴设备:简单列表
      List() {
        ForEach(this.items, (item: T) => {
          ListItem() {
            this.itemBuilder(item)
          }
        })
      }
      .scrollBar(BarState.Off)
    } else if (this.deviceType === 'tv') {
      // 智慧屏:网格布局
      GridRow({ columns: 4 }) {
        ForEach(this.items, (item: T) => {
          GridCol({ span: 1 }) {
            this.itemBuilder(item)
          }
        })
      }
    } else {
      // 手机/平板:标准列表
      List() {
        ForEach(this.items, (item: T) => {
          ListItem() {
            this.itemBuilder(item)
          }
        })
      }
      .divider({ strokeWidth: 1, color: '#F0F0F0' })
    }
  }
}

11.6.5 10.6.5 多端测试策略

测试矩阵

// 定义测试设备矩阵
export const TestDevices = {
  phone: {
    name: 'Phone',
    screen: { width: 1080, height: 2340, density: 480 },
    orientation: ['portrait', 'landscape']
  },
  tablet: {
    name: 'Tablet',
    screen: { width: 2560, height: 1600, density: 320 },
    orientation: ['portrait', 'landscape']
  },
  foldable: {
    name: 'Foldable',
    screen: { 
      folded: { width: 1080, height: 2340, density: 480 },
      unfolded: { width: 2200, height: 2340, density: 480 }
    },
    orientation: ['portrait', 'landscape']
  },
  wearable: {
    name: 'Wearable',
    screen: { width: 466, height: 466, density: 320 },
    shape: 'circular'
  },
  tv: {
    name: 'TV',
    screen: { width: 3840, height: 2160, density: 320 },
    orientation: 'landscape'
  }
};

自动化测试脚本

import { describe, it, expect } from '@ohos/hypium';

export default function multiDeviceTest() {
  describe('MultiDeviceTest', () => {
    // 测试不同设备类型的布局
    it('test_phone_layout', 0, () => {
      // 模拟手机设备
      let deviceType = 'phone';
      let layout = getAdaptiveLayout(deviceType);
      expect(layout.type).assertEqual('single_column');
    });
    
    it('test_tablet_layout', 0, () => {
      // 模拟平板设备
      let deviceType = 'tablet';
      let layout = getAdaptiveLayout(deviceType);
      expect(layout.type).assertEqual('multi_column');
    });
    
    it('test_wearable_layout', 0, () => {
      // 模拟穿戴设备
      let deviceType = 'wearable';
      let layout = getAdaptiveLayout(deviceType);
      expect(layout.type).assertEqual('compact');
    });
  });
}

function getAdaptiveLayout(deviceType: string) {
  // 根据设备类型返回布局配置
  if (deviceType === 'phone') {
    return { type: 'single_column', columns: 1 };
  } else if (deviceType === 'tablet') {
    return { type: 'multi_column', columns: 3 };
  } else if (deviceType === 'wearable') {
    return { type: 'compact', columns: 1 };
  }
  return { type: 'default', columns: 1 };
}

11.7 本章小结

本章深入讲解了HarmonyOS多端适配的核心技术和最佳实践:

  1. 多设备形态:了解了HarmonyOS支持的设备类型及其能力差异,掌握了自适应设计原则和一次开发多端部署策略。

  2. 响应式布局:深入学习了GridRow/GridColumn网格布局、媒体查询、断点系统设计、Flex弹性布局以及百分比与相对单位的使用。

  3. 折叠屏适配:掌握了折叠/展开状态检测、双屏模式处理、屏幕尺寸变化响应和自适应UI切换技术。

  4. 平板适配:学习了大屏布局策略、分栏导航(NavigationSplitView)、多窗口支持以及手写笔与键盘适配。

  5. 穿戴设备适配:了解了小屏幕UI设计、圆形屏幕适配、精简交互模式和性能约束下的优化方法。

  6. 多端适配最佳实践:掌握了资源限定词、动态资源加载、设备类型检测、自适应组件封装和多端测试策略。

通过本章学习,开发者能够构建真正意义上”一次开发,多端部署”的HarmonyOS应用,为不同设备形态的用户提供最佳体验。

11.8 练习题

题目1(单选题):HarmonyOS中,用于实现响应式网格布局的组件是?

A. Flex和Column B. GridRow和GridColumn C. Row和Column D. Stack和RelativeContainer

答案:B 解析:GridRow和GridColumn是HarmonyOS专门提供的响应式网格布局组件,基于12列网格系统,支持断点配置,是实现响应式布局的核心组件。Flex虽然也可以实现弹性布局,但不是专门的网格布局组件。

题目2(单选题):在HarmonyOS多端适配中,断点系统的主要作用是?

A. 提高应用性能 B. 根据屏幕尺寸应用不同布局 C. 管理应用生命周期 D. 处理用户输入

答案:B 解析:断点系统是响应式设计的核心,用于定义不同屏幕尺寸的临界值(如600vp、840vp等),根据设备宽度自动切换相应的布局方案,实现一套代码适配多种设备。

题目3(多选题):以下哪些是HarmonyOS支持的设备类型?

A. 智能手机 B. 平板电脑 C. 智慧屏 D. 穿戴设备

答案:ABCD 解析:HarmonyOS支持多种设备类型,包括智能手机、平板电脑、折叠屏、智慧屏(电视)、穿戴设备、车机等,体现了其分布式操作系统的特点。

题目4(判断题):资源限定词只能用于区分不同语言,不能用于区分不同设备类型。

答案:错误 解析:资源限定词不仅可以用于区分语言(如en_US、zh_CN),还可以用于区分设备类型(phone、tablet、wearable、tv等)、屏幕方向、深色模式等多种设备配置,实现动态资源加载。

题目5(单选题):折叠屏设备在半折叠状态下,可以使用哪种模式实现上下屏显示不同内容?

A. 全屏模式 B. 分屏模式 C. 双屏模式 D. 浮动窗口模式

答案:C 解析:折叠屏在半折叠状态下可以使用双屏模式(Dual Screen Mode),将屏幕分为上下两部分,分别显示不同的内容,如上方显示视频,下方显示控制界面。

题目6(判断题):平板适配中,NavigationSplitView组件最多支持两栏导航。

答案:错误 解析:NavigationSplitView支持三栏导航(sidebar、content、detail),适用于平板和折叠屏等大屏设备,可以充分利用屏幕空间提供更好的信息展示。

题目7(单选题):穿戴设备适配中,为了优化性能,应该采取以下哪种策略?

A. 使用高清大图 B. 增加复杂动画 C. 限制列表缓存数量 D. 启用所有系统功能

答案:C 解析:穿戴设备性能有限,应该采取性能优化策略,如限制列表缓存数量(cachedCount)、使用小尺寸图片、简化动画、减少重绘等,以保证流畅的用户体验。

题目8(多选题):以下哪些是HarmonyOS多端适配的最佳实践?

A. 使用资源限定词 B. 实现响应式布局 C. 检测设备类型 D. 为每个设备开发独立应用

答案:ABC 解析:多端适配的最佳实践包括使用资源限定词、实现响应式布局、检测设备类型、封装自适应组件等。应该避免为每个设备开发独立应用,而是采用”一次开发,多端部署”的策略。