Flutter擴展NestedScrollView(二)列表滾動同步解決

接著上篇,沒看上篇的小夥伴建議先看下上篇,免得斷片中..

我繼續講下第2個問題的解決方案。

當在裡面放上TabView的,並且選項卡是緩存狀態的時候,會出現滾動會互相影響的問題

上篇我們說到在我們的主角NestedScrollView當中,有2個ScrollController。

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

一個是內,一個外。外部負責headerSliv??erBuilder裡面的滾動小部件內部是負責體內面的滾動小部件當外滾動到底了之後,就會看看內裡面是否有能滾動的東東,開始滾動

Tabview是在體裡面,這裡我們肯定需要對內進行處理。首先我們要明白,NestedScrollView是怎麼處理外和內的關係的。

找到這個_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 its 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是我們要關注的東西,通過調試,我發現,如果tabview的每個tab做了緩存,那麼每個tab裡面列表的ScrollPosition將一直緩存在這個ScrollController裡面。當tab到tabview的某個tab的時候,ScrollController將會這個ScrollPosition附上,如果沒有緩存,將會在離開的時候脫離掉。

@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 ...終於我只能想到一個解決方法。暫時解決這個問題。

提供一個容器,把內裡面的滾動列表包裹起來,並且設置它的標籤的唯一鍵

//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);
// }
}

然後在剛才連接方法中通過先祖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鍵..我這裡是一級標籤+二級標籤的索引..比如Tab00代表一級標籤第一個下面的二級標籤的第一個。

定義標籤裡面的列表的時候如下,比如第一個標籤下面的二級標籤的第一個列表,那麼它的密鑰為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,如果你有更好的方式解決這個問題或者有什麼不明白的地方,都請告訴我,由衷感謝。

知乎關注我:私信回復「資料」獲取高級UI、Gradle、RxJava、小程序、Hybrid、移動架構、React Native、性能優化等技術教程!架構師課程、NDK、混合式開發全方 面的 Android高級實踐技術講解性能優化架構思維導圖,和BATJ面試題及答案。


推薦閱讀:

TAG:Flutter | Android | Android開發 |