HarmonyOS桌游卡版连载02:图文碰撞与越界提示【共创稿事节】
一张桌游卡的成品尺寸确定以后,真正影响阅读的是版面元素怎样共存。“森林邮局”卡面上有猫头鹰插画、名称横幅、效果说明和角落装饰。每个部分单独看都能放进卡片,但用户调整名称字号后,标题区可能压住插画,说明文字也可能被挤出安全框。仅靠肉眼检查当前屏幕并不稳定:切换预览缩放时,边缘很细的重叠会消失,重新导出又可能出现。上一篇已经统一毫米坐标,本篇不改成品规格,而是在这套坐标上建立可解释的图文区域约束。

元素碰撞从业务身份开始
卡面上要检验的并非所有可见图形。背景纹理可以延伸到出血线以外,角标装饰也可能特意覆盖局部插画;名称、正文和关键图形却需要保证安全。为每个元素设置稳定ID、类型、坐标、宽高与是否需要保持在安全框内的标志,再把允许重叠的关系写成显式规则。以后用户移动标题时,只有与标题相关的碰撞需要重新计算,而不是把背景画布也放进检测列表。
实例规格沿用成品63×88毫米、四周出血3毫米、安全内缩5毫米,画布69×94毫米,安全矩形(8,8,53,78)。初始状态的插画框位于(10,12),宽49、高45;标题横幅在(13,60),宽43、高9;说明区位于(12,72),宽45、高11。它们都处于安全框之内,也没有互相遮挡。假如把标题移到y=53,标题将与插画在纵向重叠4毫米;若把说明区向下移到y=78,其下沿就到89毫米,超过安全区下沿86毫米3毫米。检测结果应指出“标题与插画相交”和“说明区下沿越界”,而非只展示一个笼统红色数字。
项目结构沿用model/CardGeometry.ets的矩形定义,在model/CardElement.ets描述卡面元素,在service/LayoutInspector.ets负责重叠及越界检测,在pages/LayoutCheckPage.ets呈现风险列表和定位动作。这些检查不需要访问相册、网络或导出文件,因此可以在用户拖动过程中即时执行。实际图片解码与画面资源管理应另设模块,避免几何检测的纯函数依赖图片读取状态。
import { RectMm } from '../model/CardGeometry';
export type ElementKind = 'ILLUSTRATION' | 'TITLE' | 'TEXT' | 'ORNAMENT';
export interface CardElement {
id: string;
label: string;
kind: ElementKind;
box: RectMm;
keepInsideSafe: boolean;
}
export interface LayoutWarning {
code: 'OVERLAP' | 'OUTSIDE_SAFE' | 'INVALID_BOX';
firstId: string;
secondId: string;
detail: string;
}
export function overlaps(a: RectMm, b: RectMm): boolean {
return a.x < b.x + b.width && b.x < a.x + a.width &&
a.y < b.y + b.height && b.y < a.y + a.height;
}
export function fullyInside(inner: RectMm, outer: RectMm): boolean {
return inner.x >= outer.x && inner.y >= outer.y &&
inner.x + inner.width <= outer.x + outer.width &&
inner.y + inner.height <= outer.y + outer.height;
}
这段相交判定使用半开边界。两块矩形恰好在边上接触时不会被计为重叠,有实际交集才提示;这样的处理方式适合纯几何排版。若设计要求标题与插画之间至少留1毫米间距,下一层规则可先扩大检测矩形再调用同一函数,不必悄悄修改“相交”的基本定义。fullyInside对完整矩形判断,不能只检查左上角,因为左上角合法而右下角出界是常见问题。
从一个风险到可定位的解释
检测结果必须保留元素ID而不仅是位置。用户在列表上点击“标题与插画碰撞”,编辑器能按照firstId找到标题并聚焦;说明框被移到安全区以外时,能高亮说明元素及它突破的边界。如果只保存数组下标,删除一个装饰元素后,下标将错指别的控件。稳定ID同时方便后续做撤销与模板版本记录。
有些卡牌设计允许装饰层覆盖背景,或者标题横幅覆盖专门留出的半透明底色,不能把这种允许关系误判为错误。最简单的做法是由设计规格给出允许重叠的ID对,检查器在循环中跳过这些例外。例外规则必须随模板保存,不能靠页面临时隐藏提示;否则换一台设备打开会出现不同结果。对单个元素仍要检查宽高是否为正数、坐标是否为有限值,异常矩形不进入正常相交算法,否则会出现NaN绕过所有比较的情况。
import { CardElement, LayoutWarning, overlaps, fullyInside } from '../model/CardElement';
import { RectMm } from '../model/CardGeometry';
export function inspectLayout(elements: CardElement[], safe: RectMm,
allowedPairs: string[]): LayoutWarning[] {
const warnings: LayoutWarning[] = [];
const invalidIds: string[] = [];
for (const e of elements) {
const b = e.box;
if (![b.x, b.y, b.width, b.height].every((v: number) => Number.isFinite(v)) ||
b.width <= 0 || b.height <= 0) {
warnings.push({ code: 'INVALID_BOX', firstId: e.id, secondId: '',
detail: `${e.label}的尺寸不完整` });
invalidIds.push(e.id);
continue;
}
if (e.keepInsideSafe && !fullyInside(b, safe)) {
warnings.push({ code: 'OUTSIDE_SAFE', firstId: e.id, secondId: '',
detail: `${e.label}超出安全框` });
}
}
for (let i = 0; i < elements.length; i++) {
for (let j = i + 1; j < elements.length; j++) {
const a = elements[i]; const b = elements[j];
if (invalidIds.includes(a.id) || invalidIds.includes(b.id)) { continue; }
const pair = [a.id, b.id].sort().join('|');
if (allowedPairs.includes(pair)) { continue; }
if (a.box.width > 0 && a.box.height > 0 &&
b.box.width > 0 && b.box.height > 0 && overlaps(a.box, b.box)) {
warnings.push({ code: 'OVERLAP', firstId: a.id, secondId: b.id,
detail: `${a.label}与${b.label}区域相交` });
}
}
}
return warnings;
}
allowedPairs是业务字段,不属于ArkUI系统接口。校验器可在任何页面调用,同样能在导出前重新运行。输入存在非法框时,代码把它的ID记入invalidIds并在碰撞遍历中跳过,避免一条数据异常同时触发很多噪声提示。这样“尺寸不可计算”与“版面需要调整”便保持了明确的处理层级。卡面元素少时两层循环的开销很小,若将来一次检查几百个区域,再考虑按网格空间索引减少无关配对。

将模型风险变成编辑反馈
页面状态分为当前元素列表、检测结果和被选中的元素ID。每次拖动结束时替换对应元素的坐标,立即调用inspectLayout刷新风险列表;拖动中如果每个手势事件都更新全部组件,可能使旧风险闪烁,适合采用页面级合并更新。初始示例先将标题移到53毫米的位置、说明框移到78毫米,用户点击“自动回到安全布局”后再还原初始坐标。虽然页面能提供快速修复,但不应直接覆盖用户已保存的卡面版本,修复应当是一项明确的编辑操作。
对于颜色的使用,也不要只靠红绿区分:碰撞警告附“图文相交”字样,越界警告附“超过安全框”,修复后显示“无碰撞、无越界”。高亮边框至少与被选中的元素绑定,不应该仅凭碰撞列表顺序确定位置。手机上提示列表可放在卡面预览下方,平板则能把诊断列表移到右侧栏;同一份风险数组在两种布局中保持完全一致,为下一步引入多端布局奠定基础。
import { CardElement, LayoutWarning } from '../model/CardElement';
import { inspectLayout } from '../service/LayoutInspector';
import { buildGuides, CardSpec } from '../model/CardGeometry';
@Entry
@Component
struct LayoutCheckPage {
@State private elements: CardElement[] = [
{ id: 'art', label: '插画', kind: 'ILLUSTRATION',
box: { x: 10, y: 12, width: 49, height: 45 }, keepInsideSafe: true },
{ id: 'title', label: '名称', kind: 'TITLE',
box: { x: 13, y: 53, width: 43, height: 9 }, keepInsideSafe: true },
{ id: 'effect', label: '说明', kind: 'TEXT',
box: { x: 12, y: 78, width: 45, height: 11 }, keepInsideSafe: true }
];
@State private warnings: LayoutWarning[] = [];
@State private selectedId: string = '';
aboutToAppear(): void { this.refreshWarnings(); }
private refreshWarnings(): void {
const spec: CardSpec = { trimWidthMm: 63, trimHeightMm: 88,
bleedMm: 3, safeInsetMm: 5 };
this.warnings = inspectLayout(this.elements, buildGuides(spec).safe, []);
}
private restoreLayout(): void {
this.elements = this.elements.map((item: CardElement) => {
if (item.id === 'title') {
return { ...item, box: { ...item.box, y: 60 } };
}
if (item.id === 'effect') {
return { ...item, box: { ...item.box, y: 72 } };
}
return item;
});
this.refreshWarnings();
}
build() {
Column({ space: 12 }) {
Text('森林邮局 · 版面检查').fontSize(22)
Text(`发现${this.warnings.length}处需要核对的位置`)
List({ space: 8 }) {
ForEach(this.warnings, (w: LayoutWarning) => {
ListItem() {
Text(w.detail).fontColor('#B24520').padding(12)
.onClick(() => { this.selectedId = w.firstId; })
}
}, (w: LayoutWarning) => `${w.code}:${w.firstId}:${w.secondId}`)
}.height(240)
Text(`当前定位:${this.selectedId || '尚未选择'}`)
Button('调整为安全布局').onClick(() => this.restoreLayout())
}.padding(16)
}
}
这段页面代码以普通ArkUI列表展示风险,预览上的矩形描边复用第一篇的Canvas绘制能力即可。需要特别注意@State数组的更新方式:用map返回新数组有助于让刷新关系清楚,避免修改数组成员后忘记同步视图。aboutToAppear适合装载默认状态与触发首次检查;如果用户切换页面后重新回到编辑区,应从保存的版面数据恢复元素,而不是再次将所有元素重置到示例坐标。
越界与碰撞的调试次序
出现异常警告时,先核对坐标原点是否统一。把毫米值与像素值混进同一组矩形,是最容易导致“看起来不碰撞、算法却提示相交”的原因。然后检查安全框是否由当前规格生成:成品63×88、出血3、安全边距5,对应(8,8,53,78),如果图层采用了旧规格,便会系统性偏移。最后看元素ID是否重复,若两个区域共用ID,定位和允许重叠规则都会产生歧义。
错误处理应区分可修复与不可计算两类。名称太靠近插画属于可修复的排版问题,保留编辑态并给出建议位置;坐标为非有限值则属于不可计算,应阻止把这组数据纳入导出快照。设计师故意让装饰元素伸出出血边界时,也不应把它解释成标题越界。可让配置明确指定装饰性元素不强制位于安全区,同时继续检查标题、说明、编号等重要文本。这样既给创作留空间,又能让约束保持可追溯。

这里的重叠判定仍是轴对齐矩形规则,不适用于旋转文字或不规则剪影的精确交集。加入旋转和矢量轮廓之前,先把矩形作为交互包围盒,提示中写明“区域相交”,避免把它说成实际可见像素发生覆盖。若卡面图片经过裁剪,模型里的插画包围盒与可见区域必须同步更新。修复结果最终以同一份毫米数据重新检查,不能凭预览绿标就跳过导出前复核。
从第一篇的“尺寸口径统一”走到这一篇的“元素约束检查”,编辑器已经有了一条清楚的工程路径:用户修改毫米位置,模型输出风险,页面用稳定ID定位,再以新的位置重算。下一步把这套检查区分别放入手机窄屏和平板分栏时,所改变的应该只是页面编排,不是碰撞与越界规则。
相关官方参考为ArkTS基础语言说明、ArkUI列表与布局原则、状态变量变更通知。后续适配API 26将使用ContainerReader容器断点;当前矩形相交函数是业务算法,并非HarmonyOS提供的独立系统接口。
更多推荐

所有评论(0)