手机上放一张“原图 / 超分结果”对照图,看起来只是中间有一根可以拖动的分隔线。但把预览做成真正可复用的组件时,会遇到一种很隐蔽的问题:竖线看上去跟着手指走,实际遮罩边界却慢半拍;切到窄窗口后,线已经到了边缘,触点仍然被上一次的宽度解释。用户看到的是一条抖动的线,开发者容易误以为超分图片没加载完。

这里不讨论图像增强模型,更不把视觉示意当作算法测试。Demo 叫 SplitRuler,任务号 SR-742,采用两张本地演示图片表示原图与“增强图”,并只验证叠加预览的坐标换算。示例合同固定为图片标称尺寸 1080×720、可交互视口 320×240 vp、当前触点本地横坐标 208 vp,对应 65% 分隔比例。真正的超分能力没有调用,状态标记为 DEMO_ONLY / SR_NOT_RUN。

上图是概念封面,下文的 DevEco 与手机界面也是按同一套数据绘制的演示图,不是实际编译、设备触摸或算法性能证据。

一、拖拽线背后有三种不同的距离

1. 图片像素、布局 vp 和指针位置不能混用

原始位图 1080×720,描述的是图片自身的数据坐标;页面给 Canvas 的 320×240 vp,是组件可交互的逻辑布局大小;手指按下时产生的是事件坐标。三者都能用数字表达,却绝不能直接互相当成对方。在折叠屏、多窗口和屏幕缩放场景里,这种侥幸尤其容易暴露。

页面第一次测量可能拿到 320 vp,随后容器被系统调整到 296 vp。若手势处理还按旧宽度计算,实际已经触到右侧边缘的用户会得到小于 100% 的比例。反过来,把原图的 1080 当作组件宽度,208 会被算成不到两成;线条与遮罩的关系自然错位。示例里我们统一只存无量纲的 0~1 比例,把当前宽度当成计算输入,而不是把 208 这样的临时坐标长期留在状态里。

2. 先收敛数据再参与绘制

这个小函数只做一件事:把当前 Canvas 内的本地 x 坐标折算为比例。它不依赖异步图片加载,也不修改 UI,便于用边界输入直接做单元测试。

export function normalizeDivider(localX: number, widthVp: number): number {
  if (!Number.isFinite(localX) || !Number.isFinite(widthVp) || widthVp <= 0) {
    return 0.5;
  }
  return Math.min(1, Math.max(0, localX / widthVp));
}

export function dividerX(ratio: number, widthVp: number): number {
  const safeRatio = Number.isFinite(ratio) ? Math.min(1, Math.max(0, ratio)) : 0.5;
  return safeRatio * Math.max(0, widthVp);
}

它可以处理负数、超过右边界、零宽度以及非有限数字。为什么出错时返回 0.5?因为演示预览的中线是可辨认的稳定状态;实际产品可以把错误视为禁用拖动,但不应让 NaN 继续流入绘图命令。注意这些函数处理的是已经确认属于当前组件的本地 vp 坐标,不能直接传入屏幕绝对坐标。

二、把 Canvas 当成“刻度层”,不要当作图片算法

1. 图像和刻度分层才方便收口

展示层可使用两个 Image 与业务遮罩组成对照区域,Canvas 单独画刻度线、分隔位置和中间圆点。真正的超分结果尚未就绪时仍然可以绘制刻度层;图片加载失败也不会让可交互坐标系突然改变。这里的截图仅展示 UI 合同,没有证明任何增强效果。

Canvas 的 onReady 表示绘制上下文可以开始使用;它不是某张照片解码完成的回调,更不等于该任务实际完成。必须把“画布准备好”和“业务预览数据可用”拆开。下面的组件只绘制分隔指示,参数以 320×240 vp 的演示视口为例,接口使用 ArkUI Canvas 与 CanvasRenderingContext2D。

@Entry
@Component
struct SplitRulerPage {
  @State private ratio: number = 0.65;
  private readonly viewportWidthVp: number = 320;
  private readonly viewportHeightVp: number = 240;
  private readonly settings: RenderingContextSettings = new RenderingContextSettings(true);
  private readonly ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private canvasReady: boolean = false;

  private drawGuide(): void {
    if (!this.canvasReady) return;
    this.ctx.clearRect(0, 0, this.viewportWidthVp, this.viewportHeightVp);
    const x = dividerX(this.ratio, this.viewportWidthVp);
    this.ctx.strokeStyle = '#F8FAFC';
    this.ctx.lineWidth = 3;
    this.ctx.beginPath();
    this.ctx.moveTo(x, 0);
    this.ctx.lineTo(x, this.viewportHeightVp);
    this.ctx.stroke();
  }

  build() {
    Column({ space: 12 }) {
      Text('SplitRuler · SR-742').fontSize(18).fontWeight(FontWeight.Bold)
      Text(`分隔比例 ${Math.round(this.ratio * 100)}%`)
      Canvas(this.ctx)
        .width(this.viewportWidthVp)
        .height(this.viewportHeightVp)
        .backgroundColor('#D8E6F1')
        .onReady(() => { this.canvasReady = true; this.drawGuide(); })
    }
  }
}

清理上一帧的线条是必要的,否则保留模式下历史刻度可能留在画布上。示例的 clearRect 只负责清除刻度层,并没有清理真实图片内容;组件销毁后它也不持有相册数据库、网络连接或大图句柄。以后若换成 PixelMap 渲染,必须另外处理图片对象的创建、替换与释放,不能照搬这个轻量状态模型。

2. 本地触点有明确的进入边界

触摸事件到来时,不应该每移动一个像素就立刻驱动一次昂贵的超分推理。组件只更新比例和刻度位置;图像真实像素的重新合成,应由独立任务根据采样频率或松手时机处理。这个切分对多形态设备比较重要,因为窗口改尺寸与手指拖动可以同时发生。

// 加入 SplitRulerPage,Canvas 节点后调用此事件处理方法。
private handleLocalTouch(event: TouchEvent): void {
  if (event.type !== TouchType.Down && event.type !== TouchType.Move) return;
  const point = event.touches[0];
  if (!point) return;
  this.ratio = normalizeDivider(point.x, this.viewportWidthVp);
  this.drawGuide();
}

// Canvas(this.ctx) ...
//   .onTouch((event: TouchEvent) => this.handleLocalTouch(event))

注释中的挂载形式是为了突出这一层职责,实际集成需将 .onTouch 写到上述 Canvas 修饰链上。示例演示宽度是常数,生产工程应把实际布局测量值保存并在变化时重算,且确认事件 x 的局部坐标语义。拖动中产生的瞬时比例只服务画面,不应直接作为“已输出超分文件”的业务标记。

这张白色 DevEco 风格图演示了 SplitRuler 的工程目录、坐标函数、右侧模拟器布局以及底部预设日志。它是排版演示,不是 DevEco 的真实编译截图;日志文字表示预定验收格式,不是已采集到的 HiLog。

三、调试应当先检查不变量

1. 少看“顺不顺”,多看“算得对不对”

示例最关键的断言是 208 ÷ 320 = 0.65。其次检查 -10 应钳制为 0,420 应钳制为 1,widthVp=0 时显示 0.5 的安全中线。若改成 296 vp,应该用同一 65% 计算新的位置 192.4 vp,而不是仍沿用旧值 208。这样就能把数值问题与图片加载顺序问题分开。

另外测试横向触点越过画布、触摸结束、连续转动窗口、应用退到后台再回来。尤其是横竖屏切换后,Canvas 绘制区域的实际大小可能变化;组件内缓存的固定演示尺寸不能当成设备宽度。建议把实际测量值与当前比例分别记录,日志里永远带上任务号和测量单位,避免不同坐标系数据混在一起。

2. 让演示数据形成唯一口径

本轮约定的日志文案为 SplitRuler SR-742 localX=208 width=320 ratio=65%,紧随其后的是 SplitRuler SR-742 preview=DEMO_ONLY super_resolution=NOT_RUN。界面只报告预览比例 65%,而不是声称输出图分辨率提高多少。只要没有真实调用超分能力,就不出现“增强完成”或具体性能毫秒数。

这张 9:16 纯屏幕图里的时间、5G、Wi-Fi、信号、电量均为模拟状态栏元素;对照图片区也是示意素材。它和 DevEco 演示图共用 SR-742、1080×720、320×240 vp 和 65%,任何一个数变化,都应重新核对正文、示意图与日志契约。

四、哪些边界不值得硬套

对于仅展示一张固定宽度图片的页面,使用简单 Stack、Image 和遮罩会比 Canvas 更直观。Canvas 适合抽出刻度、参考线、辅助标记等小型绘制层,未必适合直接承载大规模图片像素操作。遇到复杂叠加、频繁重绘或专业图像编辑,还要单独评估原生 Drawing 的能力与开销,不能因示例轻量就推断移动端性能。

另一个容易忽略的边界是系统无障碍:拖动分隔线不应成为唯一比较途径。正式产品应提供可聚焦的 Slider、数值反馈与清晰标签,使读屏和键盘操作能够得到同一结果。图片宽度和屏幕密度变化之后,应再次核对这条无障碍路径是否仍然可用,而不是只看手势效果。

本轮得到的只是一个可推导的 UI 约束:分隔比例是主状态,组件宽度是即时输入,图像像素是另一套坐标。把这三者分离后,画面错位更容易被定位,换尺寸也有明确恢复入口。真实超分服务、真实图片解码、绘制帧率和设备触摸精度均未在本文环境中运行验证,上架或教学复用前仍需在目标版本 DevEco 和真机上确认。

五、官方依据与复查入口

这里采用文档可确认的 Canvas、CanvasRenderingContext2D、onReady、绘制及擦除操作;normalizeDivider 和 dividerX 是本文业务自定义函数,并非 HarmonyOS 系统 API。

Logo

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

更多推荐