likes
comments
collection
share

深入进阶-如何解决Flutter上的滑动冲突?

作者站长头像
站长
· 阅读数 28

深入进阶-如何解决Flutter上的滑动冲突

学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概二十篇左右文章分析,欢迎关注,共同进步。 深入进阶-如何解决Flutter上的滑动冲突?

导语

一次需求中遇到了这样的场景,PageView中有三个页面,其中一个页面是TabBarView结构。结果出现了当滑动到TabBar的时候,外层PageView无法滑动(滑动冲突)。最终在stackoverflow上找到了这个问题的解法,过程中顺便将Flutter的手势与滑动机制总结了一番。这也是Flutter进阶必须掌握的一个知识点,相信我,这一定是全网最详细,易懂的总结!!

这个系列会分为四篇:

4、解决Flutter滑动冲突的两种思路

读完本文你将收获:两个具体的场景学习如何解决Flutter的滑动冲突


引言

通过前三期的介绍我们已经对Flutter的滑动过程有了清晰的认识,但理论最终要落地到实践才有意义,下面从两个很常见的业务需求中遇到的问题和大家一起看看如何解决滑动冲突。

Case 1:CustomScrollView嵌套ListView

深入进阶-如何解决Flutter上的滑动冲突?

如图,页面的第二个部分是一个滑动到顶部之后固定的TabBar,首先大家会想到CustomScrollView吧(其实NestedScrollView已经解决了嵌套冲突,不过有的时候会失效,最XX的是我当时不知道有这玩意),将TabBar配置到SliverPersistentHeaderdelegate属性中。

深入进阶-如何解决Flutter上的滑动冲突?

Case 2: 网易云结构 PageView嵌套TabBarView 网易云结构

深入进阶-如何解决Flutter上的滑动冲突?

case2类似网易云,整个页面是一个PageView,有三个Child,第二个Child是一个TabBarView。当滑动到第二个Child的时候手势被TabBarView获取处理。这时如果滑动到Tab的最后一页时,会发现整个页面无法继续滑动到第三个Page页面。原因也很简单,其实TabBarView就是对PageVIew进行了封装使其能和Tab交互,所以当滑动到最后一个页面后,TabBarView认为已经滑动到最后一页了,而所有的手势事件都已经被TabBarView处理,外面的PageView无法收到手势事件,所以外面的Page无法滑动。


Case 1:自定义ScrollPosition

参考文章:zhuanlan.zhihu.com/p/106197796…

ScrollController上官方为我们提供了两种自定义滑动行为的建议:

To further customize scrolling behavior with a Scrollable:

  1. You can provide a viewportBuilder to customize the child model. For example, SingleChildScrollView uses a viewport that displays a single box child whereas CustomScrollView uses a Viewport or a ShrinkWrappingViewport, both of which display a list of slivers.
  2. You can provide a custom ScrollController that creates a custom ScrollPosition subclass. For example, PageView uses a PageController, which creates a page-oriented scroll position subclass that keeps the same page visible when the Scrollable resizes.

这里选择了第二种方式,自定义ScrollPosition

可以提供一个自定义的[ScrollController]来创建一个自定义的 [ScrollPosition]子类。例如,[PageView]使用了 [PageController],它创建了一个面向页面的滚动位置子类,该子类在[Scrollable]调整大小时保持同一页面可见。

具体做法:

深入进阶-如何解决Flutter上的滑动冲突?

整体解法如图,自定义三个类ConflictScrollController(滑动控制器),ConflictScrollPosition(处理滑动的实际对象),ConflictScrollCoordinator(滑动协调器)。关系:ConflictScrollController生成ConflictScrollPosition

ConflictScrollPosition->将滑动事件交给ConflictScrollCoordinator进行协调。

指一些核心的处理逻辑

ConflictScrollPosition#applyUserOffset

/// 当手指滑动时,该方法会获取到滑动距离
/// [delta]滑动距离,正增量表示下滑,负增量向上滑
/// 我们需要把子部件的 滑动数据 交给协调器处理,主部件无干扰
@override
void applyUserOffset(double delta) {
  ScrollDirection userScrollDirection =
      delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse;
  if (debugLabel != coordinator.pageLabel)
    //如果是嵌套内部滑动控件,则通过协调器处理
    return coordinator.applyUserOffset(delta, userScrollDirection, this);
  //否则使用自身的行为处理
  updateUserScrollDirection(userScrollDirection);
  setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}

ConflictScrollCoordinator#applyUserOffset

/// 子部件滑动数据协调
/// [userScrollDirection]用户滑动方向
/// [position]被滑动的子部件的位置信息
void applyUserOffset(double delta,
    [ScrollDirection userScrollDirection, ConflictScrollPosition position]) {
  if (userScrollDirection == ScrollDirection.reverse) {
    //如果是上滑先交给外部控制器消费,有剩余再自己消费
    updateUserScrollDirection(_pageScrollPosition, userScrollDirection);
    final innerDelta = _pageScrollPosition.applyClampedDragUpdate(delta);
    if (innerDelta != 0.0) {
      updateUserScrollDirection(position, userScrollDirection);
      position.applyFullDragUpdate(innerDelta);
    }
  } else {
    updateUserScrollDirection(position, userScrollDirection);
    //否则先自己处理,有剩余在交给外部
    final outerDelta = position.applyClampedDragUpdate(delta);
    if (outerDelta != 0.0) {
      updateUserScrollDirection(_pageScrollPosition, userScrollDirection);
      _pageScrollPosition.applyFullDragUpdate(outerDelta);
    }
  }
}

协调器中先判断滑动的方向如果是上滑先交给外部控制器消费,有剩余再自己消费,否则先自己处理,有剩余在交给外部。

当然不光要处理applyUserOffset,还有goBallistic,完整代码参考:github.com/canoninmajo…


Case 2:监听ScrollNotification

case2的解决办法来自stackOverFlow:[How to “merge” scrolls on a TabBarView inside a PageView?]

解决的思路是:当滑动到TabBarView的最后一页(无论是左还是右的时候)系统会发出OverscrollNotification的通知。那么我们在外部监听,当收到这个通知的时候将滑动事件交给外部处理即可。附可运行链接dart pad

NotificationListener(
            onNotification: (notification) {
              if (notification is ScrollStartNotification) {
              	//滑动起始的通知先存起来
                dragStartDetails = notification.dragDetails;
              }
              if (notification is OverscrollNotification) {
              	//当发生OverScroll的时候,生成外部滑动的drag对象
                drag = _pageController.position.drag(dragStartDetails, () {});
                //使用外部滑动的drag对象进行滑动
                drag.update(notification.dragDetails);
              }
              if (notification is ScrollEndNotification) {
              	//滑动结束后取消
                drag?.cancel();
              }
              return true;
            },
            child: TabBarView(
              controller: _tabController,
              children: <Widget>[
                Container(color: Colors.green[800]),
                Container(color: Colors.green),
                Container(color: Colors.green[200]),
              ],
            ),
          )

不过这个按照下面的做法可以使滑动过程更加的流畅

if (notification is UserScrollNotification &&
        notification.direction == ScrollDirection.forward &&
        !_tabController.indexIsChanging &&
        dragStartDetails != null &&
        _tabController.index == 0) {
      _pageController.position.drag(dragStartDetails, () {});
    }

    // Simialrly Handle the last tab.
    if (notification is UserScrollNotification &&
        notification.direction == ScrollDirection.reverse &&
        !_tabController.indexIsChanging &&
        dragStartDetails != null &&
        _tabController.index == _tabController.length - 1) {
      _pageController.position.drag(dragStartDetails, () {});
    }

结语

至此深入进阶系列的Flutter事件分发相关文章总算写完了,感觉阅读源码是一个逐渐明朗的过程。对于一个装置如果不了解他的原理,就会把他想象的很复杂,但一层层解开后会发现其实整个过程没那么复杂,遇到问题也能从里面找到解决的思路。但源码的阅读切忌以小失大,先有一条主线索,看整个流程,之后再去把握细节,就不会陷入源码无法得其道了。

最后

下一期将会和大家一起研究Flutter UI原理,Flutter的是如何渲染?Widget的build是由谁回调?将在下期中一一揭晓。