Flutter Tab嵌套滑动如丝

时间:2021-7-20 作者:qvyue

前言

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

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

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

Flutter Tab嵌套滑动如丝
image
Flutter Tab嵌套滑动如丝
image

代码时间

获取父和子

  void _updateAncestor() {
    if (_ancestor != null) {
      _ancestor._child = null;
      _ancestor = null;
    }
    if (widget.link) {
      _ancestor = context.findAncestorStateOfType();
      _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 = {
              VerticalDragGestureRecognizer:
                  GestureRecognizerFactoryWithHandlers(
                () => 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 = {
              HorizontalDragGestureRecognizer:
                  GestureRecognizerFactoryWithHandlers(
                () => 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.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: [
      List("Tab000"),
      List("Tab001"),
      List("Tab002"),
      List("Tab003"),
    ],
    controller: tabController2,
    link: true,
  )

滚动方向

  /// 滚动方向
  /// 默认为水平滚动
  final Axis scrollDirection;

  Row(
    children: [
      ExtendedTabBar(
        indicator: const ColorTabIndicator(Colors.blue),
        labelColor: Colors.black,
        scrollDirection: Axis.vertical,
        tabs: const [
          ExtendedTab(
            text: 'Tab0',
            scrollDirection: Axis.vertical,
          ),
          ExtendedTab(
            text: 'Tab1',
            scrollDirection: Axis.vertical,
          ),
        ],
        controller: tabController,
      ),
      Expanded(
        child: ExtendedTabBarView(
          children: [
            const ListWidget(
              'Tab1',
              scrollDirection: Axis.horizontal,
            ),
            const ListWidget(
              'Tab1',
              scrollDirection: Axis.horizontal,
            ),
          ],
          controller: tabController,
          scrollDirection: Axis.vertical,
        ),
      )
    ],
  )

缓存大小

  /// 缓存页面的个数
  /// 默认为0
  /// 如果设置为1,那么意味内存里面有两页
  final int cacheExtent;
  
  ExtendedTabBarView(
    children: [
      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)]

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。