在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前置思考

1.1 平板是"生产力设备",不是"大号手机"

平板的定位是生产力:办公文档、视频会议、教育白板、多任务协同。用户买平板不是因为它屏幕大能看视频,而是因为大屏能同时干多件事——一边开文档一边查资料、一边视频会议一边记笔记。

如果平板 App 只是把手机界面拉宽,用户会得到:

❌ 灾难1: 全屏弹窗键盘遮住半个屏幕
❌ 灾难2: 文档只能竖着看,横屏空一大片
❌ 灾难3: 没有多窗口,视频会议不能和笔记同屏
❌ 灾难4: 键盘鼠标适配为零,外接键鼠毫无体验

1.2 平板大屏的三层能力

第一层: 布局适配(跑得开)
  → 分栏布局、多列栅格、充分利用横屏空间

第二层: 多窗口(干得多)
  → 分屏多窗口、自由窗口、画中画、平行视界

第三层: 外设适配(用得爽)
  → 键盘快捷键、鼠标右键、拖拽交互、触控笔

1.3 本文价值

本文给出平板大屏适配的完整方案:分屏多窗口架构、平行视界实现、拖拽交互(Drag)、自由窗口管理、键盘鼠标适配,让应用在平板上达到"原生生产力工具"的体验。

二、核心原理

2.1 平板窗口体系全景

┌────────────────────────────────────────────┐
│            平板桌面 (Free Window)           │
├──────────────┬─────────────────────────────┤
│  主窗口      │  副窗口/自由窗口              │
│  (应用A)     │  (应用B / 小窗)              │
├──────────────┴─────────────────────────────┤
│  分屏模式 (Split)      │  平行视界 (PV)     │
│  左右各 50%            │  应用内双窗格      │
├────────────────────────────────────────────┤
│  画中画 (PiP)          │  悬浮窗            │
│  视频悬浮              │  快捷工具          │
└────────────────────────────────────────────┘
  • 分屏多窗口:系统级左右分屏,两个应用并行(系统能力,应用需适配多实例);
  • 平行视界:同一个应用内左右双窗格(列表+详情),平板专属体验;
  • 自由窗口:应用窗口可拖拽、缩放、最小化,类似桌面系统;
  • 画中画:视频播放悬浮小窗,可边看边操作其他应用。

2.2 平行视界原理

平行视界(Parallel View)是鸿蒙平板的杀手锏:一个 Activity 内两个 UI 栈

┌────────────────────────────────────────────┐
│  左窗格 (列表)          │  右窗格 (详情)     │
│  ┌─────────┐           │  ┌─────────────┐  │
│  │ 邮件列表 │           │  │ 邮件正文      │  │
│  │ 第1封   │◄─点击─►   │  │ 详细内容...   │  │
│  │ 第2封   │           │  │ 附件/回复     │  │
│  └─────────┘           │  └─────────────┘  │
└────────────────────────────────────────────┘
    两个 UI 栈共享同一 ViewModel
    左窗格点击 → 右窗格 push 新页面
    右窗格返回 → 只 pop 右栈,左栈不受影响

实现核心:应用内声明 isStartWindow: true 的窗口为"分栏窗口",系统自动将列表和详情分到左右两栏。

2.3 拖拽交互(Drag)

拖拽三要素:
① 拖拽源: 长按触发 → DragItem 承载数据
② 拖拽数据: 传递 String / Uri / PixelMap
③ 拖拽目标: 接收数据 → 执行放置逻辑

拖拽流程:
  长按 → 抖动反馈 → 悬浮拖影 → 移动 → 目标高亮 → 松手 → 数据回调

三、源码/API 深度解析

3.1 多窗口生命周期适配

import { window } from '@kit.ArkUI';

// 监听窗口尺寸变化(分屏/自由窗口时自动回调)
const listener: window.SizeChangeListener = {
  onSizeChange: (oldSize: window.Size, newSize: window.Size) => {
    if (newSize.width < 600) {
      // 分屏后被压缩 → 切换到紧凑布局
      this.switchCompactLayout();
    } else {
      this.switchWideLayout();
    }
  }
};

// 获取当前窗口信息
const win = window.getLastWindow(getContext(this));
win.then((w: window.Window) => {
  const props: window.WindowProperties = w.getWindowProperties();
  const rect = props.windowRect;
  LoggerUtil.info(TAG, '窗口: ' + rect.width + 'x' + rect.height);
});

3.2 平行视界实现

// 声明支持平行视界(应用配置)
// module.json5 中:
// {
//   "abilities": [{
//     "name": "MainAbility",
//     "supportWindowMode": ["fullscreen", "split", "floating"],
//     "parallelView": { "enabled": true }
//   }]
// }

@Entry
@ComponentV2
struct MailPage {
  @Local selectedMailId: number = -1;
  @Local mails: MailItem[] = [];

  @Builder
  renderListPane() {
    List() {
      ForEach(this.mails, (m: MailItem) => {
        ListItem() {
          Row() {
            Text(m.sender).fontSize(14).fontWeight(FontWeight.Bold)
            Text(m.subject).fontSize(12).fontColor('rgba(255,255,255,0.7)')
          }
          .padding(12).backgroundColor('rgba(255,255,255,0.05)').borderRadius(8)
          .onClick(() => { this.selectedMailId = m.id; })
        }
      }, (m: MailItem) => m.id)
    }
    .layoutWeight(1)
  }

  @Builder
  renderDetailPane() {
    if (this.selectedMailId >= 0) {
      Column() {
        Text('📧 邮件正文').fontSize(16).fontWeight(FontWeight.Bold)
        Text('这是第 ' + this.selectedMailId + ' 封邮件的详细内容…')
          .fontSize(13).margin({ top: 10 })
      }
      .layoutWeight(1).padding(16)
    } else {
      Text('👈 在左侧选择一封邮件').fontSize(13).fontColor('rgba(255,255,255,0.4)')
    }
  }

  build() {
    Row() {
      this.renderListPane()      // 左窗格
      Divider().vertical(true).color('rgba(255,255,255,0.1)')
      this.renderDetailPane()    // 右窗格
    }
    .width('100%').height('100%')
  }
}

3.3 拖拽交互实现

// 拖拽源: 图片/文件卡片长按拖出
@Builder
renderDraggableCard(item: FileItem) {
  Column() {
    Text(item.icon).fontSize(32)
    Text(item.name).fontSize(12).fontColor('#FFFFFF')
  }
  .padding(12)
  .backgroundColor('rgba(255,255,255,0.06)').borderRadius(10)
  .onDragStart(() => {
    // 拖拽数据: 文件 URI
    const dragData = new DragData();
    dragData.setData('file:///storage/' + item.path);
    return dragData;
  })
}

// 拖拽目标: 文件夹接收放置
@Builder
renderDropTarget() {
  Column() {
    Text('📁 拖文件到这里').fontSize(14)
  }
  .width('100%').height(80)
  .border({ width: 2, color: '#4FC3F7', style: BorderStyle.Dashed }).borderRadius(10)
  .onDrop((event: DragEvent, extraParams: string) => {
    const uri = extraParams;
    this.moveFile(uri, this.targetDir);
    ToastUtil.success('文件已移动');
  })
}

3.4 键盘鼠标适配

// 1. 键盘快捷键
.onKeyEvent((event: KeyEvent) => {
  if (event.type === KeyType.Down) {
    if (event.keyCode === KeyCode.KEYCODE_N && event.ctrlKey) {
      this.createNewDoc();      // Ctrl+N 新建
    }
    if (event.keyCode === KeyCode.KEYCODE_S && event.ctrlKey) {
      this.saveDoc();           // Ctrl+S 保存
    }
  }
})

// 2. 鼠标右键菜单
.onMouse((event: MouseEvent) => {
  if (event.action === MouseAction.Press && event.button === MouseButton.Right) {
    this.showContextMenu(event.x, event.y);  // 弹出右键菜单
  }
})

四、企业级实战落地

4.1 平板适配清单

阶段 动作 验收
布局 横屏分栏布局 + 大屏栅格 横竖屏无空白无裁剪
多窗口 支持分屏/自由窗口/悬浮 窗口尺寸变化布局自适应
平行视界 列表+详情双窗格 左栈右栈独立导航
拖拽 文件/图片拖拽 + 拖放目标 拖拽流畅、放置准确
外设 键盘快捷键 + 鼠标右键 键鼠可达核心操作

4.2 完整示例:文档办公应用

@Entry
@ComponentV2
struct TabletOfficeDemo {
  @Local docs: DocItem[] = [];
  @Local activeDocId: number = -1;
  @Local isWide: boolean = true;

  build() {
    Column() {
      // 顶部工具栏(键盘快捷键提示)
      Row() {
        Text('📄 文档管理').fontSize(20).fontWeight(FontWeight.Bold)
        Blank()
        Text('Ctrl+N 新建 · Ctrl+S 保存 · 右键菜单').fontSize(11)
          .fontColor('rgba(255,255,255,0.4)')
      }
      .width('100%').padding(12)

      if (this.isWide) {
        // 宽屏: 列表 + 详情 双栏
        Row() {
          List() { /* 文档列表 */ }.width('30%')
          Divider().vertical(true).color('rgba(255,255,255,0.1)')
          Column() { /* 文档详情 + 拖放目标 */ }.layoutWeight(1)
        }.layoutWeight(1)
      } else {
        // 分屏被压缩: 单栏
        List() { /* 文档列表 */ }.layoutWeight(1)
      }
    }
    .width('100%').height('100%').backgroundColor('#0D1B2A')
  }
}

4.3 大屏适配效果对比

方案 手机 平板横屏 分屏后
单栏 ❌ 空白多
双栏(列表+详情) ❌ 挤 ✅ 详情变窄
三栏 ✅ 生产力 ⚠️ 详情隐藏

五、问题排查与性能优化

问题 原因 解决
分屏后布局错乱 未监听窗口尺寸变化 SizeChangeListener 动态切换
平行视界不同步 左右窗格数据未共享 共用同一 ViewModel
拖拽无反馈 未设置拖影/高亮 DragItem + 目标高亮
键盘无效 未注册快捷键 onKeyEvent 处理
鼠标右键无菜单 未监听右键 onMouse 右键弹菜单
大屏 GPU 压力大 全屏渐变+复杂阴影 减少全屏重绘

5.1 性能优化要点

1. 分屏后立即回收离屏资源(隐藏详情页的预加载数据)
2. 平行视界复用 ViewModel,避免双份数据
3. 拖拽动画用系统模板,不自定义粒子效果
4. 大图加载用缩略图优先,点开再加载原图

六、高阶总结与最佳实践

  1. 生产力优先:平板布局永远考虑"能不能边看边写、边查边记"。
  2. 多窗口是标配:所有平板应用都要过"分屏后还能用吗"这道关。
  3. 平行视界是体验分水岭:列表+详情双窗格让平板 App 有"原生感"。
  4. 外设适配是加分项:键鼠+拖拽让应用从"能用"变"好用"。
  5. 布局可切换:宽屏/窄屏/分屏三种状态,布局要能平滑切换而非硬编码。

一句话记住:平板适配 = 布局分栏(跑得开)+ 多窗口(干得多)+ 键鼠拖拽(用得爽)——大屏的每一寸空间都要有价值。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐