同一张照片的原图和增强图,放在两个相邻的 Image 里,看着很容易比较;真正做成左右拖动的对照器,问题往往不是滑块有没有移动,而是两边有没有使用同一套坐标。左图裁掉了十几个像素,右图又按另一种比例缩放,分界线一动,桥栏杆的位置就错开了。这种错位会被误认为超分算法改变了结构。

本文用 CompareDock 做一套可复现的界面方案:任务号 CD-058,对照样例“石桥湖面”,预览区域固定为 360vp × 203vp,初始分界比例为 58%。按四舍五入,分界线坐标应为 209vp。图中的影像和状态是设计示例,不是实机截图,更不是图像超分模型质量的实验数据。

一、比较器首先要保证同一坐标系

很多产品把“原图”“增强图”各自交给一个可以自由铺满的容器。两个容器的宽高比、裁剪模式甚至圆角有一点不同,用户拖动时就会看到物体轮廓跳变。为了让比较结果可解释,我先固定双方的内容框:两张图片都从同一个素材裁剪位置开始,使用一致的 ImageFit.Cover 和相同的预览尺寸,只有上层图的可见区域随滑块变化。

演示资源命名为 compare_before 和 compare_after。它们是两张预先准备的、视角一致的本地图片,不是在点击滑杆时临时执行超分推理。是否有提升、锐化是否过度,要由真实资源和人的检查决定,组件不能替算法做结论。特别是摄影素材里常见的 EXIF 旋转和异步解码问题,应当在资源准备阶段统一处理,别把它们转嫁给 UI 裁剪。

界面上显示的 58% 指上层增强图占据展示框的宽度比例,不是“模型进度”,也不是图像清晰度分数。209vp 是 360 × 0.58 的舍入值。在高分辨率设备上,vp 到真实像素还会经过密度换算,显示比例与原图像素列数不能直接划等号。

二、先收口滑块值,再决定可见宽度

要解决的第一件事,是把任意输入约束在 0~100,并保证界面上的百分比、裁剪宽度和日志使用同一份状态。不要在多个回调里各写一次换算;更不要让 Slider、分界线和裁剪容器各自记一份进度。这里保留一个响应式百分比状态,衍生宽度只从它计算。

@Entry
@Component
struct ComparePage {
  @State private revealPct: number = 58;
  @State private alive: boolean = true;
  private readonly panelWidth: number = 360;
  private readonly panelHeight: number = 203;
  private readonly taskId: string = 'CD-058';

  private get revealWidth(): number {
    return Math.round(this.panelWidth * this.revealPct / 100);
  }

  private setReveal(input: number): void {
    if (!this.alive || !Number.isFinite(input)) return;
    this.revealPct = Math.max(0, Math.min(100, Math.round(input)));
  }
}

这里的 alive 不是系统事件,而是页面内部的保护状态,用来避免离页后继续更新示例状态。初始值 58 保证图片、示例日志与文中一致。Number.isFinite 用来挡住异常数字,Math.max/Math.min 约束边界,Math.round 避免小数带来的文本跳动。如果实际产品允许半个百分点移动,应同时修改步长、显示精度和坐标换算,不能只改一个地方。

这三个示例代码块均属于同一个 ComparePage 组件,按讲解重点拆开呈现,不能作为三个独立页面文件直接编译。

这段只给出数据模型;实际页面的资源装配和按钮状态还需在工程中补齐。没有加载到成对图片时,应显示“对照资源未就绪”并禁用滑块,而不是拿同一张图片冒充前后效果。

三、两张图重叠,只有上层做裁剪

第二个问题是避免拖动后内容缩放。我的取舍是让底层图片完整铺满 360vp × 203vp,上层图片也保持完整的内在尺寸,只用一个较窄且启用裁剪的容器露出左侧。分界线位置与上层可见宽度使用同一个 revealWidth;即使变为 0% 或 100%,也不需要重新加载图片。

@Builder
private comparePanel() {
  Stack({ alignContent: Alignment.TopStart }) {
    Image($r('app.media.compare_before'))
      .width(this.panelWidth).height(this.panelHeight)
      .objectFit(ImageFit.Cover)
    Column() {
      Image($r('app.media.compare_after'))
        .width(this.panelWidth).height(this.panelHeight)
        .objectFit(ImageFit.Cover)
    }
    .width(this.revealWidth).height(this.panelHeight).clip(true)
    Row().width(2).height(this.panelHeight)
      .backgroundColor('#FFFFFF')
      .position({ x: Math.max(0, this.revealWidth - 1), y: 0 })
  }
  .width(this.panelWidth).height(this.panelHeight)
  .borderRadius(12).clip(true)
}

Stack 的孩子按层次叠放,Column.clip(true) 负责限制上层图的绘制边界。被裁的是视口,不是上层图片的逻辑宽度:如果把 Image 自身直接缩成 209vp,ImageFit.Cover 可能改变可见内容,比较基准就丢了。分界线用白色细条标识切割位置,实际产品最好再加一个可触摸的把手,并扩大触控热区,避免只剩两像素可点。

这里选择固定宽度,是为了把问题缩小到一个可以讲清楚的静态场景。进入折叠屏、平板或分屏模式时,不能直接照抄 360vp。应根据容器实际可用宽度同步更新两个图片的外框和 revealWidth,先完成布局,再恢复比例。页面宽度变化时只保存百分比,不保存老的绝对坐标,能避免从 360vp 切到 600vp 时停在错误位置。

四、不要把滑动过程误记为处理进度

Slider 的 onChange 会给出数值,也会带上 SliderChangeMode。移动中更新界面,是实时反馈;结束时记录最终比例,才适合写入分析或偏好配置。频繁拖动期间如果把每个像素变化都写到数据库,会产生无意义的 I/O,还会让日志掩盖真正的加载错误。示例只记录 Moving 和 End 两种关键阶段。

Slider({ value: this.revealPct, min: 0, max: 100,
         step: 1, style: SliderStyle.OutSet })
  .width(this.panelWidth)
  .onChange((value: number, mode: SliderChangeMode) => {
    this.setReveal(value);
    if (mode === SliderChangeMode.Moving ||
        mode === SliderChangeMode.End) {
      console.info(`[CompareDock] ${this.taskId} mode=${mode}` +
        ` pct=${this.revealPct} x=${this.revealWidth}vp`);
    }
  })

官方对 Slider 的 Begin、Moving、End、Click 有明确的事件语义,不应假设任何一次点击都会先触发 Moving。这里把比例更新放在事件类型判断之前,所以即使用户直接点击滑轨,也能改变比较位置。日志里出现 pct=58 x=209vp 时,表示对照条的展示状态,不表示超分计算已经完成。

生命周期同样要收口。页面关闭时把 alive 改成 false,异步资源回调必须先检查页面是否仍在使用;如果持有动态创建的 PixelMap,还需要按所有权释放,不能因为此例用了静态 Image 资源就推断动态资源会自动按业务时机销毁。重新进入页面时应先恢复资源就绪状态,再决定是否恢复上次的滑杆比例。

五、用边界样例验证,而不是凭视觉感觉

当前演示界面把 58% 显示为 209vp,CD-058 只是一组预设任务字段。它同时给出原图和增强图的同源视角,是为了让开发者检查“边界线是否稳定”,不是提供算法效果证据。0% 应只见原图,100% 应只见增强图,58% 应看到位于 209vp 的分隔线;快速反复滑到两端,不应改变图片整体的缩放比例。

建议在 DevEco 中做三层验收。第一层看静态构图:两张图的建筑边缘在分界线两侧是否连续。第二层看状态日志:pct、x、页面任务号是否始终一致。第三层用真实资源检验:横屏切换、系统字体放大、边栏收缩、图片解码失败时的占位和可交互性。只看一张生成的示意截图,无法验证真实像素对齐,也无法说明性能表现。

真正上线时,资源加载、展示坐标和算法效果是三个独立责任。对比器只解决“如何公平展示两个结果”,不要把来源不一致的两幅图、不同裁剪的缩略图和未经核实的超分输出塞进同一个比较窗口。把这条边界写清楚,比做一个更花哨的滑动动画重要。

六、给后续扩展留一道明确的门

如果未来需要对照 4K 图片,优先评估分级预览和纹理内存,而不是把全分辨率图像长期留在页面。Image 使用本地资源的示例与运行时创建 PixelMap 的链路不同:后者应处理读取失败、页面销毁、替换前的引用持有和异步释放。对于折叠屏适配,可在容器尺寸稳定后基于百分比重新计算裁剪宽度,但不要在旧坐标上继续叠加偏移。

本篇的结果,是一个数据统一、逻辑边界明确的设计与代码示例:CD-058、360vp × 203vp、58% 和 209vp 可以逐项核对。它没有实机 FPS、内存峰值或画质评测结论。读者应将本文代码接入自己的同源图片资源并做编译和真机检查,才能形成可发布的工程证据。

官方参考:华为 ArkUI Slider 组件 API(Slider.onChange、SliderChangeMode),Stack 层叠布局与剪裁实践。

  • https://developer.huawei.com/consumer/en/doc/harmonyos-references-V14/ts-basic-components-slider-V14
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-layout-development-stack-layout-V5
  • https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1513
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐