美文网首页Flutter
Flutter Tab嵌套滑动如丝

Flutter Tab嵌套滑动如丝

作者: 法的空间 | 来源:发表于2020-12-14 23:07 被阅读0次

    前言

    为了解决 TabBarView 嵌套滚动,一年前写了 extended_tabs,当时主要利用的是 OverscrollNotification 通知,将滚动传递给父 TabBarView。但是这种机制会导致,滚动事件未结束之前,子 TabBarView是没法获取到滚动事件,所以感觉会卡一下。

    后来做图片缩放手势的时候,发现 PageView 会跟手势冲突,所以我去掉 PageView 的手势,使用 RawGestureDetector 监听来控制图片缩放以及PageView 的手势,详细内容大家可以看以往一篇介绍, https://juejin.cn/post/6844903814324027400#heading-6

    其实要解决 TabBarView 嵌套滚动的问题,我们也可以把手势由自己掌控。网页例子

    image image

    代码时间

    获取父和子

      void _updateAncestor() {
        if (_ancestor != null) {
          _ancestor._child = null;
          _ancestor = null;
        }
        if (widget.link) {
          _ancestor = context.findAncestorStateOfType<_ExtendedTabBarViewState>();
          _ancestor?._child = this;
        }
      }
    

    PageView 失去滚动

    首先我们需要让 PageView 失去作用,很简单,我们给它加一个 NeverScrollableScrollPhysics。 下面代码中_defaultScrollPhysics 是一个 NeverScrollableScrollPhysics

      void _updatePhysics() {
        _physics = _defaultScrollPhysics.applyTo(widget.physics == null
            ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
            : const PageScrollPhysics().applyTo(widget.physics));
    
        if (widget.physics == null) {
          _canMove = true;
        } else {
          _canMove = widget.physics.shouldAcceptUserOffset(_testPageMetrics);
        }
      }
    

    增加手势监听

    下面这块代码其实就是 ScrollableState 里面的源码, 地址 https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/scrollable.dart#L499

      void _initGestureRecognizers([ExtendedTabBarView oldWidget]) {
        if (oldWidget == null ||
            oldWidget.scrollDirection != widget.scrollDirection ||
            oldWidget.physics != widget.physics) {
          if (_canMove) {
            switch (widget.scrollDirection) {
              case Axis.vertical:
                _gestureRecognizers = <Type, GestureRecognizerFactory>{
                  VerticalDragGestureRecognizer:
                      GestureRecognizerFactoryWithHandlers<
                          VerticalDragGestureRecognizer>(
                    () => VerticalDragGestureRecognizer(),
                    (VerticalDragGestureRecognizer instance) {
                      instance
                        ..onDown = _handleDragDown
                        ..onStart = _handleDragStart
                        ..onUpdate = _handleDragUpdate
                        ..onEnd = _handleDragEnd
                        ..onCancel = _handleDragCancel
                        ..minFlingDistance = widget.physics?.minFlingDistance
                        ..minFlingVelocity = widget.physics?.minFlingVelocity
                        ..maxFlingVelocity = widget.physics?.maxFlingVelocity;
                    },
                  ),
                };
                break;
              case Axis.horizontal:
                _gestureRecognizers = <Type, GestureRecognizerFactory>{
                  HorizontalDragGestureRecognizer:
                      GestureRecognizerFactoryWithHandlers<
                          HorizontalDragGestureRecognizer>(
                    () => HorizontalDragGestureRecognizer(),
                    (HorizontalDragGestureRecognizer instance) {
                      instance
                        ..onDown = _handleDragDown
                        ..onStart = _handleDragStart
                        ..onUpdate = _handleDragUpdate
                        ..onEnd = _handleDragEnd
                        ..onCancel = _handleDragCancel
                        ..minFlingDistance = widget.physics?.minFlingDistance
                        ..minFlingVelocity = widget.physics?.minFlingVelocity
                        ..maxFlingVelocity = widget.physics?.maxFlingVelocity;
                    },
                  ),
                };
                break;
            }
          }
        }
      }
    

    build 方法中将返回的 WidgetRawGestureDetector 包裹起来

        if (_canMove) {
          result = RawGestureDetector(
            gestures: _gestureRecognizers,
            behavior: HitTestBehavior.opaque,
            child: result,
          );
        }
        return result;
    

    处理手势

    • 手势事件用 _hold_dragposition 紧密联系了起来,代码比较简单,在 down, start, update, end, cancel 事件中做出相应的处理,这样就可以将手势传递给 position
      Drag _drag;
      ScrollHoldController _hold;
    
      void _handleDragDown(DragDownDetails details) {
        assert(_drag == null);
        assert(_hold == null);
        _hold = position.hold(_disposeHold);
      }
    
      void _handleDragStart(DragStartDetails details) {
        // It's possible for _hold to become null between _handleDragDown and
        // _handleDragStart, for example if some user code calls jumpTo or otherwise
        // triggers a new activity to begin.
        assert(_drag == null);
        _drag = position.drag(details, _disposeDrag);
        assert(_drag != null);
        assert(_hold == null);
      }
    
      void _handleDragUpdate(DragUpdateDetails details) {
        // _drag might be null if the drag activity ended and called _disposeDrag.
        assert(_hold == null || _drag == null);
        _drag?.update(details);
      }
    
      void _handleDragEnd(DragEndDetails details) {
        // _drag might be null if the drag activity ended and called _disposeDrag.
        assert(_hold == null || _drag == null);
        _drag?.end(details);
        assert(_drag == null);
      }
    
      void _handleDragCancel() {
        // _hold might be null if the drag started.
        // _drag might be null if the drag activity ended and called _disposeDrag.
        assert(_hold == null || _drag == null);
        _hold?.cancel();
        _drag?.cancel();
        assert(_hold == null);
        assert(_drag == null);
      }
    
      void _disposeHold() {
        _hold = null;
      }
    
      void _disposeDrag() {
        _drag = null;
      }
    
    • extended_tabs 的实现中,我们主要需要考虑,当 update (hold 和 start 没法区分手势的方向) 的时候发现没法拖动了(到达最小值和最大值), 父 和 子 TabBarView 的状态。

    • 在 _handleAncestorOrChild 方法中,我们分别取判断 父和子 是否满足能够滚动的条件。

    1.delta 左右
    2.extentAfter == 0 达到最大
    3.extentBefore == 0 达到最小

      void _handleDragUpdate(DragUpdateDetails details) {
        // _drag might be null if the drag activity ended and called _disposeDrag.
        assert(_hold == null || _drag == null);
        _handleAncestorOrChild(details, _ancestor);
    
        _handleAncestorOrChild(details, _child);
    
        _drag?.update(details);
      }
    
      bool _handleAncestorOrChild(
          DragUpdateDetails details, _ExtendedTabBarViewState state) {
        if (state?._position != null) {
          final double delta = widget.scrollDirection == Axis.horizontal
              ? details.delta.dx
              : details.delta.dy;
          
          if ((delta < 0 &&
                  _position.extentAfter == 0 &&
                  state._position.extentAfter != 0) ||
              (delta > 0 &&
                  _position.extentBefore == 0 &&
                  state._position.extentBefore != 0)) {
            if (state._drag == null && state._hold == null) {
              state._handleDragDown(null);
            }
    
            if (state._drag == null) {
              state._handleDragStart(DragStartDetails(
                globalPosition: details.globalPosition,
                localPosition: details.localPosition,
                sourceTimeStamp: details.sourceTimeStamp,
              ));
            }
            state._handleDragUpdate(details);
            return true;
          }
        }
    
        return false;
      }
    
    • 最后在 end, canel 事件中也对 父和子 做出来对应操作即可。
      void _handleDragEnd(DragEndDetails details) {
        // _drag might be null if the drag activity ended and called _disposeDrag.
        assert(_hold == null || _drag == null);
    
        _ancestor?._drag?.end(details);
        _child?._drag?.end(details);
        _drag?.end(details);
    
        assert(_drag == null);
      }
    
      void _handleDragCancel() {
        // _hold might be null if the drag started.
        // _drag might be null if the drag activity ended and called _disposeDrag.
        assert(_hold == null || _drag == null);
        _ancestor?._hold?.cancel();
        _ancestor?._drag?.cancel();
        _child?._hold?.cancel();
        _child?._drag?.cancel();
        _hold?.cancel();
        _drag?.cancel();
        assert(_hold == null);
        assert(_drag == null);
      }
    
    

    使用

    dependencies:
      flutter:
        sdk: flutter
      extended_tabs: any
    

    色块指示器

        TabBar(
          indicator: ColorTabIndicator(Colors.blue),
          labelColor: Colors.black,
          tabs: [
            Tab(text: "Tab0"),
            Tab(text: "Tab1"),
          ],
          controller: tabController,
        )
    

    嵌套滚动

      /// 如果开启,当当前TabBarView不能滚动的时候,会去查看父和子TabBarView是否能滚动,
      /// 如果能滚动就会直接滚动父和子
      /// 默认开启
      final bool link;
      
      ExtendedTabBarView(
        children: <Widget>[
          List("Tab000"),
          List("Tab001"),
          List("Tab002"),
          List("Tab003"),
        ],
        controller: tabController2,
        link: true,
      )
    

    滚动方向

      /// 滚动方向
      /// 默认为水平滚动
      final Axis scrollDirection;
    
      Row(
        children: <Widget>[
          ExtendedTabBar(
            indicator: const ColorTabIndicator(Colors.blue),
            labelColor: Colors.black,
            scrollDirection: Axis.vertical,
            tabs: const <ExtendedTab>[
              ExtendedTab(
                text: 'Tab0',
                scrollDirection: Axis.vertical,
              ),
              ExtendedTab(
                text: 'Tab1',
                scrollDirection: Axis.vertical,
              ),
            ],
            controller: tabController,
          ),
          Expanded(
            child: ExtendedTabBarView(
              children: <Widget>[
                const ListWidget(
                  'Tab1',
                  scrollDirection: Axis.horizontal,
                ),
                const ListWidget(
                  'Tab1',
                  scrollDirection: Axis.horizontal,
                ),
              ],
              controller: tabController,
              scrollDirection: Axis.vertical,
            ),
          )
        ],
      )
    

    缓存大小

      /// 缓存页面的个数
      /// 默认为0
      /// 如果设置为1,那么意味内存里面有两页
      final int cacheExtent;
      
      ExtendedTabBarView(
        children: <Widget>[
          List("Tab000"),
          List("Tab001"),
          List("Tab002"),
          List("Tab003"),
        ],
        controller: tabController2,
        cacheExtent: 1,
      )  
    

    结语

    2020年只剩下2周,这是一个不普通的一年,很庆幸的是周围的人都安安全全的。亲眼见证了这么多,有时候感觉能健康快乐地写代码就很不错了。很多时候,只要肯用心,不管再难的问题,不管是工作学习还是生活上的,相信我们都会克服的。

    爱Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果[图片上传失败...(image-1755ff-1607958437913)]QQ群:181398081

    最最后放上Flutter Candies全家桶,真香。

    [图片上传失败...(image-773835-1607958437913)]

    相关文章

      网友评论

        本文标题:Flutter Tab嵌套滑动如丝

        本文链接:https://www.haomeiwen.com/subject/hssbgktx.html