Flutter Tab嵌套滑动如丝

前言

为了解决 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)]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容