同一个任务中心,放在手机上是纵向列表,展开到平板又希望同时看到分类、任务和详情。真正麻烦的地方不是把宽度乘以一个系数,而是窗口随时发生变化:折叠屏由折叠态进入展开态,平板进入分屏,电脑用户拖动窗口边缘,应用得到的可用区域都与设备外壳尺寸不同。如果页面只通过设备名称决定布局,主从栏很容易挤在一起,原本能够阅读的正文被压得只剩几行。

更可靠的设计是以当前应用窗口的可用宽度驱动布局,同时把“选中了哪条任务”“筛选条件是什么”保留为独立业务状态。页面可以从单列切到两列、三列,但它们读取同一套任务数据。布局变化不应该导致网络请求重新发起,也不应把未提交的输入内容清空。本文用一个任务工作台说明从断点制定到交互复核的完整工程方法。

任务中心在不同窗口宽度下的布局

断点是内容决定的,不是设备名称决定的

断点通常从最拥挤的界面开始推导。任务列表需要展示名称、状态和最后更新时间,单项的理想最小宽度约为 260vp;右侧详情包含操作区域,宽度低于 320vp 时,按钮和正文就容易相互抢空间。左右两栏之间还需要留出 12~20vp 的间隔,所以将 600vp 设为单列转双列的工程起点较为合适。继续扩展到分类导航、任务列表、任务详情三个区域时,可将 840vp 作为第二个断点。这里的数值是本页面的设计选择,不属于所有应用都必须照搬的系统阈值。

本项目把宽度划为三段:不足 600vp 时优先完成单个任务,600~839vp 时侧重列表和详情,达到 840vp 时增加分类区。窗口高度、文字缩放和语言长度同样会影响内容容纳能力,因此断点只是布局规则的入口,组件还要设置合理的最小高度、留白和滚动行为。华为开发者的媒体查询资料明确支持根据应用页面的 width、height 等特征更新样式,并允许在查询条件中使用 vp 单位。

具体排布可用 GridRow 与 GridCol 实现。它们负责把栅格宽度映射到不同的断点列数,业务模型本身没有必要为每种设备复制一份。下面先展示一个适合工作台概览页的布局:小窗口一列、中等窗口两列、大窗口三列。TaskSummary 是应用内部的数据类型,列表内容可以从接口适配层注入。

interface TaskSummary {
  id: string;
  name: string;
  progress: number;
}

@Entry
@Component
struct OverviewPage {
  @State tasks: TaskSummary[] = [
    { id: 'review', name: '需求评审', progress: 40 },
    { id: 'design', name: '交互设计', progress: 70 },
    { id: 'release', name: '版本准备', progress: 25 }
  ];

  @Builder
  taskCard(item: TaskSummary) {
    Column({ space: 8 }) {
      Text(item.name).fontSize(20).fontWeight(FontWeight.Medium)
      Text(`完成进度 ${item.progress}%`).fontSize(14)
      Progress({ value: item.progress, total: 100 })
        .color('#2778DC')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F3F7FE')
    .borderRadius(14)
  }

  build() {
    GridRow({
      columns: { sm: 4, md: 8, lg: 12 },
      breakpoints: {
        value: ['600vp', '840vp'],
        reference: BreakpointsReference.WindowSize
      },
      gutter: { x: 12, y: 12 }
    }) {
      ForEach(this.tasks, (item: TaskSummary) => {
        GridCol({ span: { sm: 4, md: 4, lg: 4 } }) {
          this.taskCard(item)
        }
      }, (item: TaskSummary) => item.id)
    }
    .width('100%')
    .padding(16)
  }
}

这段代码表达的是三种宽度区间对应的栅格占比,而不是强行给屏幕固定像素值。GridCol 的 span 对应列数:小窗口占四列,中窗口占八列中的四列,大窗口占十二列中的四列。稳定的 item.id 用作列表键,数据更新时更容易维持组件身份。项目中还应避免为所有子节点都设置与父栅格相同的跨度,那会增加无效的布局层次;华为 Code Linter 也有针对这一写法的检查规则。

DevEco Studio 中的断点栅格与预览布局

窗口变化时,业务状态要留在页面之外

仅靠栅格能完成概览卡片排布,却处理不了“单栏时跳到详情,大栏时并排显示”的路由语义。我们可以用媒体查询监听当前窗口宽度,将是否宽屏保存在一个页面状态里。检测依据选择 min-width 而不是 device-width:后者代表设备层面的尺寸,并不等同于当前窗口可绘制区域;例如应用进入分屏后,窗口变窄,布局应立即收缩。

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

@Entry
@Component
struct TaskWorkspace {
  @State wide: boolean = false;
  @State selectedId: string = 'review';
  private widthListener?: mediaquery.MediaQueryListener;

  private onWidthChange = (result: mediaquery.MediaQueryResult): void => {
    this.wide = result.matches;
  };

  aboutToAppear(): void {
    this.widthListener = this.getUIContext()
      .getMediaQuery().matchMediaSync('(min-width: 600vp)');
    this.widthListener.on('change', this.onWidthChange);
    // 初次布局也应依据当前窗口宽度建立状态
    this.wide = this.widthListener.matches;
  }

  aboutToDisappear(): void {
    this.widthListener?.off('change', this.onWidthChange);
  }

  build() {
    Column() {
      Text('项目任务中心').fontSize(26).fontWeight(FontWeight.Bold)
      Row({ space: 12 }) {
        Column({ space: 12 }) {
          Button('需求评审').onClick(() => { this.selectedId = 'review'; })
          Button('交互设计').onClick(() => { this.selectedId = 'design'; })
          Button('版本准备').onClick(() => { this.selectedId = 'release'; })
        }
        .width(this.wide ? '38%' : '100%')
        if (this.wide) {
          Column({ space: 12 }) {
            Text('当前任务').fontSize(18)
            Text(this.selectedId).fontSize(24)
            Text('这里接入具体任务详情与操作按钮')
          }
          .layoutWeight(1)
          .padding(16)
          .backgroundColor('#F4F8FE')
          .borderRadius(12)
        }
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .padding(20)
    .width('100%')
  }
}

需要留意:这段主从区代码在窄窗口保留任务列表,选择后的详情导航应由既有 Navigation 页面栈接管;宽窗口则在同页展示详情。实际工程可以把选中 ID 放进页面共享的状态对象,让窄屏路由和宽屏详情组件都订阅同一字段。这样从窄屏进入详情、扩展窗口再回到双栏时,不会因为重新创建一个详情组件就切换到默认任务。对开发者而言,真正要守住的是状态与视图分离,断点计算只负责决定哪些组件显示。

这还涉及一个常被忽略的细节:媒体查询的监听要在页面出现时绑定,页面离开时解绑。否则多次返回同一页面,监听器会累积,窗口改变一次却触发多次回调。对于展示在多个页面的全局工作台,应把监听逻辑放到与页面生命周期匹配的容器中,而不是在每个列表项里独立监听一次。

针对多窗口交互做一张真正有用的检查表

可用宽度变化期望排布核对重点
小于 600vp单列内容按钮可点击、正文可滚动、详情有返回路径
600~839vp双栏或双列卡片列表选择与详情同步、间距不挤压
不低于 840vp更充分的多列布局分类可见、长标题换行、空态完整
由宽变窄重新排布保留选中 ID、筛选条件和未提交输入
由窄变宽重新排布不重复发起数据加载、滚动位置合理

这里的检查不应该只看页面有没有横向滚动条。字体从默认尺寸放大后是否发生遮挡,中文标题换成更长的多语言文本后是否截断,键盘弹出后详情操作区是否还能触达,都值得一并复核。图片区域应保持宽高比,含数据的卡片不要只靠颜色区分状态;在多设备之间切换输入方式时,这些细节比将每个按钮按比例拉伸更重要。

小窗口任务列表及断点切换要点

把响应式布局纳入后续上架路线

做多端适配时,一个容易拖慢研发的办法是按手机、平板、电脑分别维护三套页面。短期内每套页面都能快速达到视觉目标,但需求增加“筛选任务”“批量操作”后,同一个业务规则就得改三遍。采用共用数据模型、有限的布局分支和统一的详情路由,维护成本会稳得多。

界面是否合适应由数据可读性、操作密度和窗口行为来判断。窗口宽度是输入,状态不丢失是约束,布局只是输出。接下来的多设备适配章节会继续讨论鼠标、键盘、触控与焦点处理,使这套工作台不仅看起来适应不同尺寸,也能符合不同输入方式的交互习惯。

再强调一条工程原则:断点变化只应重新计算布局,不能悄悄改变任务列表的排序规则。假设手机采用最近更新排序,平板却换成优先级排序,用户会误以为任务在切换后消失。让筛选、排序与分页状态由业务层统一管理,不同布局只负责卡片密度和信息呈现顺序,才能避免同一工作台在不同终端出现业务语义偏差。

分栏详情较长时,还必须明确滚动容器属于哪一级:整页滚动,或左右栏各自滚动。采用嵌套滚动时需要核对手势竞争和焦点跳转,不应让用户滚动某一栏却把另一栏的内容带走。配合稳定业务键、必要的焦点恢复与清晰的空态,窗口变化才能成为自然的视觉调整,而不是一次突兀的页面重置。

相关官方资料:媒体查询与响应式设计、GridCol 栅格组件、多端栅格检查规则。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐