阳台小花园开发手记 03|HarmonyOS 7.0 互动卡片:叶片伸出边框仍与花盆对齐

1. 前言

前两篇为小花园确定了两套界面的职责,并整理了点击与摇动的触发路径。普通态显示薄荷名称、最近观察和日记入口;激活态让瓢虫从叶后探出来。现在,我想让叶片再向上伸一点,越过卡片边框。

这件事很容易被理解为"把画布放大"。但画布变大以后,原来位于画布中央的花盆应该放在哪里?如果仍让整个页面自动居中,花盆可能随布局一起移动;如果把窗口坐标直接填进局部偏移,位置又会受到卡片在桌面上所处位置的影响。

用户只想看见叶片舒展开来。花盆突然跳一下,会让这段动作显得像换了一盆植物。

本文继续使用绑定薄荷 plant-001 的2×2桌面动态卡片。普通页面仍是 MintCard,激活页面仍是 MintLive,对应扩展为 MintFormAbility 和 MintLiveAbility。植物资料、观察日记以及花盆、叶片、瓢虫的本地资源都沿用前两篇。

我们只处理一个问题:怎样申请足够的绘制空间,同时让原背景与花盆留在正确的位置?倾斜控制和完整的进出场衔接,分别留给后面的文章。

2. 方案

遇到位置错误时,我会先寻找坐标的参照物。一个名为 left 的数值并没有完整表达位置;还需要知道,它是从哪里开始测量的。

这张卡片涉及三个矩形:

对象 参照物 在本篇中的用途
getFormRect 返回的矩形 卡片所在窗口 取得当前卡片位置与尺寸
申请中的 area 原卡片左上角 描述希望获得的出框区域
liveFormInfo.rect 激活态UI 定位其中的原卡片背景

把第一个矩形的位置复制给第二个,看起来省去了一次计算,实际上却改变了数值的含义。桌面位置属于窗口布局;出框偏移描述的是卡片周围要多留多少空间。卡片移动到桌面另一处时,前者会变,后者未必需要变。

因此,我选择将区域计算收在一个只接收宽高的函数里。窗口位置保留在日志中,不参与居中申请。进入激活态后,再使用回调提供的相对矩形定位背景。

场景动效规定,2×2卡片的最大区域宽高均为原区域的150%,并以原卡片中心对齐。实际申请必须完整包含原卡片,同时不能越出最大区域。

这给出了三层关系:原卡片在里面,实际申请包住它,最大许可区域限制最外面的边界。只检查宽高是不够的。一个很小的矩形也可能放错位置,遗漏原卡片的一条边。

本篇选择居中申请,便于解释和检查。它是项目的布局选择;区域是否合法,仍由四条边的包含关系决定。叶片需要多少额外空间,就申请多少,不必每次都用满上限。

申请区域按vp计算。截图里的像素属于另一个测量口径,不能直接抄进参数。出框能力本身也并非HarmonyOS 7.0首次提供;本系列使用的API26增量,仍是上一篇的 shake 入口。

3. 实现

先把几何计算从系统调用中分离出来。下面的类型是项目内部的数据结构,接入层负责从当前SDK返回值中提取尺寸,并统一单位。

ts 复制代码
interface CardSize {
  width: number;
  height: number;
}

interface LocalArea {
  left: number;
  top: number;
  width: number;
  height: number;
}

function validSize(size: CardSize): boolean {
  return Number.isFinite(size.width) &&
    Number.isFinite(size.height) &&
    size.width > 0 && size.height > 0;
}

每次申请前,通过 getFormRect 获取当前尺寸。读取失败或尺寸无效,就结束本次请求。默认填入一个"看起来合理"的宽高,会把数据问题藏到布局里;使用上次的尺寸,也可能把另一次布局的结果带进来。

设原卡片宽高为W、H,左上角为局部原点。倍率为s时,横向增加的宽度是 (s-1)W。居中意味着左右各分一半,因此左边界需要向负方向移动一半:

ts 复制代码
function centeredArea(
  size: CardSize,
  scale: number
): LocalArea {
  if (!validSize(size)) {
    throw new Error('Invalid card size');
  }
  if (!Number.isFinite(scale) ||
      scale < 1 || scale > 1.5) {
    throw new Error('Invalid overflow scale');
  }

  return {
    left: -(scale - 1) * size.width / 2,
    top: -(scale - 1) * size.height / 2,
    width: scale * size.width,
    height: scale * size.height
  };
}

取 s=1.5,便得到最大申请:左上偏移为 -0.25W、-0.25H,宽高为 1.5W、1.5H。负号表示区域向原卡片左侧、上侧扩展。

为了贯穿后面的计算,我们取W、H均为160vp,倍率为1.25。申请结果是 (-20,-20,200,200);取最大倍率时,则是 (-40,-40,240,240)。160vp只用于这组演算,运行时的尺寸来自接口。

生成区域与检查区域值得分开。生成函数表达当前采用的策略;检查函数表达所有申请都必须遵守的边界。以后即使调整上下空间的分配,边界检查仍然有用。

令申请的右边界为 left+width,下边界为 top+height。检查可以直接沿着这四条边写:

ts 复制代码
function areaProblem(
  size: CardSize,
  area: LocalArea
): string | undefined {
  if (!validSize(size)) {
    return 'invalid-size';
  }
  if (!Number.isFinite(area.left) ||
      !Number.isFinite(area.top) ||
      !Number.isFinite(area.width) ||
      !Number.isFinite(area.height) ||
      area.width <= 0 || area.height <= 0) {
    return 'invalid-area';
  }

  const right = area.left + area.width;
  const bottom = area.top + area.height;

  if (area.left > 0 || area.top > 0 ||
      right < size.width || bottom < size.height) {
    return 'does-not-contain-card';
  }
  if (area.left < -0.25 * size.width ||
      area.top < -0.25 * size.height ||
      right > 1.25 * size.width ||
      bottom > 1.25 * size.height) {
    return 'outside-maximum';
  }
  return undefined;
}

返回原因比一个布尔值更适合这里。看到"未包含原卡片",可以检查位置和尺寸;看到"超过最大区域",则应检查扩展边界。这些字符串属于应用诊断信息,不能当作系统错误码。

接入顺序由此变得明确:读取尺寸,计算区域,检查区域,再交给已有的 requestOverflow 请求路径。接入层还要捕获尺寸读取、计算和请求阶段的异常,记录后结束本次尝试。申请时长仍不超过3500ms,请求失败不循环重试。

点击和经正式来源机制确认的摇动共用这条路径。上一篇尚需核验的摇动来源映射,仍然要对照正式接口完成;几何检查不会替它提供来源证据。普通刷新也不会因为新增了区域计算,就获得播放动效的含义。

申请区域确定以后,还有另一半工作:在激活UI里放回原来的背景。

在上述200×200的演算区域中,原卡片左上角相对新区域位于 (20,20),宽高仍为160×160。这个结果帮助我们理解转换关系;实际布局应读取 liveFormInfo.rect,而不是把20写进页面。

接入层将回调矩形和实际圆角写入当前会话的 LocalStorage,页面通过对应状态绑定读取。之后仍按第01篇的顺序同步加载:

ts 复制代码
// onLiveFormCreate 的同步路径;存储已包含背景几何信息。
session.loadContent('pages/MintLive', initialStorage);

loadSnapshot('plant-001')
  .then(applySnapshot)
  .catch(showReadFailure);

这里的数据函数沿用项目封装。背景的位置和默认画面不需要等待观察日记,异步读取也不应推迟页面加载。

布局时,将原背景作为一个按回调矩形定位的区域,保留它的尺寸和圆角。花盆锚点、名称与日记入口继续相对这个区域排列;装饰层才使用更大的空间。不要因为根容器变大,就把花盆一起按比例放大,也不要把原背景拉伸到整个激活区域。

装饰层还需要检查父容器的裁剪设置。系统允许更大的绘制范围,并不会自动取消页面内部的裁剪。背景的圆角裁剪可以保留,但需要出框的叶片不应被收在同一个原卡片裁剪容器里。

日记按钮继续放在原卡片范围内。出框扩大的是渲染空间,原边界外的交互事件不响应。把按钮画到叶片尖端,会增加一个用户看得见却无法按预期操作的入口。

4. 检验

这套实现有两个不同的问题需要检查:提交的矩形是否正确,以及显示出来的背景是否对齐。请求返回成功,只回答了请求阶段的问题。

先用固定输入检查计算规则。以下各行均使用160×160vp的原卡片,参数顺序为 left、top、width、height:

用例 申请区域 预期判断
不出框 (0,0,160,160) 合法
有限出框 (-20,-20,200,200) 合法
紧贴最大边界 (-40,-40,240,240) 合法
左侧超出1vp (-41,-40,240,240) 超过最大区域
遗漏原卡片左边 (1,0,159,160) 未完整包含原卡片
原卡片宽度为0 不生成区域 尺寸无效

这些数值刻意贴近边界。只检查一个居中的正常输入,很难发现"仅比较宽高"的错误。

还可以增加一个性质检查:窗口位置不参与函数输入。因此,只要W、H和倍率不变,计算结果就应保持不变。这个性质直接对应桌面上的操作:把卡片挪到另一处,再激活一次。

真机检查使用同一设备、系统、SDK、资源与构建,先固定点击入口,避免同时改变来源处理和几何逻辑。在桌面位置P1录制一次激活,等待结束后把卡片移到P2,再执行同样的操作。每个条件重复三次,保留全部记录。

移动前后先比较尺寸。若桌面重新布局改变了宽高,就按新尺寸重算,并将其作为不同条件;不能继续要求两次申请完全相同。移动本身也要放在动效结束后,否则拖动触发的中断会混入对齐问题。

记录需要把输入、判断和画面关联起来:

text 复制代码
env: device=... os=... sdk=... tools=... build=...
case: id=... position=P1 trigger=click
context: formId=... plantId=plant-001 runId=...
geometry: windowRect=... area=... unit=vp
validation: problem=...
request: submitted=... result=...
live: rect=... radius=... create=... destroy=...
evidence: video=... frame=... cropOrigin=... scale=...

runId是应用侧关联编号。被本地检查拒绝的输入,应记录 submitted=false,系统结果留空。需要观察系统如何拒绝非法区域时,另设受控实验分支,保留真实提交参数与返回信息。

视觉检查选择普通态和激活态背景已经呈现、能够比较的帧,关注背景边缘、圆角、花盆底部与叶片基点。首尾过渡期间的完整表现留给第05篇,不把过渡中的一帧直接解释为稳定布局偏移。

P1、P2各自保留全屏原始画面,以证明桌面位置确实改变。制作局部叠图时,同一组前后帧使用一致的裁剪原点与缩放比例。若先把两张图里的花盆分别挪到中央,再比较它们,恰好会抹掉我们要寻找的偏移。

截图测量以px记录,同时注明设备像素密度和测量精度。不要把vp日志与px距离直接相减。没有共同时间基准时,也不从日志时间推断某一帧必然已经呈现。

结果按用例填写实际表现和证据编号,尚未执行的项保留待测。尺寸获取失败、系统拒绝、提前中断和背景错位分别归类;请求成功却没有可见画面,也需要继续检查生命周期与录屏。每次失败后还要确认普通态摘要和日记入口可用。

5. 结尾

花盆能否留在原位,取决于每个位置值是否仍在正确的参照系里。申请区域以原卡片为原点,最大边界限制它的范围;激活页面再按回调矩形安放原背景。沿着这条关系,叶片得到额外空间,花盆仍有明确的布局依据。

区域生成函数、合法性检查、输入样本和坐标表也构成了一个小型区域计算器的核心:输入尺寸与申请参数,输出矩形及拒绝原因,再用三层矩形展示关系。真机日志和叠图负责检查它与实际呈现是否一致。

下一篇继续使用同一个 MintLive 和薄荷 plant-001,让倾斜控制瓢虫在合法区域内移动。区域边界确定以后,我们便可以集中讨论位移的抖动与响应,而观察日记仍由用户亲手记录。

相关推荐
熬夜磕代码丶1 小时前
RN鸿蒙空间音频实战
华为·音视频·harmonyos
行者-全栈开发2 小时前
【鸿蒙心迹】通知与推送集成——emitter 本地通知与推送服务全链路(HarmonyOS 7.x)
华为·harmonyos
Nerd(BUG版)2 小时前
鸿蒙API18~26新增V2组件全解析
microsoft·华为·harmonyos
贾伟康2 小时前
HarmonyOS API 26 Navigation 菜单项刷新态:stale 数据、重试与一致性
harmonyos·arkts·arkui·navigation·多设备
Felven2 小时前
华为鲲鹏920s芯片serdestool工具使用说明
linux·运维·服务器·华为·鲲鹏920·serdestool
c2892480843s14 小时前
【鸿蒙心迹】网络请求封装实战:RCP 选型、拦截器与弱网预算
移动开发·网络请求·harmonyos·鸿蒙·rcp
HarmonyOS_SDK18 小时前
【剪贴板体验优化】前置预校验,航旅纵横权限弹窗体验更智能
harmonyos
SoFarAway19 小时前
拆解华为一碰传
华为·逆向
贾伟康20 小时前
【共创稿事节】HarmonyOS 7.0 Progress进度体系:从接入到生产验证
性能优化·实战·harmonyos·arkts·arkui