鸿蒙 Web组件嵌套滚动组件
本文介绍了两种实现嵌套滚动的方法:方案1通过nestedScroll属性实现简单联动,支持6种预设滚动模式;方案2通过手动派发偏移量实现精确控制,适用于复杂场景。两种方案各具特点:方案1代码简单但控制粒度较粗,方案2实现复杂但灵活性高。文章详细说明了技术实现步骤,包括禁用组件滚动、边界检测和偏移量派发等关键操作,并提供了常见问题解决方案,如滚动卡顿、边界检测不准和手势冲突等。开发者可根据实际需求选
本文同步发表于 微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
嵌套滚动指在页面中多个独立滚动区域协同工作的交互模式。当用户在Web组件区域滚动时,能够联动其他ArkUI滚动区域,实现上下左右全方位的流畅滑动。
支持的滚动容器
Web组件可内嵌于以下滚动容器:
-
Grid(网格布局)
-
List(列表)
-
Scroll(滚动容器)
-
Swiper(轮播)
-
Tabs(选项卡)
-
WaterFlow(瀑布流)
-
Refresh(下拉刷新)
-
bindSheet(底部工作表)
前提
当Web组件使用全量展开模式(layoutMode = WebLayoutMode.FIT_CONTENT)时,必须显式设置渲染模式:
Web({
src: '...',
controller: this.controller
})
.layoutMode(WebLayoutMode.FIT_CONTENT)
.renderMode(RenderMode.SYNC_RENDER) // 必须设置!
二、两种实现方式
| 特性 | 1:nestedScroll属性 | 2:滚动偏移量统一派发 |
|---|---|---|
| 实现复杂度 | 简单 | 复杂 |
| 控制粒度 | 粗粒度(方向优先) | 细粒度(像素级控制) |
| 适用场景 | 简单联动需求 | 复杂自定义控制 |
| 推荐度 | 优先考虑 | 特殊需求使用 |
建议:
-
简单联动:使用方案1(
nestedScroll属性) -
复杂控制:使用方案2(手动派发偏移量)
三、方案1:使用nestedScroll属性实现嵌套滚动
3.1 原理
通过设置Web组件的nestedScroll属性,定义滚动优先级,系统自动处理滚动事件分发。
3.2 属性:NestedScrollMode
| 枚举值 | 说明 |
|---|---|
SELF_FIRST |
当前组件优先滚动 |
PARENT_FIRST |
父组件优先滚动 |
SELF_ONLY |
仅当前组件滚动 |
PARENT_ONLY |
仅父组件滚动 |
PARALLEL |
父子组件同时滚动 |
NEVER |
不参与嵌套滚动 |
3.3 代码示例
import { webview } from '@kit.ArkWeb';
@Entry
@ComponentV2
struct NestedScroll {
private scrollerForScroll: Scroller = new Scroller();
private listScroller: Scroller = new Scroller();
controller: webview.WebviewController = new webview.WebviewController();
@Local arr: Array<number> = [];
aboutToAppear(): void {
for (let i = 0; i < 10; i++) {
this.arr.push(i);
}
}
build() {
Scroll(this.scrollerForScroll) {
Column() {
// Web组件:设置嵌套滚动策略
Web({
src: $rawfile('scroll.html'),
controller: this.controller
})
.nestedScroll({
scrollUp: NestedScrollMode.PARENT_FIRST, // 向上:父组件优先
scrollDown: NestedScrollMode.SELF_FIRST, // 向下:子组件优先
// 还可设置:scrollLeft, scrollRight
})
.height('100%')
// 其他滚动区域
Repeat<number>(this.arr)
.each((item: RepeatItem<number>) => {
Text('Scroll Area')
.width('100%')
.height('40%')
.backgroundColor(0X330000FF)
.fontSize(16)
.textAlign(TextAlign.Center)
})
}
}
}
}
3.4 运行效果
-
向上滚动:父Scroll组件先滚动,Web组件后滚动
-
向下滚动:Web组件先滚动,父Scroll组件后滚动
四、方案2:滚动偏移量由父组件统一派发
4.1 原理
完全手动控制滚动行为:
-
禁用Web和List的默认滚动
-
在Scroll组件的
onScrollFrameBegin中判断滚动状态 -
手动将偏移量派发给目标组件
4.2 滚动逻辑
向上滑动(offset > 0):
-
Web未到底部 → 偏移量派发给Web,Scroll自身不滚
-
Web已到底部,Scroll未到底部 → 仅Scroll自身滚动
-
Scroll已到底部 → 偏移量派发给List,Scroll不滚
向下滑动(offset < 0):
-
List未到顶部 → 偏移量派发给List,Scroll自身不滚
-
List已到顶部,Scroll未到顶部 → 仅Scroll自身滚动
-
Scroll已到顶部 → 偏移量派发给Web,Scroll不滚
4.3 代码实现
4.3.1 禁用Web组件滚动
// 1. 设置Web组件禁用触摸滚动
this.webController.setScrollable(false, webview.ScrollType.EVENT);
// 2. 拦截Web组件的滑动手势
.onGestureRecognizerJudgeBegin((
event: BaseGestureEvent,
current: GestureRecognizer,
otherArray<GestureRecognizer>
) => {
if (current.isBuiltIn() &&
current.getType() == GestureControl.GestureType.PAN_GESTURE) {
return GestureJudgeResult.REJECT; // 拒绝Web自带手势
}
return GestureJudgeResult.CONTINUE;
})
4.3.2 禁用List组件手势
List({ scroller: this.listScroller }) {
// ... 列表内容
}
.enableScrollInteraction(false) // 禁用List的手势交互
4.3.3 边界检测方法
List/Scroll组件边界检测:
// 滚动到上边界
scroller.currentOffset().yOffset <= 0;
// 滚动到下边界
scroller.isAtEnd() == true;
Web组件边界检测:
// 获取Web组件信息
private webHeight: number = 0;
// 获取Web窗口高度
this.webController?.runJavaScriptExt('window.innerHeight',
(error, result) => {
if (result.getType() === webview.JsMessageType.NUMBER) {
this.webHeight = result.getNumber();
}
});
// 判断是否滚动到顶部
webController.getPageOffset().y == 0;
// 判断是否滚动到底部
webController.getPageOffset().y + this.webHeight >= webController.getPageHeight();
4.3.4 滚动控制与偏移量派发
阻止Scroll组件滚动:
.onScrollFrameBegin((offset: number, state: ScrollState) => {
// 通过返回 offsetRemain: 0 阻止Scroll自身滚动
return { offsetRemain: 0 };
})
向List派发偏移量:
this.listScroller.scrollBy(0, offset);
向Web派发偏移量:
this.webController.scrollBy(0, offset);
4.4 完整实现
import { webview } from '@kit.ArkWeb';
@Entry
@ComponentV2
struct Index {
private scroller: Scroller = new Scroller()
private listScroller: Scroller = new Scroller()
private webController: webview.WebviewController = new webview.WebviewController()
private isWebAtEnd: boolean = false
private webHeight: number = 0
@Local arr: Array<number> = []
aboutToAppear(): void {
for (let i = 0; i < 10; i++) {
this.arr.push(i)
}
}
// 获取Web窗口高度
getWebHeight() {
try {
this.webController?.runJavaScriptExt('window.innerHeight',
(error, result) => {
if (result?.getType() === webview.JsMessageType.NUMBER) {
this.webHeight = result.getNumber()
}
})
} catch (error) {
console.error('获取Web高度失败:', error)
}
}
// 检查Web是否滚动到底部
checkScrollBottom() {
this.isWebAtEnd = false;
if (this.webController.getPageOffset().y + this.webHeight >=
this.webController.getPageHeight()) {
this.isWebAtEnd = true;
}
}
build() {
Scroll(this.scroller) {
Column() {
Web({
src: $rawfile('scroll.html'),
controller: this.webController,
})
.height('100%')
.bypassVsyncCondition(WebBypassVsyncCondition.SCROLLBY_FROM_ZERO_OFFSET)
.onPageEnd(() => {
// 页面加载完成后禁用Web滚动
this.webController.setScrollable(false, webview.ScrollType.EVENT);
this.getWebHeight();
})
.onGestureRecognizerJudgeBegin((event, current, others) => {
// 拦截Web的滑动手势
if (current.isBuiltIn() &&
current.getType() == GestureControl.GestureType.PAN_GESTURE) {
return GestureJudgeResult.REJECT;
}
return GestureJudgeResult.CONTINUE;
})
List({ scroller: this.listScroller }) {
Repeat<number>(this.arr)
.each((item: RepeatItem<number>) => {
ListItem() {
Text('Scroll Area')
.width('100%')
.height('40%')
.backgroundColor(0X330000FF)
.fontSize(16)
.textAlign(TextAlign.Center)
}
})
}
.height('100%')
.maintainVisibleContentPosition(true)
.enableScrollInteraction(false) // 禁用List手势
}
}
.onScrollFrameBegin((offset: number, state: ScrollState) => {
this.checkScrollBottom();
// 向上滚动逻辑
if (offset > 0) {
if (!this.isWebAtEnd) {
// Web未到底部:滚动Web
this.webController.scrollBy(0, offset)
return { offsetRemain: 0 }
} else if (this.scroller.isAtEnd()) {
// Scroll已到底部:滚动List
this.listScroller.scrollBy(0, offset)
return { offsetRemain: 0 }
}
}
// 向下滚动逻辑
else if (offset < 0) {
if (this.listScroller.currentOffset().yOffset > 0) {
// List未到顶部:滚动List
this.listScroller.scrollBy(0, offset)
return { offsetRemain: 0 }
} else if (this.scroller.currentOffset().yOffset <= 0) {
// Scroll已到顶部:滚动Web
this.webController.scrollBy(0, offset)
return { offsetRemain: 0 }
}
}
// 其他情况:Scroll自身滚动
return { offsetRemain: offset }
})
}
}
五、方案对比
| 特性 | 方案1 (nestedScroll) | 方案2 (手动派发) |
|---|---|---|
| 代码复杂度 | 低(几行代码) | 高(需完整控制逻辑) |
| 控制精度 | 方向级控制 | 像素级精确控制 |
| 性能 | 系统优化,性能好 | 需手动优化,易有性能问题 |
| 灵活性 | 有限(预设模式) | 极高(完全自定义) |
| 维护成本 | 低 | 高 |
| 适用场景 | 80%常规需求 | 20%特殊复杂需求 |
常见问题
6.1 Web组件滚动卡顿
-
解决:确保设置了
bypassVsyncCondition优化
6.2 边界检测不准确
-
问题:Web是否到底部判断错误
-
解决:确保
getWebHeight()在页面加载完成后调用
6.3 手势冲突
-
问题:Web和ArkUI组件同时响应手势
-
解决:彻底禁用Web手势(
setScrollable+onGestureRecognizerJudgeBegin)
6.4 内存泄漏
-
问题:事件监听未清理
-
解决:在
aboutToDisappear中清理所有监听
更多推荐
所有评论(0)