It should be common for TabBarView to have horizontal scrolling components, but it is easy to reproduce a sliding conflict problem when operating at high speeds

Detailed problem

  1. Slide the first TAB, whether you switch to the second TAB or not

  2. If you try to slide the first TAB horizontal scroll ListView or vertical ListView before the TabBarView scrolling animation is finished, you will find that the TabBarVie event is blocked. This is a bad interactive experience. Like a bug

Problem analysis

Since this is a conflict issue, it is worth recalling the event handling of a flutter. You can refer to my previous article on flutter event distribution and conflict handling

After the up event is triggered, the gesture arena has already cleared the members. If the next Down event comes in, it will re-enter the event competition. The internal ListView as the child should have the priority to win. Unless TabBarView blocks the event, so.. Look at the source code

Source code analysis

The flutter code version is 1.22. Start with TabBarView. What is the actual implementation of TabBarView

Override Widget Build (BuildContext context) {return NotificationListener<ScrollNotification>( onNotification: _handleScrollNotification, child: PageView( dragStartBehavior: widget.dragStartBehavior, controller: _pageController, physics: widget.physics == null ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) : const PageScrollPhysics().applyTo(widget.physics), children: _childrenWithKey, ), ); }Copy the code

You’ll notice that the implementation of TabBarView is PageViwe, which jumps directly to the build method of _PageViewState

@override Widget Build (BuildContext context) {return removes unnecessary code NotificationListener<ScrollNotification>( onNotification: (ScrollNotification notification) { child: Scrollable( , ViewportOffset position) { return Viewport( slivers: <Widget>[ ], ); },),); }Copy the code

NotificationListener->Scrollable->Viewport

NotificationListener is a Scrollable notification, Viewport is a display of control content, and gesture blocking should not be in either of these components.

If you look at the build method of ScrollableState, you can see that there is an IgnorePointer that intercepts the child’s response to the event.

No action is required on the child event. The _shouldIgnorePointer variable is ignored. _shouldIgnorePointer, ignoringSemantics: false, child: widget.viewportBuilder(context, position), ),Copy the code

If setIgnorePointer is called, use globalKey to find the RenderObject corresponding to IgnorePointer. That’s RenderIgnorePointer and ignoring. (The principle of IgnorePointer is not explained here)

  @override
  @protected
  void setIgnorePointer(bool value) {
    if (_shouldIgnorePointer == value)
      return;
    _shouldIgnorePointer = value;
    if (_ignorePointerKey.currentContext != null) {
      final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject() as RenderIgnorePointer;
      renderBox.ignoring = _shouldIgnorePointer;
    }
  }

Copy the code

By looking at setIgnorePointer’s method annotations, you can see that this method is the one that caused our problem. Call setIgnorePointer (true) when scrolling starts. Call that setIgnorePointer(false) when the scroll ends, when the animation ends

The actual solution

Look at the effect

SetIgnorePointer (false) was originally intended to be called with the up gesture, but since the gesture is blocked at this point, there is no time to call up

The PageController is passed to the PageView inside the TabBarView and listens for the ScrollStartNotification event. Manually call setIgnorePointer(false)

The pseudocode is as follows

NotificationListener<ScrollNotification>( child: TestTabBarView( children: children, onCreatePageController: () { return pageController; }, ), onNotification: (nofi) { if (nofi is ScrollStartNotification) { pageController? .position? .context? .setIgnorePointer(false); } return false; },),Copy the code

Then rewrite the TabBarView of the system because there is no entry to call setIgnorePointer from the same pageController position.

The TabBarView has a pageController for PageView by default, so we can’t get this variable outside of the TabBarView

// TestTabBarView const TestTabBarView({ Key key, @required this.children, this.controller, this.physics, this.dragStartBehavior = DragStartBehavior.start, this.onCreatePageController, }) final PageController Function() onCreatePageController; / / in _TestTabBarViewState @ override void didChangeDependencies () {super. DidChangeDependencies (); _pageController = widget.onCreatePageController() ?? PageController(initialPage: _currentIndex ?? 0); }Copy the code

The Demo code

The last

First of all, this should be a normal interaction, but it is easy to reproduce when the user is doing something fast.

Second, the current solution is not elegant at all, if there is a better way to update later