Scene description

The business needs to embed a WebView in a scrolling layout, but on Android there is a bug about the webView height: When the Webview height is too high, it will Crash or even restart the phone. So I came up with a layout where the outermost layer is a ScrollView with a scrollable WebView of fixed height inside. There are two questions:

  1. How does a WebView scroll
  2. How does webView scroll interact with external ScrollView

The solution

GestureRecognizers: [Factory(() => Eagergesturecognizer ())].toset (),

However, this approach causes the WebView to win the gesture race, and the external ScrollView cannot get the scroll event at all, resulting in the webView scrolling completely independent of the external ScrollView’s scroll, which is why this layout is rarely seen.

I came up with the idea of using NestedScrollView, but it was clear that I needed to redefine it, because what I wanted was something like this:

The InnerScrollView is completely still when the OutScrollView slides or rolls.

The OutScrollView does not slide at all when the InnerScrollView is rolled, but only when the InnerScrollView slides to the edge. If the InnerScrollView is not attached, the OutScrollView will not Fling. If the InnerScrollView is not attached, the OutScrollView will not Fling.

ScrollActivityDelegate: NestedScrollView: ScrollActivityDelegate: ScrollActivityDelegate

Scrollable->GestureRecorgnizer->Drag(ScrollDragController)->ScrollActivityDelegate

When the user’s finger drags a ScrollView:

ScrollDragController:
@override
void update(DragUpdateDetails details) {
    //other codes
    delegate.applyUserOffset(offset);
}
Copy the code

Called when the drag ends:

@override void end(DragEndDetails details) {///other codes, goBallistic means Fling delegate. GoBallistic (velocity); }Copy the code

So custom ScrollActivityDelegate is the start of Hook scrolling. In NestedScrollView, the class is _NestedScrollCoordinator, so my idea is to define a Delegate myself. Here’s how it works:

You need to determine if the InnerScrollView is scrolling

I force the InnerScrollView to be wrapped with my custom Widget:

class _NestedInnerScrollChildState extends State<NestedInnerScrollChild> { @override Widget build(BuildContext context) { return Listener( child: NotificationListener<ScrollEndNotification>( child: widget.child, onNotification: (end) { widget.coordinator._innerTouchingKey = null; // continue to bubble up return false; }, ), onPointerDown: _startScrollInner, ); } void _startScrollInner(_) { widget.coordinator._innerTouchingKey = widget.scrollKey; }}Copy the code

I used the Listener onPointerDown method to determine that the user touched the inner view, but I did not use onPointerUp or onPointerCancel to determine that the scroll ended because the Fling existed. In the Fling effect, the view may still be sliding while the finger is away from the screen, so using the ScrollEndNotification is a good idea.

The InnerScrollView is completely disabled when the OutScrollView slides

  1. The hook applyUserOffset
  @override
  void applyUserOffset(double delta) {
    if (!innerScroll) {
      _outerPosition.applyFullDragUpdate(delta);
    }
  }
Copy the code
  1. Fling

The Coordinator’s goBallistic method is first called and the beginActivity method is triggered. The beginActivity method can be intercepted directly in the beginActivity:

///_innerPositions are not a collection of innerViews, If (innerScroll) {for (final _NestedScrollPosition position in _innerPositions) {final ScrollActivity newInnerActivity = innerActivityGetter(position); position.beginActivity(newInnerActivity); scrolling = newInnerActivity.isScrolling; }}Copy the code

The InnerScrollView and OutScrollView are nested slides

  1. applyUserOffset

Use NestedScrollView as a reference

@override
  void applyUserOffset(double delta) {
    double remainDelta = innerPositionList.first.applyClampedDragUpdate(delta);
      if(remainDelta ! =0.0) { _outerPosition.applyFullDragUpdate(remainDelta); }}Copy the code
  1. Fling

The innerView triggers the call chain of the Fling gesture: ScrollDragController will call the goBallistic method of ScrollActivityDelegate -> trigger the beginActivity method of ScrollPosition and create the BallisticScrollActivity real Example -> The BallisticScrollActivity instance in combination with Simulation continuously calculates the roll distance.

The BallisticScrollActivity has a method:

 /// Move the position to the given location.
  ///
  /// If the new position was fully applied, returns true. If there was any
  /// overflow, returns false.
  ///
  /// The default implementation calls [ScrollActivityDelegate.setPixels]
  /// and returns true if the overflow was zero.
  @protected
  bool applyMoveTo(double value) {
    return delegate.setPixels(value) == 0.0;
  }
Copy the code

When this method returns false immediately stop rolling, just NestedScrollView have to create custom OutBallisticScrollActivity method, so I’m applyMove there to judge if it is innerView is rolling it returns false

  @override
  bool applyMoveTo(double value) {
    if (coordinator.innerScroll) {
         return false;
    }
// other codes
}
Copy the code

An optimization can be added: the innerView lets go if the Fling is triggered at the edge.

Supports multiple Inner Scroll Views

There can only be one outView, but innerView can theoretically have more than one. I have attached a link to this article for reference [:](“Flutter extends NestedScrollView (2) list scroll sync solution – nuggets (juejin. Cn)”). The core is to bind position to ScrollView when ScrollController attaches detach.

Implement webView scrolling

[:](Avenue to Simplicity: the path to conflict resolution of Flutter nested sliding – V Master on line 1 (Vimerzhao. Top))

All the scroll views in a Flutter are ultimately implemented by Scrollable+Viewport. The Scrollable gets the scroll gesture, distance calculation, etc. The drawing is handed over to the Viewport. Dart: Dart: viewPort. Dart:

@override
  void paint(PaintingContext context, Offset offset) {
    if (firstChild == null)
      return;
    if(hasVisualOverflow && clipBehavior ! = Clip.none) { _clipRectLayer.layer = context.pushClipRect( needsCompositing, offset, Offset.zero & size, _paintContents, clipBehavior: clipBehavior, oldLayer: _clipRectLayer.layer, ); }else {
      _clipRectLayer.layer = null; _paintContents(context, offset); }}void _paintContents(PaintingContext context, Offset offset) {
    for (final RenderSliver child in childrenInPaintOrder) {
      if (child.geometry!.visible)
        context.paintChild(child, offset + paintOffsetOf(child));
    }
  }
Copy the code

The paintOffsetOf(child) can be simplified to the drawing bias caused by scrolling. For example, a viewport with a height of 500 and a content height of 1000 draws content from [0-500] by default, and when the user slides up by 100 draws content from [100,600], where 100 is the paintOffset.

So I ended up creating a custom Viewport, but the paintOffset was always passed 0 when the Flutter side was drawn. I passed the real offset to the WebView and then called window.scrollto (0,offset) to slide the webView content. In short, a traditional ScrollView is one where the content doesn’t move and the canvas moves, and my solution is one where the canvas doesn’t move but the content moves. [](“inner_scroll_webview.dart (github.com)”)

I put together a a pub the code library [] (nested_inner_scroll | Flutter Package (pub. Dev))