“森林邮局”卡牌编辑器已经有两套确定的业务规则:成品是63×88毫米,出血画布69×94毫米,安全区53×78毫米;标题、插画和说明区域按照毫米坐标检查碰撞与越界。进入多设备适配时,最需要守住的是这两套规则,而不是把手机页面等比例拉大。手机上的卡面和属性表通常上下排列,平板上则适合让画布常驻左侧、属性和诊断结果显示在右侧。无论编辑区怎么重排,标题的毫米位置、被选中的元素和检查结论都不能改变。

手机与平板的编辑区域编排

判断依据从设备型号改为容器宽度

编辑器可能出现在手机全屏、平板分栏、折叠屏半屏以及应用内侧滑面板之中。用“平板一定宽”决定双栏,看似省事,实际只要用户收窄窗口就会出现属性面板挤占画布的情况。更合适的测量对象是编辑器所在的容器:宽度足够时左侧保留卡面,右侧呈现元素属性;宽度变小时,属性区移到画布下方,检查入口仍可见。卡牌尺寸与容器宽度是两种不同数据,前者使用毫米,后者只用来决定页面排布。

华为开发者文档中的ContainerReader容器断点从API 26.0.0开始提供,支持获取容器尺寸和宽度断点。它与过去按照窗口或设备类型分类的方式有本质差异。本文把它接入界面层,仍保留前两篇的CardGeometry、CardElement与LayoutInspector,不把任何碰撞计算改写为屏幕像素判断。项目在entry/src/main/ets/pages/ResponsiveEditorPage.ets承载布局,model/EditorPresentation.ets描述展示状态,service/GeometryProjector.ets继续负责毫米到预览单位的投影,components/CardCanvas.ets和components/InspectorPane.ets分别实现画布与属性面板。

版面里还存在另一种常见混淆:容器越宽,卡牌预览不一定需要无限放大。要设置画布最大展示宽度,并为属性面板留出最小可读宽度。宽屏状态主要增加同时可见的信息量,而不是扩大字体或者修改元素的毫米坐标。这样手机页面上的猫头鹰插画与平板预览里的插画才能来自同一份几何数据。

先把布局状态与卡牌模型分开

前两篇已经把卡牌元素存为稳定ID和RectMm坐标。现在新增的编辑器展示状态只包含选中元素、面板方式及用户缩放比例。选中元素title的区域例如是(13,60,43,9),这组数字不能随着面板从右侧移动到下方而改变。只有发生明确的拖动或输入动作时,才写回模型。预览投影使用当前画布的实际可用宽高计算缩放,不使用设备屏幕的固定宽高。

export interface RectMm {
  x: number;
  y: number;
  width: number;
  height: number;
}
export interface EditorSelection {
  elementId: string;
  zoom: number;
}
export interface ViewRect {
  left: number;
  top: number;
  width: number;
  height: number;
}

export function projectRect(box: RectMm, canvasWidthMm: number,
  canvasHeightMm: number, viewportW: number, viewportH: number): ViewRect {
  if (viewportW <= 0 || viewportH <= 0 ||
      canvasWidthMm <= 0 || canvasHeightMm <= 0) {
    throw new Error('预览空间尚未准备好');
  }
  const scale = Math.min((viewportW - 24) / canvasWidthMm,
    (viewportH - 24) / canvasHeightMm);
  if (!Number.isFinite(scale) || scale <= 0) {
    throw new Error('预览尺寸不可计算');
  }
  const originX = (viewportW - canvasWidthMm * scale) / 2;
  const originY = (viewportH - canvasHeightMm * scale) / 2;
  return { left: originX + box.x * scale, top: originY + box.y * scale,
    width: box.width * scale, height: box.height * scale };
}

这个函数只转换显示矩形,不能反向覆盖原始尺寸;触摸拖动若要写回毫米,需要使用当前同一比例进行逆变换,并对转换后的数值做边界校验。画布尚无尺寸时让页面显示占位,不要把零宽误读成卡片规格异常。缩放倍率由用户控制时,也应在容器适配比例之外叠加,退出预览或切换面板后不应重置已保存的毫米坐标。

将ContainerReader放在编辑器区域而非页面最外层

本工程的导航栏和顶部工具条需要保持稳定,随宽度改变的是它们下面的编辑区。把ContainerReader放在编辑区的Flex内部,可以让断点由编辑区自身的剩余宽度决定。组件通过双向绑定提供容器尺寸与WidthBreakpoint;页面只在窄断点使用上下排列,在其余断点使用左右排列。业务数据并不经过这一判断,选中的卡面元素仍指向同一个ID。

import { ContainerReader, Size } from '@kit.ArkUI';

@Entry
@Component
struct ResponsiveEditorPage {
  @State private editorSize: Size = { width: 0, height: 0 };
  @State private widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  @State private selectedId: string = 'title';

  @Builder
  private cardArea() {
    Column({ space: 8 }) {
      Text('森林邮局 · 卡面预览').fontSize(20)
      Text('画布69×94 / 成品63×88 / 安全区53×78毫米')
      Text(`选中元素:${this.selectedId}`)
    }.padding(12).backgroundColor('#F4F7F3')
  }

  @Builder
  private inspector() {
    Column({ space: 8 }) {
      Text('元素属性').fontSize(18)
      Text('标题  x=13  y=60  w=43  h=9(毫米)')
      Button('检查碰撞').onClick(() => { this.selectedId = 'title'; })
    }.padding(12)
  }

  build() {
    Column({ space: 8 }) {
      Text('卡牌版式编辑').fontSize(22)
      Flex({ direction: FlexDirection.Column }) {
        ContainerReader({
          size: this.editorSize!!,
          widthBreakpoint: this.widthBp!!
        }) {
          if (this.widthBp === WidthBreakpoint.WIDTH_SM) {
            Column({ space: 12 }) {
              this.cardArea()
              this.inspector()
            }
          } else {
            Row({ space: 16 }) {
              this.cardArea()
              this.inspector()
            }
          }
        }
      }.width('100%').layoutWeight(1)
    }.padding(12).width('100%').height('100%')
  }
}

这里用最小的页面骨架说明断点位置,实际的cardArea可以替换成上一章的Canvas预览,inspector替换为可编辑表单。WidthBreakpoint的枚举与绑定形式应依据API 26的SDK声明使用;工程若仍以较低版本SDK构建,就不能把新组件写进既有低版本代码后假定它可用。适配方案应先确认构建SDK与目标设备的版本覆盖,再决定是否保留旧的窗口宽度布局作为兼容路径。断点属于排版策略,不属于卡面数据版本。

白色开发环境中的容器断点与画布预览

编辑状态不能因断点切换而丢失

手机紧凑布局里,用户打开标题属性后向下滑动,可能把画布完全移出可见范围;平板双栏时则能同时观察画布和数字输入。页面需要把选择ID、当前草稿对象与风险列表留在外层状态中,不能在两个条件分支里分别生成独立的编辑副本。否则同一标题会在切换布局之后回到旧坐标。特别是折叠屏改变折叠姿态时,断点可能连续变化,不能让每次变更都触发从磁盘重新读取草稿。

ContainerReader提供的容器尺寸可用于决定画布展示区域,但没有理由直接调用inspectLayout。后者接受的仍是毫米元素和安全框;当画布由宽屏切成窄屏,正确的检查结果应保持一致。应分别维护“视图重新投影”和“版式实体发生修改”两条更新路径。前者只触发绘制与布局刷新,后者才更新编辑修订号、记录修改动作并重新生成碰撞列表。

import { RectMm } from '../model/CardGeometry';

export interface CardElement {
  id: string;
  box: RectMm;
  keepInsideSafe: boolean;
}
export interface EditorState {
  revision: number;
  selectedId: string;
  elements: CardElement[];
}

export function replaceElementPosition(state: EditorState,
  elementId: string, nextBox: RectMm): EditorState {
  const item = state.elements.find((e: CardElement) => e.id === elementId);
  if (!item) { throw new Error('未找到指定元素'); }
  if (![nextBox.x, nextBox.y, nextBox.width, nextBox.height]
      .every((v: number) => Number.isFinite(v))) {
    throw new Error('元素位置不可用');
  }
  return {
    ...state,
    revision: state.revision + 1,
    elements: state.elements.map((e: CardElement) =>
      e.id === elementId ? { ...e, box: { ...nextBox } } : e)
  };
}

export function changePresentation(state: EditorState,
  selectedId: string): EditorState {
  return { ...state, selectedId };
}

这两种函数的区别直接关系到下一篇的快照保存:换选中元素、收起属性面板或改变容器宽度,不应让版式修订号连续上涨。只有坐标、字号等影响最终内容的属性发生改变时,才产生新的草稿修订。若利用索引定位元素,增删一张装饰后可能把选中目标误指向别的图形,因此状态中使用稳定ID而非数组位置。

双端调试时先看数据,再看排布

调试可以从两组容器状态入手:窄容器中画布与属性区上下排列,宽容器中左右排列;再把宽屏应用窗口收窄,验证编辑区可以独立回到紧凑布局。需要关注的是容器实际宽度、当前断点、selectedId、毫米元素矩形和视图投影比例,而不是只记录设备型号。测试场景可以设计为:选中标题元素、展开属性、缩窄容器,再检查标题坐标是否仍为(13,60,43,9),安全矩形是否仍为(8,8,53,78)。这些是可复核的预期条件,不需要把预览画面当成实体设备的运行记录。

出现界面重叠时,先核对右侧属性区是否设置了合理的最小宽度,随后检查画布绘制是否使用了上一轮容器尺寸。若断点切换后检测数量变化,优先排查是否误把视图坐标写回毫米模型;如果标题选中状态丢失,则检查两个分支是否各自持有不同的状态变量。画布上的辅助线建议始终保留文字标识:蓝色出血、红色裁切、绿色安全区,避免仅凭颜色判断。

紧凑屏幕上的卡面编辑与属性面板

这一轮把“能不能重排”和“重排之后数据是否相同”分成了两个验收维度。手机上的单列页面与平板双栏页面使用的是同一组卡牌尺寸、元素ID和碰撞规则,断点只决定工具区的位置。接下来需要把完整的卡面状态做成可恢复的版本快照,在生成导出清单前重新检查素材和几何条件,防止最后一次编辑绕过保存或导出校验。

相关接口与约束可参照ContainerReader容器断点官方开发指导、ArkUI布局概述及ArkUI官方资源。其中容器断点是API 26引入的能力,毫米坐标投影和元素稳定ID则是应用自行实现的业务逻辑,二者不应混为系统接口。

Logo

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

更多推荐