前言

用户在平板上打开文档,目录和正文分居两侧,阅读过程中又把另一个应用拖进系统分屏。设备没有更换,当前应用能使用的窗口宽度却缩小了。如果页面仍然按照全屏宽度安排内容,正文可能变得拥挤,原来同时可见的两页也可能不再满足平行视界的显示条件。

即使应用已经确定了进入和返回的路径,窗口变化仍然会影响用户能否继续阅读。大家需要分别检查当前窗口是否满足两页显示条件,以及页面如何利用剩余的内容空间。检查过程中,路由和业务数据应该继续对应同一次阅读任务;保留这个前提,再比较窗口变化前后的显示,才能区分尺寸计算错误与内容重新初始化造成的异常。

一、为什么两页不能一直并排

页面获得的空间取决于应用窗口。平板上的应用可以处于窄分屏中,折叠设备上的应用也可能在设备展开后,因为窗口形状不同而采用不同配置分支。因此,设备名称只能帮助大家确定配置范围,不能直接说明当前布局状态。大家判断某一时刻能否分栏时,还需要检查应用窗口的宽度和宽高比。

平行视界的宽横屏条件要求窗口宽度至少为 600vp,并且窗口宽高比大于 1.2。方形宽屏同样要求窗口宽度至少为 600vp,同时窗口的高宽比与宽高比均不大于 1.2。宽度足够却过于狭长的窗口,也未必落入这两个分支。因此,应用还需要保留普通页面独立完成任务的路径,不能只为两页同时显示安排操作入口。

大家记录窗口宽度时,还需要保留尺寸单位。分辨率中的像素数量不能直接代替配置条件里的 vp,相同的像素宽度在不同密度下也未必代表相同的布局空间。窗口分支的判断需要采用与条件一致的尺寸单位,再对照截图检查实际排版。如果配置判断与观察记录使用不同单位,边界附近的显示变化就难以解释。

当前配置通过 wideSplit 将宽横屏中的目录与正文比例设为 1:2,通过 squareSplit 将方形宽屏中的比例设为 1:1。这组分配用于保留目录辨认与正文阅读所需的空间。应用窗口发生变化以后,大家需要观察相同内容是否采用了对应分支的配置,以及左右两侧是否仍然可读。

        "wideSplit": {
          "ratio": "1 | 2"
        },
        "squareSplit": {
          "ratio": "1 | 1"
        },

配置写明了两类窗口采用的显示方式与比例,但静态配置没有提供运行时的窗口测量结果。如果当前画面没有按预期分栏,大家应当先记录窗口宽高及设备配置,再核对窗口是否满足条件。反复修改比例无法排除窗口根本没有满足目标分支条件的情况,还可能使原本可用的比例失去比较依据。

设备节点会进一步影响窗口采用哪份配置。当前工程使用 common,项目如果另写 phonetablet 等专用节点,我们就需要检查该设备最终采用的整份配置。设备专用配置生效以后,公共配置不会继续按字段逐项叠加到该设备。因此,专用节点中漏写的字段不能被视为一定会从 common 补齐,一次局部修改也可能影响原本依赖的选项。

例如,公共节点允许目标窗口分栏,专用节点却只保留普通显示方式,我们就需要从专用节点解释该设备的显示结果。排查记录可以同时写明设备、窗口宽高、实际采用的配置节点和最终画面。大家据此才能区分配置范围不符合预期,还是窗口本来就不满足条件,避免把两类原因都归为某种设备不支持。

大家为连续操作准备记录时,可以按照下表中的窗口状态安排观察点。表格列出需要检查的条件和预期规则,尚未运行的画面没有被记作已经通过。

窗口状态需要确认的条件后续观察
宽横屏宽度及宽高比满足宽横屏条件目录和正文是否按对应配置分配
方形宽屏宽度及两个方向的比例满足方形宽屏条件分支切换后内容是否继续可读
窄窗口已不满足目标分栏条件当前内容是否能在普通页面继续使用
系统分屏允许分屏内启用,且窗口仍满足条件两页关系与尺寸来源是否正确
自由多窗当前属于自由多窗模式按普通页面检查任务是否可完成

自由多窗模式不支持平行视界,普通页面需要自行适配;系统分屏则有单独的启用选项。这两种模式都可能让应用窗口变小,却适用不同的显示规则。因此,检查记录需要写明系统窗口模式,单独一张窄窗口截图无法说明当前显示由哪项规则决定。

大家还需要让窗口从两个方向经过分栏的临界条件。窗口由宽变窄时,检查重点是两页显示退出以后,当前内容是否仍然可用;窗口由窄恢复为宽时,检查重点则是路径中是否重复增加了同一个目标。两次操作的最终宽度可以相同,但应用经历的变化顺序不同,可能暴露不同的问题。一次任务中的连续窗口变化,才能提供这些过程信息,分别重启到两种窗口无法覆盖同样的检查范围。

二、旧页面拿到的宽度应当怎样理解

即使应用窗口满足分栏条件,页面内部仍然可能排版异常。已有页面有时使用整个窗口或屏幕宽度计算卡片,而平行视界中的内容只占其中一部分。如果页面继续使用原来的整窗或整屏宽度,卡片尺寸、文字换行或断点判断就可能超出内容区域能够容纳的范围。大家需要沿卡片使用的尺寸来源检查,才能确定计算从哪里开始偏离实际空间。

enableReducedContainerSize 用于调整虚拟容器相关的尺寸信息,默认关闭。这个选项开启后,系统会在平行视界显示期间,按右侧页面的比例缩放应用的 lpx、横向断点、窗口宽度和屏幕宽度等相关信息。尺寸调整作用于整个应用,系统不会因此为左右两栏分别返回各自的实际宽度。左右比例不同的时候,页面计算尤其需要区分应用取得的尺寸信息与当前栏实际占用的空间。

系统分屏还有一项例外:应用启用分屏支持后,屏幕宽度仍然保持原始大小。应用如果一处读取窗口宽度,另一处读取屏幕宽度,两处读取结果就不能被假定为按相同比例缩放。大家需要先找到页面实际调用的取宽接口,再比较该接口返回的宽度与内容区域的关系。这个检查可以避免把窗口宽度与屏幕宽度当作同一种输入处理。

如果页面使用 WindowProperties.drawableRect,大家还需要单独检查 drawableRectHook。这个选项默认关闭,开启后,系统会按右侧页面的比例调整可绘制区域。当前配置同时开启了这个选项与系统分屏支持,两项配置分别对应可绘制区域调整和分屏内的启用条件。

        "drawableRectHook": true,
        "enableInSplitScreen": true

drawableRectHook 调整可绘制区域的尺寸,enableInSplitScreen 允许符合条件的系统分屏窗口使用平行视界。分屏支持选项开启以后,应用窗口仍然需要满足宽度与形状条件,因此这个选项不能保证所有分屏窗口都显示两页,也不会让自由多窗获得支持。页面还需要根据自己实际使用的尺寸来源安排内容,才能在符合分栏条件时保持可读。

现有阅读页使用 Scroll 包含正文,没有读取屏幕宽度、监听窗口变化或按断点切换业务组件的代码。因此,已有构建结果只能确认这些配置被接受,不能证明旧页面的每种取宽方式都已经适配。大家接入实际项目时,需要找到页面中具体的尺寸计算位置,再使用该页面取得的数据比较选项开启前后的差别。

大家可以从最先出现异常的组件开始追踪尺寸来源。例如,页面标题正常而下方卡片被截断时,检查需要先确认卡片宽度来自固定值、父容器约束,还是外部数值计算。尺寸来源明确以后,大家才能判断虚拟容器选项是否与这次异常有关。页面进入两栏这一变化本身不要求重写所有组件,修改范围围绕已经观察到的异常收缩,也方便检查普通页面是否受到连带影响。

对于会根据宽度计算列数的卡片区域,大家可以记录计算使用的接口、取得的数值和最终列数。如果可用空间减少而列数不变,排查就需要继续检查断点与数据来源;如果列数已经变化但卡片仍被截断,大家则需要检查固定宽度和内边距。每一步检查都使用前一步的观察结果缩小范围,修改理由也比同时调整多个容器选项更容易说明。

大家修改尺寸计算时,还需要确认组件是否已经根据父容器约束调整布局。已经能够自适应的组件,不一定需要再按屏幕宽度手动缩放;代码额外乘一次比例,反而可能把内容压得过窄。这是通用布局判断,具体是否需要修改取决于当前组件的尺寸输入。配置选项的开启不能单独成为重写所有子组件尺寸计算的依据。

应用级尺寸调整还要求大家分别观察左右两侧内容。右侧正文按照调整后的宽度换行以后,如果左侧组件也使用同一份窗口宽度参与计算,大家仍然需要检查左侧组件是否符合所在区域的约束。右侧显示恢复正常,不能证明左侧已经取得自己的实际宽度。左右比例不同时,两侧结果的对照尤其有助于发现把应用级数值当作局部宽度的写法。

三、用一次阅读任务检查窗口变化

大家分别启动宽窗口与窄窗口,可以确认两种初始画面,却容易遗漏窗口切换过程中的问题。用户可能在已经打开内容以后调整窗口,因此检查需要从一次尚未结束的阅读任务开始。内容进入、窗口变化、继续阅读和返回沿用同一条路径,大家才能发现显示调整是否意外触发了清空或重复入栈。

当前首页在 aboutToAppear() 中执行进入 WindowDetailPage 的路径操作,右侧正文提供滚动容器。正式版编译接受这段代码,但初始化期间的路径操作仍然存在页面构建时序风险,可能导致跳转失败或白屏。已有构建结果没有排除这个风险,因此大家需要先运行检查第一次进入是否成功,再比较窗口变化过程。

大家开始连续检查时,需要记录已打开的内容和目标路径,再将窗口从宽横屏调整为方形、缩窄并恢复。每次窗口变化以后,大家先确认页面仍然显示原来的文档,再检查滚动位置和返回目标。当前正文是静态内容,页面尚未实现阅读位置保存,因此滚动位置能否保留仍然需要观察,路由或平行视界不能被预先视为自动提供了这项保证。

阅读位置的观察需要以能够辨认的段落或条目作为参照。窗口宽度改变以后,文字会重新排版,相同滚动距离未必仍然指向相同内容,所以一个孤立的距离数值不足以说明阅读是否连续。真实阅读应用需要由自己的状态保存与恢复逻辑决定如何定位内容,以及何时恢复位置。当前页面没有实现这些逻辑,连续阅读可以作为检查要求,但窗口恢复后自动返回原段落还不能作为已有功能。

如果窗口恢复后,路径中出现两份相同目标,大家需要检查尺寸变化是否触发了重复进入逻辑。如果目标正确但内容回到顶部,检查就需要转向阅读位置的保存方式。这两种问题都可能在宽窗口恢复以后出现,但分别需要修改不同位置。诊断记录将路径与页面状态分开保存,能够帮助大家避免为了修复滚动位置而错误地清空路由。

大家还需要为系统分屏安排一段连续操作。应用在窗口条件允许时进入分屏以后,测试人员继续调整分隔位置,观察页面何时退出两页显示,以及普通页面是否仍然可操作。检查记录还需要对照页面实际使用的窗口宽度与屏幕宽度,特别保留屏幕宽度不缩放这一例外。这样,大家才能识别原本在全屏下正确的计算是否在分屏后出现误差。

折叠设备上的过渡过程仍然需要在目标真机补充检查。设备展开与折叠除了改变窗口形状,还可能涉及屏幕切换、系统动画和触控区域变化,模拟器中的窗口调整无法完整复现这些过程。测试人员可以先沿普通页面路径确认任务能否继续,再在目标设备上观察过渡过程。尚未覆盖的设备仍然需要保留待确认的范围。

对于暂时不能稳定适配的窗口,团队可以选择保留普通页面:用户仍然能够打开内容和返回,大家也可以将问题限定在分栏条件或尺寸计算上。这个工程判断以普通页面本身可用为前提。如果窗口缩窄以后,用户连主要按钮也无法触达,大家就需要继续修复页面布局,关闭分栏本身还不能证明适配完成。

普通页面还可以用于对照故障是否来自新增配置。如果内容在普通窗口下同样被截断,大家需要继续检查页面自身的固定尺寸与排列;如果问题只在两页显示时出现,检查则集中到页面实际使用的尺寸来源。两种显示方式的比较需要保留同一份数据和字体设置,内容差异才不会干扰判断。大家据此既能说明页面需要修改哪里,也能确定暂时关闭哪个范围的分栏。

总结

大家处理窗口适配时,需要先确认设备配置与实际窗口条件,再核对页面读取的尺寸来自哪里。虚拟容器选项按右侧页面比例调整应用相关的尺寸信息,系统分屏中的屏幕宽度则保留原值,因此具体取宽代码决定了页面会受到什么影响。大家还需要分别检查路径是否正确、内容是否可读,防止显示变化重置用户正在进行的任务。

团队将这些检查用于现有应用时,可以先保留一条普通页面路径,再逐项接入分栏与尺寸选项。问题出现以后,大家才能通过前后的对照结果确定失败位置,并选择相应的回退范围。

我目前手里的设备还不支持 HarmonyOS 7 ,所以相关内容现阶段主要通过 HarmonyOS 7 模拟器进行验证,真机上的系统表现、设备差异和实际体验,后面有条件再继续补测,最终还是以实际设备运行结果为准。

完整代码

Index.ets

@Entry
@Component
struct Index {
  @Provide('pageStack')
  pageStack: NavPathStack = new NavPathStack()

  @Builder
  pageMap(name: string) {
    if (name === 'WindowDetailPage') {
      WindowDetailPage()
    }
  }

  aboutToAppear(): void {
    this.pageStack.pushPathByName('WindowDetailPage', null)
  }

  build() {
    Navigation(this.pageStack) {
      Column({ space: 12 }) {
        Text('窗口适配清单')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .width('100%')

        Text('同一套页面用于窄窗口、长方形宽窗口和方形宽窗口。')
          .fontSize(15)
          .fontColor('#5F6678')
          .lineHeight(22)
          .width('100%')

        this.checkItem('窄窗口', '保留普通单页 Navigation')
        this.checkItem('长方形宽窗口', '目录 1,内容 2')
        this.checkItem('方形宽窗口', '目录 1,内容 1')
        this.checkItem('系统分屏', '满足窗口条件后允许进入')
      }
      .width('100%')
      .height('100%')
      .padding(24)
      .alignItems(HorizontalAlign.Start)
    }
    .mode(NavigationMode.Stack)
    .title('窗口适配')
    .navDestination(this.pageMap)
    .width('100%')
    .height('100%')
  }

  @Builder
  private checkItem(title: string, detail: string) {
    Column({ space: 6 }) {
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .width('100%')
      Text(detail)
        .fontSize(14)
        .fontColor('#667085')
        .width('100%')
    }
    .width('100%')
    .padding(18)
    .backgroundColor('#F2F4FA')
    .borderRadius(16)
    .alignItems(HorizontalAlign.Start)
  }
}

@Component
struct WindowDetailPage {
  build() {
    NavDestination() {
      Scroll() {
        Column({ space: 16 }) {
          Text('当前内容页')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
            .width('100%')

          Text('窗口变化时,页面结构和数据保持一致。系统按 EasyGo 的窗口条件决定是否进入平行视界,并按当前窗口类型选择比例。')
            .fontSize(17)
            .lineHeight(28)
            .width('100%')

          Text('人工检查项目')
            .fontSize(20)
            .fontWeight(FontWeight.Medium)
            .width('100%')

          Text('• 横竖屏切换后标题是否完整\n• 折叠状态变化后内容是否被挤压\n• 系统分屏宽度满足条件时是否进入平行视界\n• 窄窗口返回时路由栈是否正常')
            .fontSize(16)
            .lineHeight(28)
            .width('100%')
        }
        .width('100%')
        .padding(24)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .height('100%')
    }
    .title('内容页')
  }
}

module.json5

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "easyGo": "$profile:easy_go",
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "ohos.want.action.home"
            ]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      }
    ]
  }
}

easy_go.json

{
  "common": {
    "displayModeOptions": {
      "wideWindowMode": "navigationSplit",
      "squareWindowMode": "navigationSplit",
      "navigationSplitOptions": {
        "homePage": "navBar",
        "relatedPage": "WindowDetailPage",
        "enableReducedContainerSize": true,
        "wideSplit": {
          "ratio": "1 | 2"
        },
        "squareSplit": {
          "ratio": "1 | 1"
        },
        "drawableRectHook": true,
        "enableInSplitScreen": true
      }
    }
  }
}
Logo

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

更多推荐