1. 典型场景
新闻类页面:内容页为Web组件实现,评论区使用ArkUI原生List实现。
主题等应用主页:上半区为推荐页,下半区为分类推荐页(分类栏滑动到顶部后不会消失)。
2. 实现思路(以新闻类为例)
使用Scroll嵌套Web和List组件实现。Scroll作为父组件响应滚动手势,Web和List组件禁用滚动手势,滚动偏移量由父组件Scroll给Web和List组件派发。
2.1 派发逻辑:
手势向上滑动
1)如果web没有滚动到底部,则Scroll将滚动偏移量派发给web,Scroll组件本身不滚动
2)如果web滚动到底部,Scroll没有滚动到底部,则Scroll自身滚动,不给Web和List派发滚动偏移量
3)如果Scroll滚动到底部,则滚动偏移量派发给List,Scroll组件本身不滚动
手势向下滑动
1)如果List没有滚动到顶部,则Scroll将滚动偏移量派发给List,Scroll组件本身不滚动
2)如果List滚动到顶部,Scroll没有滚动到顶部,则Scroll自身滚动,不给Web和List派发滚动偏移量
3)如果Scroll滚动到顶部,则滚动偏移量派发给Web,Scroll组件本身不滚动
3. 关键实现
3.1 禁用Web滚动手势
.onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => {
if (current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) {
return GestureJudgeResult.REJECT
}
return GestureJudgeResult.CONTINUE
})
3.2 禁用List组件滚动手势
enableScrollInteraction(false)
3.3 List和Scroll组件滚动边界判断
滚动到上边界:
scroller.currentOffset().yOffset <= 0
滚动到下边界:
scroller.isAtEnd() == true
3.4 Web组件滚动边界判断
获取Web组件自身高度:
webController.getPageHeight()
获取Web组件内容高度:
webController?.runJavaScriptExt('window.innerHeight')
获取Web组件的滚动偏移量:
webController?.runJavaScriptExt('document.documentElement.scrollTop || document.body.scrollTop')
3.5 禁用Scroll组件滚动
Scroll组件绑定onScrollFrameBegin事件,将剩余偏移量设置为0
3.6 滚动偏移量派发
通过对应组件滚动控制器的scrollBy方法设置
4. 实例代码
import webview from '@ohos.web.webview'
@Entry
@ComponentV2
struct MyComponent {
scroller:Scroller = new Scroller()
listScroller:Scroller = new Scroller()
webController:webview.WebviewController = new webview.WebviewController()
isWebEnd:boolean = false
webHeight:number = 0
scrollTop:number = 0
@Local arr:Array<number> = []
aboutToAppear() {
for (let i = 0; i <= 100; i++) {
this.arr.push(i)
}
}
getWebHeight() {
try{
this.webController?.runJavaScriptExt('window.innerHeight',
(error, result) => {
if (error || !result) {
return
}
if (result.getType() === webview.JsMessageType.NUMBER) {
this.webHeight = result.getNumber()
}
})
} catch (error) {
}
}
getWebScrollTop() {
try{
this.webController?.runJavaScriptExt('document.documentElement.scrollTop || document.body.scrollTop',
(error, result) => {
if (error || !result) {
return
}
if (result.getType() === webview.JsMessageType.NUMBER) {
this.scrollTop = result.getNumber()
this.isWebEnd = false
if (this.scrollTop + this.webHeight >= this.webController.getPageHeight()) {
this.isWebEnd = true
}
}
})
} catch (error) {
}
}
build() {
Scroll(this.scroller) {
Column() {
Web({
src: "https://gitee.com",
controller: this.webController
}).height('100%')
.onPageEnd(() => {
this.webController.setScrollable(false, webview.ScrollType.EVENT)
this.getWebHeight()
})
.onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => {
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("item" + item.item.toString())
}
})
}.height('100%')
.maintainVisibleContentPosition(true)
.enableScrollInteraction(false)
}
}
.onScrollFrameBegin((offset: number, state: ScrollState) => {
this.getWebScrollTop()
if (offset > 0) {
if (!this.isWebEnd) {
this.webController.scrollBy(0, offset)
return {offsetRemain: 0}
} else if (this.scroller.isAtEnd()) {
this.listScroller.scrollBy(0, offset)
return {offsetRemain: 0}
}
} else if (offset < 0) {
if (this.listScroller.currentOffset().yOffset > 0) {
this.listScroller.scrollBy(0, offset)
return {offsetRemain: 0}
} else if (this.scroller.currentOffset().yOffset <= 0) {
this.webController.scrollBy(0, offset)
return {offsetRemain: 0}
}
}
return {offsetRemain: offset}
})
}
}