Flutter 扩展NestedScrollView (二)列表滚动同步解决

接着上篇,没看上篇的小伙伴建议先看下上篇,免得断片中。。

我继续讲下第2个问题的解决方案。

当在里面放上tabview,并且tab是缓存状态的时候,会出现滚动会互相影响的问题

上篇我们说到 在我们的主角NestedScrollView当中,有2个ScrollController.

class _NestedScrollController extends ScrollController {
  _NestedScrollController(
      this.coordinator, {
        double initialScrollOffset = 0.0,
        String debugLabel,
复制代码

一个是inner,一个outer。 outer是负责headerSliverBuilder里面的滚动widgets inner是负责body里面的滚动widgets 当outer滚动到底了之后,就会看看inner里面是否有能滚动的东东,开始滚动

Tabview是在body里面,这里我们肯定需要对inner进行处理。 首先我们要明白,NestedScrollView是怎么处理outer和inner的关系的。

找到这个_NestedScrollCoordinator 的applyUserOffset方法中处理了整个NestedScrollView的滑动处理

 @override
  void applyUserOffset(double delta) {
    updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
    assert(delta != 0.0);
    if (_innerPositions.isEmpty) {
      _outerPosition.applyFullDragUpdate(delta);
    } else if (delta < 0.0) {
      // dragging "up"
      // TODO(ianh): prioritize first getting rid of overscroll, and then the
      // outer view, so that the app bar will scroll out of the way asap.
      // Right now we ignore overscroll. This works fine on Android but looks
      // weird on iOS if you fling down then up. The problem is it's not at all
      // clear what this should do when you have multiple inner positions at
      // different levels of overscroll.
      final double innerDelta = _outerPosition.applyClampedDragUpdate(delta);
      if (innerDelta != 0.0) {
        for (_NestedScrollPosition position in _innerPositions)
          position.applyFullDragUpdate(innerDelta);
      }
    } else {
      // dragging "down" - delta is positive
      // prioritize the inner views, so that the inner content will move before the app bar grows
      double outerDelta = 0.0; // it will go positive if it changes
      final List<double> overscrolls = <double>[];
      final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();
      for (_NestedScrollPosition position in innerPositions) {
        final double overscroll = position.applyClampedDragUpdate(delta);
        outerDelta = math.max(outerDelta, overscroll);
        overscrolls.add(overscroll);
      }
      if (outerDelta != 0.0)
        outerDelta -= _outerPosition.applyClampedDragUpdate(outerDelta);
      // now deal with any overscroll
      for (int i = 0; i < innerPositions.length; ++i) {
        final double remainingDelta = overscrolls[i] - outerDelta;
        if (remainingDelta > 0.0)
          innerPositions[i].applyFullDragUpdate(remainingDelta);
      }
    }
  }
复制代码
Iterable<_NestedScrollPosition> get _innerPositions {
    return _innerController.nestedPositions;
  }
复制代码

看到_innerPositions是我们要关注的东西,通过debug,我发现,如果tabview的每个tab做了缓存,那么每个tab里面列表的ScrollPosition将一直缓存在这个ScrollController里面。 当tab到tabview的某个tab的时候,ScrollController将会将这ScrollPosition attach 上,如果没有缓存,将会在离开的时候detach掉。


  @override
  void attach(ScrollPosition position) {
    assert(position is _NestedScrollPosition);
    super.attach(position);
    coordinator.updateParent();
    coordinator.updateCanDrag();
    position.addListener(_scheduleUpdateShadow);
    _scheduleUpdateShadow();
  }

  @override
  void detach(ScrollPosition position) {
    assert(position is _NestedScrollPosition);
    position.removeListener(_scheduleUpdateShadow);
    super.detach(position);
    _scheduleUpdateShadow();
  }
复制代码

真相只有一个。。是的。。可以说。。造成缓存tabview的各个tab里面的列表互相影响的原因,是因为歪果仁说: as design(我就是这样设计的,不服吗).

按照我的思想啊,我滚动的时候。当然只想影响当前显示的这个列表啊。这不科学啊。。

找到原因找到原理,一切就都好解决了。现在的关键点在于,我怎么能知道显示对应的是哪个列表的?!

这个问题问了很多人。。也查找了好久都没找到好的方式去获取当前 激活的 列表对应的 ScrollPosition。。终于我只能想到一个 workaround。暂时解决这个问题。

提供一个容器,把inner里面的滚动列表包裹起来,并且设置它的tab 的唯一key

//pack your inner scrollables which are in  NestedScrollView body
//so that it can find the active scrollable
//compare with NestedScrollViewInnerScrollPositionKeyBuilder
class NestedScrollViewInnerScrollPositionKeyWidget extends StatefulWidget {
  final Key scrollPositionKey;
  final Widget child;
  NestedScrollViewInnerScrollPositionKeyWidget(
      this.scrollPositionKey, this.child);
  @override
  _NestedScrollViewInnerScrollPositionKeyWidgetState createState() =>
      _NestedScrollViewInnerScrollPositionKeyWidgetState();
}

class _NestedScrollViewInnerScrollPositionKeyWidgetState
    extends State<NestedScrollViewInnerScrollPositionKeyWidget> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

//  @override
//  void didChangeDependencies() {
//    // TODO: implement didChangeDependencies
//    //print("didChangeDependencies"+widget.scrollPositionKey.toString());
//    super.didChangeDependencies();
//  }
//
//  @override
//  void didUpdateWidget(NestedScrollViewInnerScrollPositionKeyWidget oldWidget) {
//    // TODO: implement didUpdateWidget
//    //print("didUpdateWidget"+widget.scrollPositionKey.toString()+oldWidget.scrollPositionKey.toString());
//    super.didUpdateWidget(oldWidget);
//  }
}
复制代码

然后在刚才attach方法中通过先祖NestedScrollViewInnerScrollPositionKeyWidget

@override
  void attach(ScrollPosition position) {
    assert(position is _NestedScrollPosition);

    super.attach(position);
    attachScrollPositionKey(position as _NestedScrollPosition);
    coordinator.updateParent();
    coordinator.updateCanDrag();
    position.addListener(_scheduleUpdateShadow);
    _scheduleUpdateShadow();
  }

  @override
  void detach(ScrollPosition position) {
    assert(position is _NestedScrollPosition);
    position.removeListener(_scheduleUpdateShadow);
    super.detach(position);
    detachScrollPositionKey(position as _NestedScrollPosition);
    _scheduleUpdateShadow();
  }
  
  void attachScrollPositionKey(_NestedScrollPosition position) {
    if (position != null && scrollPositionKeyMap != null) {
      var key = position.setScrollPositionKey();
      if (key != null) {
        if (!scrollPositionKeyMap.containsKey(key)) {
          scrollPositionKeyMap[key] = position;
        } else if (scrollPositionKeyMap[key] != position) {
          //in demo ,when tab to tab03, the tab02 key will be tab00 at first
          //then it become tab02.
          //this is not a good solution

          position.clearScrollPositionKey();
          Future.delayed(Duration(milliseconds: 500), () {
            attachScrollPositionKey(position);
          });
        }
      }
    }
  }

  void detachScrollPositionKey(_NestedScrollPosition position) {
    if (position != null &&
        scrollPositionKeyMap != null &&
        position.key != null &&
        scrollPositionKeyMap.containsKey(position.key)) {
      scrollPositionKeyMap.remove(position.key);
      position.clearScrollPositionKey();
    }
  }
  
复制代码

获取先祖NestedScrollViewInnerScrollPositionKeyWidget方法

Key setScrollPositionKey() {
    //if (haveDimensions) {
    final type = _typeOf<NestedScrollViewInnerScrollPositionKeyWidget>();

    NestedScrollViewInnerScrollPositionKeyWidget keyWidget =
    (this.context as ScrollableState)
        ?.context
        ?.ancestorWidgetOfExactType(type);
    _key = keyWidget?.scrollPositionKey;
    return _key;
  }
复制代码

找到这个_NestedScrollCoordinator 的applyUserOffset方法中我们现在要替换掉 _innerPositions为_currentInnerPositions

Iterable<_NestedScrollPosition> get _innerPositions {
    //return _currentPositions;
    return _innerController.nestedPositions;
  }

  Iterable<_NestedScrollPosition> get _currentInnerPositions {
    return _innerController
        .getCurrentNestedPositions(innerScrollPositionKeyBuilder);
  }

复制代码

getCurrentNestedPositions里面的代码

  Iterable<_NestedScrollPosition> getCurrentNestedPositions(
      NestedScrollViewInnerScrollPositionKeyBuilder
      innerScrollPositionKeyBuilder) {
    if (innerScrollPositionKeyBuilder != null &&
        scrollPositionKeyMap.length > 1) {
      var key = innerScrollPositionKeyBuilder();
      if (scrollPositionKeyMap.containsKey(key)) {
        return <_NestedScrollPosition>[scrollPositionKeyMap[key]];
      } else {
        return nestedPositions;
      }
    }

    return nestedPositions;
  }
复制代码

SampeCode

extended.NestedScrollView(
        headerSliverBuilder: (c, f) {
          return _buildSliverHeader(primaryTabBar);
        },
        //
        pinnedHeaderSliverHeightBuilder: () {
          return pinnedHeaderHeight;
        },
        innerScrollPositionKeyBuilder: () {
          var index = "Tab";
          if (primaryTC.index == 0) {
            index +=
                (primaryTC.index.toString() + secondaryTC.index.toString());
          } else {
            index += primaryTC.index.toString();
          }
          return Key(index);
        },
复制代码

这里由你自己协定tab key。。我这里是一级tab+二级tab的index。。比如 Tab00代表一级tab第一个下面的二级tab的第一个。

定义tab里面的列表的时候如下,比如第一个tab下面的二级tab的第一个列表,那么它的key 为Tab00.

return extended.NestedScrollViewInnerScrollPositionKeyWidget(
        Key("Tab00"),
        // myRefresh.RefreshIndicator(
        // child:
        ListView.builder(
            itemBuilder: (c, i) {
              return Container(
                //decoration: BoxDecoration(border: Border.all(color: Colors.orange,width: 1.0)),
                alignment: Alignment.center,
                height: 60.0,
                child: Text(widget.tabKey.toString() + ": List$i"),
              );
            },
            itemCount: 100)
        //,
        //onRefresh: onRefresh,
        // )
        );
复制代码

最后放上 Github extended_nested_scroll_view,如果你有更好的方式解决这个问题或者有什么不明白的地方,都请告诉我,由衷感谢。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现 Flutter NestedScrollView 和 TabView 滚动列表某一项,可以使用 ScrollController 和 TabController 来实现。 步骤如下: 1. 在 NestedScrollView 中添加 controller 属性,用于监听滚动事件。 2. 在 TabView 中添加 controller 属性,用于控制 Tab 切换。 3. 在列表中添加 controller 属性,用于监听列表滚动事件。 4. 在 TabView 中添加 onTap 属性,用于监听 Tab 切换事件。 5. 在 onTap 事件中,使用 ScrollController 和 TabController 来控制列表和 TabView 的滚动和切换。 具体实现细节可以参考以下代码: ```dart class MyPage extends StatefulWidget { @override _MyPageState createState() => _MyPageState(); } class _MyPageState extends State<MyPage> with SingleTickerProviderStateMixin { TabController _controller; ScrollController _scrollController; List _tabs = ['Tab1', 'Tab2', 'Tab3', 'Tab4', 'Tab5']; List<Widget> _tabViews = []; @override void initState() { super.initState(); _controller = TabController(length: _tabs.length, vsync: this); _scrollController = ScrollController(); for (int i = 0; i < _tabs.length; i++) { _tabViews.add(ListView.builder( controller: _scrollController, itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('${_tabs[i]} - Item $index'), ); }, )); } } @override void dispose() { _controller.dispose(); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( controller: _scrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( title: Text('NestedScrollView'), pinned: true, floating: true, snap: true, bottom: TabBar( controller: _controller, tabs: _tabs.map((tab) => Tab(text: tab)).toList(), ), ), ]; }, body: TabBarView( controller: _controller, children: _tabViews, ), ), ); } } ``` 以上代码中,我们使用了一个 List 来存储 Tab 标题,使用 for 循环来创建 TabView,每个 TabView 都是一个 ListView,通过传入不同的数据来区分不同的 TabView,同时在 ListView 中添加了 controller 属性来监听列表滚动事件。 在 TabBar 中添加了 onTap 属性,当 Tab 切换时,使用 _controller.animateTo() 方法来控制 TabView 的滚动位置,同时也使用 _scrollController.animateTo() 方法来控制列表滚动位置。 这样就可以实现在 NestedScrollView 和 TabView 中滚动列表某一项的效果了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值