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,如果你有更好的方式解决这个问题或者有什么不明白的地方,都请告诉我,由衷感谢。
想学习更多Android知识,或者获取相关资料请加入Android技术开发交流2群:935654177。本群可免费获取Gradle,RxJava,小程序,Hybrid,移动架构,NDK,React Native,性能优化等技术教程!
- NestedScrollView与RecyclerView嵌套,以及NestedScrollView不会滚动到屏幕顶部解决
- 解决android ScrollView滚动显示不全问题
- 解决ListView在ScrollView中加载完成后,界面直接滚动到ListView的位置的办法【已测试,很好用】
- NestedScrollView、RecycleView、ViewPager 常见问题汇总,及解决
- 无法同步Chrome书签、扩展的解决方法
- 在ScrollView添加一个ListView造成的滚动问题的简单解决办法()
- Android 中listview 和scrollview 滚动冲突事件的解决方法
- 解决Android中,禁止ScrollView内的控件改变之后自动滚动
- 解决TextView多行滑动与NestedScrollView等,滑动冲突,我的解决方案
- 重写ScrollView实现两个ScrollView的同步滚动显示
- NestedScrollView替代了ScrollView解决嵌套难题
- 解决点击状态栏时ScrollView自动滚动到初始位置失效办法
- 重写ScrollView实现两个ScrollView的同步滚动显示
- 含有按钮的ScrollView在iOS8中无法滚动的解决办法 | ScrollView with UIControl/UIButton subviews not scrollable under iOS 8
- 解决Android中,禁止ScrollView内的控件改变之后自动滚动
- 解决点击状态栏时ScrollView自动滚动到初始位置失效办法
- 列表_下拉列表_可扩展的列表_滚动_画廊_图像切换_网格_标签_惰性装载
- Xmarks书签扩展不能同步的解决方法
- Android实现两个ScrollView互相联动,同步滚动的效果
- IOS scrollView 不能滚动解决方法