1# Web组件嵌套滚动 2<!--Kit: ArkWeb--> 3<!--Subsystem: Web--> 4<!--Owner: @zourongchun--> 5<!--Designer: @zhufenghao--> 6<!--Tester: @ghiker--> 7<!--Adviser: @HelloCrease--> 8 9Web组件嵌套滚动的典型应用场景为,在页面中,多个独立区域需进行滚动,当用户滚动Web区域内容时,可联动其他滚动区域,实现上下左右全方位滑动页面的嵌套滚动体验。内嵌于可滚动容器([Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md)、[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)、[Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md)、[Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md)、[Tabs](../reference/apis-arkui/arkui-ts/ts-container-tabs.md)、[WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md)、[Refresh](../reference/apis-arkui/arkui-ts/ts-container-refresh.md)、[bindSheet](../reference/apis-arkui/arkui-ts/ts-universal-attributes-sheet-transition.md#bindsheet))中的Web组件,接收到滑动手势事件后,需要设置ArkUI的[NestedScrollMode](../reference/apis-arkui/arkui-ts/ts-appendix-enums.md#nestedscrollmode10)枚举属性,实现Web组件与ArkUI可滚动容器的嵌套滚动。 10 11Web组件嵌套滚动可通过[方案1:使用nestedScroll属性实现嵌套滚动](#使用nestedscroll属性实现嵌套滚动)或[方案2:滚动偏移量由滚动父组件统一派发](#滚动偏移量由滚动父组件统一派发)两个方案实现,方案的选择应取决于应用嵌套滚动的具体业务场景。如果只是简单的Web组件与其他父组件联动滚动建议通过方案1实现;如果应用需要自定义控制Web组件和其他滚动组件滚动,以及一些复杂场景建议使用方案2。 12 13> **说明:** 14> 15> 如果Web组件用到了全量展开的场景(layoutMode为`WebLayoutMode.FIT_CONTENT`),需要显式指明渲染模式(`RenderMode.SYNC_RENDER`),详见[layoutMode](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#layoutmode11)。 16 17## 使用nestedScroll属性实现嵌套滚动 18 19使用Web组件[nestedScroll](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#nestedscroll11)属性来设置上下左右四个方向,或者设置向前、向后两个方向的嵌套滚动模式,实现与父组件的滚动联动,同时也允许在过程中动态改变嵌套滚动的模式。 20 21**完整代码** 22```ts 23// xxx.ets 24import { webview } from '@kit.ArkWeb'; 25 26@Entry 27@ComponentV2 28struct NestedScroll { 29 private scrollerForScroll: Scroller = new Scroller() 30 private listScroller: Scroller = new Scroller() 31 controller: webview.WebviewController = new webview.WebviewController(); 32 @Local arr: Array<number> = [] 33 34 aboutToAppear(): void { 35 for (let i = 0; i < 10; i++) { 36 this.arr.push(i) 37 } 38 } 39 40 build() { 41 Scroll(this.scrollerForScroll) { 42 Column() { 43 Web({ src: $rawfile("index.html"), controller: this.controller }) 44 .nestedScroll({ 45 scrollUp: NestedScrollMode.PARENT_FIRST,//向上滚动父组件优先 46 scrollDown: NestedScrollMode.SELF_FIRST,//向下滚动子组件优先 47 }).height("100%") 48 Repeat<number>(this.arr) 49 .each((item: RepeatItem<number>) => { 50 Text("Scroll Area") 51 .width("100%") 52 .height("40%") 53 .backgroundColor(0X330000FF) 54 .fontSize(16) 55 .textAlign(TextAlign.Center) 56 }) 57 } 58 } 59 } 60} 61``` 62加载的html文件。 63 64```html 65<!-- index.html --> 66<!DOCTYPE html> 67<html> 68<head> 69 <meta name="viewport" id="viewport" content="width=device-width, initial-scale=1.0"> 70 <style> 71 .blue { 72 background-color: lightblue; 73 } 74 .green { 75 background-color: lightgreen; 76 } 77 .blue, .green { 78 font-size:16px; 79 height:200px; 80 text-align: center; /* 水平居中 */ 81 line-height: 200px; /* 垂直居中(值等于容器高度) */ 82 } 83 </style> 84</head> 85<body> 86<div class="blue" >webArea</div> 87<div class="green">webArea</div> 88<div class="blue">webArea</div> 89<div class="green">webArea</div> 90<div class="blue">webArea</div> 91<div class="green">webArea</div> 92<div class="blue">webArea</div> 93</body> 94</html> 95``` 96 97 98## 滚动偏移量由滚动父组件统一派发 99 100**实现思路** 101 1021. 手指向上划动: 103 104 (1) 如果Web页面没有滚动到底部,Scroll组件将滚动偏移量派发给Web,Scroll组件自身不滚动。 105 106 (2) 如果Web页面滚动至底部,而Scroll组件尚未滚动至底部,则仅Scroll组件自身滚动,不向Web组件和List组件传递滚动位移。 107 108 (3) 如果Scroll组件滚动到底部,则滚动偏移量派发给List组件,Scroll组件自身不滚动。 1092. 手指向下划动: 110 111 (1) 如果List组件没有滚动到顶部,则Scroll组件将滚动偏移量派发给List组件,Scroll组件自身不滚动。 112 113 (2) 当List组件滚动至顶部,而Scroll组件未到达顶部时,Scroll组件将自行滚动,滚动偏移量不会派发给List组件和Web组件。 114 115 (3) 如果Scroll组件滚动到顶部,则滚动偏移量派发给Web,Scroll组件自身不滚动。 116 117**关键实现** 118 1191. 如何禁用Web组件滚动手势。 120 121 (1) 首先调用Web组件滚动控制器方法,设置Web禁用触摸([setScrollable](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#setscrollable12))的滚动。 122 ```ts 123 this.webController.setScrollable(false, webview.ScrollType.EVENT); 124 ``` 125 (2) 再使用[onGestureRecognizerJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ongesturerecognizerjudgebegin13)方法,禁止Web组件自带的滑动手势触发。 126 ```ts 127 .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, otherArray<GestureRecognizer>) => { 128 if (current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) { 129 return GestureJudgeResult.REJECT; 130 } 131 return GestureJudgeResult.CONTINUE; 132 }) 133 ``` 1342. 如何禁止[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)组件的手势。 135 ```ts 136 .enableScrollInteraction(false) 137 ``` 1383. 如何检测List组件、Scroll组件是否滚动到边界。 139 140 (1) 滚动到上边界:scroller.currentOffset().yOffset <= 0; 141 142 (2) 滚动到下边界:scroller.isAtEnd() == true; 143 1444. 如何检测Web组件是否滚动到边界。 145 146 (1) 获取Web组件自身高度、内容高度和当前滚动偏移量来判定。 147 148 (2) 判断Web组件是否滚动到顶部:webController.getPageOffset().y == 0; 149 150 (3) 判断Web组件是否滚动到底部:webController.getPageOffset().y + this.webHeight >= webController.getPageHeight(); 151 152 (4) 获取Web组件自身高度:webController.[getPageHeight()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#getpageheight); 153 154 (5) 获取Web组件窗口高度:webController?.[runJavaScriptExt](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#runjavascriptext10)('window. innerHeight'); 155 156 (6) 获取Web组件的滚动偏移量:webController.[getPageOffset()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#getpageoffset20); 1575. 如何让Scroll组件不滚动。 158 159 Scroll组件绑定[onScrollFrameBegin](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#onscrollframebegin9)事件,将剩余滚动偏移量返回0,scroll组件就不滚动,也不会停止惯性滚动动画。 1606. 滚动偏移量如何派发给List。 161 ```ts 162 this.listScroller.scrollBy(0, offset) 163 ``` 1647. 滚动偏移量如何派发给Web。 165 ```ts 166 this.webController.scrollBy(0, offset) 167 ``` 1688. 设置Web组件[bypassVsyncCondition](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#bypassvsynccondition20)为WebBypassVsyncCondition.SCROLLBY_FROM_ZERO_OFFSET,加快Web组件首帧滚动绘制。 169 ```ts 170 .bypassVsyncCondition(WebBypassVsyncCondition.SCROLLBY_FROM_ZERO_OFFSET) 171 ``` 172 173**完整代码** 174```ts 175// xxx.ets 176import { webview } from '@kit.ArkWeb'; 177 178@Entry 179@ComponentV2 180struct Index { 181 private scroller:Scroller = new Scroller() 182 private listScroller:Scroller = new Scroller() 183 private webController: webview.WebviewController = new webview.WebviewController() 184 private isWebAtEnd:boolean = false 185 private webHeight:number = 0 186 @Local arr: Array<number> = [] 187 188 aboutToAppear(): void { 189 for (let i = 0; i < 10; i++) { 190 this.arr.push(i) 191 } 192 } 193 194 getWebHeight() { 195 try { 196 this.webController?.runJavaScriptExt('window.innerHeight', 197 (error, result) => { 198 if (error || !result) { 199 return; 200 } 201 if (result.getType() === webview.JsMessageType.NUMBER) { 202 this.webHeight = result.getNumber() 203 } 204 }) 205 } catch (error) { 206 } 207 } 208 209 checkScrollBottom() { 210 this.isWebAtEnd = false; 211 if (this.webController.getPageOffset().y + this.webHeight >= this.webController.getPageHeight()) { 212 this.isWebAtEnd = true; 213 } 214 } 215 216 build() { 217 Scroll(this.scroller) { 218 Column() { 219 Web({ 220 src: $rawfile("index.html"), 221 controller: this.webController, 222 }).height("100%") 223 .bypassVsyncCondition(WebBypassVsyncCondition.SCROLLBY_FROM_ZERO_OFFSET) 224 .onPageEnd(() => { 225 this.webController.setScrollable(false, webview.ScrollType.EVENT); 226 this.getWebHeight(); 227 }) 228 // 在识别器即将要成功时,根据当前组件状态,设置识别器使能状态 229 .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => { 230 if (current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) { 231 return GestureJudgeResult.REJECT; 232 } 233 return GestureJudgeResult.CONTINUE; 234 }) 235 List({ scroller: this.listScroller }) { 236 Repeat<number>(this.arr) 237 .each((item: RepeatItem<number>) => { 238 ListItem() { 239 Text("Scroll Area") 240 .width("100%") 241 .height("40%") 242 .backgroundColor(0X330000FF) 243 .fontSize(16) 244 .textAlign(TextAlign.Center) 245 } 246 }) 247 }.height("100%") 248 .maintainVisibleContentPosition(true) 249 .enableScrollInteraction(false) 250 } 251 } 252 .onScrollFrameBegin((offset: number, state: ScrollState)=>{ 253 this.checkScrollBottom(); 254 if (offset > 0) { 255 if (!this.isWebAtEnd) { 256 this.webController.scrollBy(0, offset) 257 return {offsetRemain:0} 258 } else if (this.scroller.isAtEnd()) { 259 this.listScroller.scrollBy(0, offset) 260 return {offsetRemain:0} 261 } 262 } else if (offset < 0) { 263 if (this.listScroller.currentOffset().yOffset > 0) { 264 this.listScroller.scrollBy(0, offset) 265 return {offsetRemain:0} 266 } else if (this.scroller.currentOffset().yOffset <= 0) { 267 this.webController.scrollBy(0, offset) 268 return {offsetRemain:0} 269 } 270 } 271 return {offsetRemain:offset} 272 }) 273 } 274} 275``` 276加载的html文件。 277 278```html 279<!-- index.html --> 280<!DOCTYPE html> 281<html> 282<head> 283 <meta name="viewport" id="viewport" content="width=device-width, initial-scale=1.0"> 284 <style> 285 .blue { 286 background-color: lightblue; 287 } 288 .green { 289 background-color: lightgreen; 290 } 291 .blue, .green { 292 font-size:16px; 293 height:200px; 294 text-align: center; /* 水平居中 */ 295 line-height: 200px; /* 垂直居中(值等于容器高度) */ 296 } 297 </style> 298</head> 299<body> 300<div class="blue" >webArea</div> 301<div class="green">webArea</div> 302<div class="blue">webArea</div> 303<div class="green">webArea</div> 304<div class="blue">webArea</div> 305<div class="green">webArea</div> 306<div class="blue">webArea</div> 307</body> 308</html> 309``` 310 311<!--RP1--><!--RP1End-->