鸿蒙 HarmonyOS 6 | Scroll组件手势缩放能力全解析
图片浏览、设计稿预览、文档查看,这几类页面有一个共同点,内容尺寸通常都会超过可视区域。用户想看细节,就得放大;内容放大之后,又得能自由拖动浏览。过去做这类交互,开发者往往要自己接 PinchGesture、算缩放中心、处理滚动和缩放之间的手势冲突,代码写起来不轻松,调试成本也不低。鸿蒙 6 在 API 20 这条线上把 Scroll 的手势缩放能力补进来了,相关接口已经进入 ArkUI 的新增能力
前言
图片浏览、设计稿预览、文档查看,这几类页面有一个共同点,内容尺寸通常都会超过可视区域。用户想看细节,就得放大;内容放大之后,又得能自由拖动浏览。过去做这类交互,开发者往往要自己接 PinchGesture、算缩放中心、处理滚动和缩放之间的手势冲突,代码写起来不轻松,调试成本也不低。鸿蒙 6 在 API 20 这条线上把 Scroll 的手势缩放能力补进来了,相关接口已经进入 ArkUI 的新增能力范围。
这套能力落到工程里,主要解决两件事。第一,缩放这件事终于成了 Scroll 的原生能力,不需要继续靠外层手势和内部状态机拼接。第二,缩放后的自由滚动、边界回弹、状态回调都被统一进了同一套组件接口里,页面行为会稳定很多。

一、先把这套缩放能力的接口看清楚
Scroll 在 API 20 之后,和缩放直接相关的接口有四个属性和三个事件。四个属性分别是 maxZoomScale、minZoomScale、zoomScale 和 enableBouncesZoom。其中 maxZoomScale 和 minZoomScale 控制缩放范围,zoomScale 用来设置或同步当前缩放比例,enableBouncesZoom 控制超出边界时是否保留缩放回弹效果。相关 API 已经列进 ArkUI 6.0.0(20) Beta3 的变更清单。
三个事件分别是 onZoomStart、onDidZoom 和 onZoomStop。这组事件的职责很清楚,开始缩放时通知一次,缩放过程中持续回调,结束时再通知一次。对开发者来说,这刚好覆盖了完整的交互生命周期,埋点、状态同步、缩放提示、工具栏联动都能接在这里。
这套能力还有一个前提条件,scrollable 必须设置成 ScrollDirection.FREE。原因也很直接,内容缩放之后,通常要同时支持横向和纵向滚动,自由滚动模式才能承接这种场景。自由滚动模式下拿到的内容总大小,是子组件缩放后的总大小。
如果项目里还要做版本兼容,这里还要多记一件事。华为给出的 API 兼容性判断说明里提到,只有当设备版本满足 API Level≥20 时,Scroll 才会设置这些缩放相关属性。也就是说,这套能力虽然写法统一,但项目如果要兼容更低版本,最好还是做一层 API 保护判断。
二、先把最小可用版本接起来
这类能力上手并不复杂,最小可用版本通常就是一个 Scroll 包一层内容组件,再把缩放范围和回调接进去。图片浏览是最典型的场景,直接看代码会更清楚。
@Entry
@Component
struct ZoomableImageViewer {
@State currentScale: number = 1.0
build() {
Column() {
Scroll() {
Image($r('app.media.sample_landscape'))
.width('100%')
.objectFit(ImageFit.Contain)
}
.width('100%')
.height('100%')
.scrollable(ScrollDirection.FREE)
.minZoomScale(0.5)
.maxZoomScale(3.0)
.enableBouncesZoom(true)
.zoomScale(this.currentScale)
.onZoomStart(() => {
console.info('开始缩放')
})
.onDidZoom((scale) => {
this.currentScale = scale
console.info(`当前缩放比例: ${scale}`)
})
.onZoomStop(() => {
console.info('结束缩放')
})
}
.width('100%')
.height('100%')
}
}
这段代码接完之后,双指捏合缩放就已经能跑起来了。minZoomScale(0.5) 允许内容缩小到原始尺寸的一半,maxZoomScale(3.0) 允许放大到三倍,enableBouncesZoom(true) 则把超界后的回弹保留下来。对图片浏览这类页面来说,这一套默认行为已经够用了。
这里有一个很实用的点,zoomScale 可以和状态变量绑定。这样一来,页面既能在手势缩放时被动接收比例变化,也能主动通过代码修改比例。比如双击放大、点击恢复原始尺寸,这种交互就很好接。写法和普通动画状态切换没什么差别。
@State currentScale: number = 1.0
private zoomIn(): void {
animateTo({ duration: 300 }, () => {
this.currentScale = 2.0
})
}
private resetZoom(): void {
animateTo({ duration: 300 }, () => {
this.currentScale = 1.0
})
}
这类程序化缩放很适合文档预览和图片浏览器。用户双击一下放大,再双击一下恢复,交互会自然很多。
三、性能和滚动协同
手势缩放接起来很快,真正容易出问题的地方通常有两个。一个是 onDidZoom 回调太频繁,另一个是缩放和滚动同时存在时,页面状态收不住。
先说第一个。onDidZoom 在缩放过程中会持续触发,频率很高。这里如果直接塞重逻辑,比如图片解码、远程上报、复杂布局重算,页面很容易掉帧。更稳的处理方式,是让这个回调只做轻量级状态同步,把重逻辑放进防抖或节流处理里。
@State currentScale: number = 1.0
private zoomDebounceTimer: number = -1
private handleScaleChange(scale: number): void {
this.currentScale = scale
if (this.zoomDebounceTimer !== -1) {
clearTimeout(this.zoomDebounceTimer)
}
this.zoomDebounceTimer = setTimeout(() => {
console.info(`稳定后的缩放比例: ${scale}`)
this.zoomDebounceTimer = -1
}, 200)
}
这种处理方式很适合设计稿预览、长文档查看和高分辨率图片浏览。用户在缩放过程中保持流畅,业务逻辑在停下来后再补。
再说第二个。Scroll 这套能力默认已经把缩放和滚动的关系处理到比较顺了,正常情况下不需要开发者自己去管优先级。项目如果确实有特殊场景,比如缩放时暂时不希望滚动手势介入,可以用 enableScrollInteraction(false) 来临时关掉滚动交互。这个属性在 Scroll 通用接口里本来就是现成能力。
@Entry
@Component
struct ZoomStateDemo {
@State isZooming: boolean = false
build() {
Scroll() {
Image($r('app.media.sample_landscape'))
.width('100%')
.objectFit(ImageFit.Contain)
}
.scrollable(ScrollDirection.FREE)
.minZoomScale(0.8)
.maxZoomScale(3.0)
.enableScrollInteraction(!this.isZooming)
.onZoomStart(() => {
this.isZooming = true
})
.onZoomStop(() => {
this.isZooming = false
})
}
}
不过这类做法更适合引导型场景,普通图片浏览和文档预览一般没必要这么做。用户放大完内容,马上开始拖动查看,这是很自然的行为,强行截断滚动反而会破坏体验。
四、实际场景运用
这套能力最适合的场景其实很集中,图片浏览器、设计稿预览、PDF 或长图查看、地图类页面都很适合。关键点不在能不能缩放,在缩放之后内容是不是还好操作。
图片浏览器最简单,内容本身就是单一大图,直接放进 Scroll 里就能跑。设计稿预览则要更注意边界,通常需要更大的最大缩放比例,回弹也经常要关掉,这样方便用户看像素级细节。电子书和文档预览这类页面则更适合把缩放结果映射到字体、行高或整体容器比例上,而不是简单放大一张内容图。
如果是设计稿预览,下面这种写法会更稳一点。缩放范围更大,回弹关闭,预览行为也更可控。
@Entry
@Component
struct MockupPreviewPage {
@State previewScale: number = 1.0
build() {
Scroll() {
Image($r('app.media.design_mockup'))
.width('100%')
.objectFit(ImageFit.Contain)
}
.scrollable(ScrollDirection.FREE)
.minZoomScale(0.25)
.maxZoomScale(4.0)
.zoomScale(this.previewScale)
.enableBouncesZoom(false)
.onDidZoom((scale) => {
this.previewScale = scale
})
}
}
如果是文档阅读器,则更适合把缩放比例收成字体缩放系数,而不是只把整个内容区域拉大。这样处理之后,阅读体验会更稳定,文字也更容易保持清晰。
@Entry
@Component
struct ReaderPage {
@State fontScale: number = 1.0
private chapters: string[] = [
'这是第一段内容。',
'这是第二段内容。',
'这是第三段内容。'
]
build() {
Scroll() {
Column({ space: 12 }) {
ForEach(this.chapters, (chapter: string) => {
Text(chapter)
.fontSize(16 * this.fontScale)
.lineHeight(24 * this.fontScale)
})
}
.width('100%')
.padding(16)
}
.scrollable(ScrollDirection.FREE)
.minZoomScale(0.8)
.maxZoomScale(2.0)
.zoomScale(this.fontScale)
.onDidZoom((scale) => {
this.fontScale = scale
})
}
}
这种写法更适合长文本场景,缩放结果直接体现在字体排版上,用户感知也更自然。
总结
鸿蒙 6 API 20 给 Scroll 补进的这套手势缩放能力,真正有价值的地方很明确。缩放范围有了原生属性,缩放状态有了完整回调,滚动协同也被统一进了组件本身。过去需要手工处理的缩放中心、优先级冲突、回弹边界,这一轮都被收进了组件接口里。
先把 scrollable 设成 ScrollDirection.FREE,再把缩放范围和回弹策略定下来,最后把 onDidZoom 里的逻辑压轻,别让高频回调拖慢界面。这样写下来,图片浏览、设计稿预览、文档查看这类页面会顺很多,代码也会比过去好维护得多。
更多推荐




所有评论(0)