讲道理我起的好长的名字啊,不过文如上题,搜索到这里的兄弟应该都知道我说的是啥情况,正好
~~
我这个方案可能有点笨拙TT,不过自测有效,有其它想法的老哥希望可以帮忙指点一下~
下面进入正题
在这之前我们先了解一下,一个ScrollView
是怎么滚动的
点进源码里面看,可以发现他直接继承了StatelessWidget
,那我们就直接看看build方法
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController scrollController = primary
? PrimaryScrollController.of(context)
: controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
return primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
可以看到,这里直接返回一个scrollable
或者一个子节点是scrollable
的InheritedWidget
scrollable
是一个StatefulWidget
,那我们就看看它的state
首先scrollable
持有一个scrollposition
对象,是通过其scrollcontroller
构建的
_position = controller?.createScrollPosition(_physics, this, oldPosition)
//scrollcontroller
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return ScrollPositionWithSingleContext(
physics: physics,
context: context,//这里可以看到,scrollposition也是持有其scrollable的
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
在其state
的setCanDrag
方法中,对其拖动设置了一系列的监听
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);
}
这里就可以看出来,当拖动触发时,就会通过当前scrollable
的position
生成一个Drag/Hold
对象,并调用相应的方法 这个position
有几个子类,我们先随便看一个实现
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
final ScrollDragController drag = ScrollDragController(
delegate: this,
details: details,
onDragCanceled: dragCancelCallback,
carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
);
beginActivity(DragScrollActivity(this, drag));
assert(_currentDrag == null);
_currentDrag = drag;
return drag;
}
可以看到生成了一个ScrollDragController
对象,当手势拖动而调用这个对象的update
方法时
void update(DragUpdateDetails details) {
···省略
delegate.applyUserOffset(offset);
}
可以看到直接调用其委托对象的applyUserOffset
方法进行偏移,而这个委托对象根据刚才的drag
方法可以得知正是我们scrollable
中的position
最后,由position
通知其scrollcontext
,也就是之前的scrollable进行滑动
void applyUserOffset(double delta) {
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}
double setPixels(double newPixels) {
assert(_pixels != null);
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
if (newPixels != pixels) {
···
final double oldPixels = _pixels;
_pixels = newPixels - overscroll;
if (_pixels != oldPixels) {
notifyListeners();
didUpdateScrollPositionBy(_pixels - oldPixels);
}
if (overscroll != 0.0) {
didOverscrollBy(overscroll);
return overscroll;
}
}
return 0.0;
}
void didUpdateScrollPositionBy(double delta) {
activity.dispatchScrollUpdateNotification(copyWith(), context.notificationContext, delta);
}
具体的滑动流程这里就不细说了,我们只是要知道这个事件是怎么传递的就好了,有兴趣的老哥可以自行分析
然后我们要知道NestedScrollView
是怎么进行滚动传递的,以及为啥会出现同步滑动的情况
NestedScrollView
是一个statefulwidget
,那我们就先看看它的build
方法
Widget build(BuildContext context) {
return _InheritedNestedScrollView(
state: this,
child: Builder(
builder: (BuildContext context) {
_lastHasScrolledBody = _coordinator.hasScrolledBody;
return _NestedScrollViewCustomScrollView(
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
physics: widget.physics != null
? widget.physics.applyTo(const ClampingScrollPhysics())
: const ClampingScrollPhysics(),
controller: _coordinator._outerController,
slivers: widget._buildSlivers(
context,
_coordinator._innerController,
_lastHasScrolledBody,
),
handle: _absorberHandle,
);
},
),
);
}
List<Widget> _buildSlivers(BuildContext context, ScrollController innerController, bool bodyIsScrolled) {
final List<Widget> slivers = <Widget>[];
slivers.addAll(headerSliverBuilder(context, bodyIsScrolled));
slivers.add(SliverFillRemaining(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
));
return slivers;
}
先忽略其他奇奇怪怪的方法,我们发现在我们body
的外面,包裹了一层PrimaryScrollController
,同时它还持有innerController
,这个innerController
暂时先不管它是啥
还记不记得在最开始ScrollView
的build
方法中,生成Scrollable
的时候,我们已经见过这个PrimaryScrollController
了,再回顾一下
final ScrollController scrollController = primary
? PrimaryScrollController.of(context)
: controller;
再看看PrimaryScrollController.of(context)
static ScrollController of(BuildContext context) {
final PrimaryScrollController result = context.inheritFromWidgetOfExactType(PrimaryScrollController);
return result?.controller;
}
可以看到,在生成scrollable
的时候,在primary = true
的情况下是会向上查找的,看看有没有PrimaryScrollController
,如果有的话,scrollable使用的controller
实际就是nestedscrollview
中的innerController
了
而之前看过了,scrollable
中的position
就是scrollcontroller
来生成的,那么在这种情况下:
_innerController = _NestedScrollController(this, initialScrollOffset: 0.0, debugLabel: 'inner');
//_NestedScrollController
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return _NestedScrollPosition(
coordinator: coordinator,
physics: physics,
context: context,
initialPixels: initialScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
实际上是生成了_NestedScrollPosition
并返回给了body
中的scrollable
构造方法中有一个参数coordinator
暂时先不管
好了,下面我们在回头看刚才NestedScrollView
的build
方法,实际上是生成了一个_NestedScrollViewCustomScrollView
,继承自大名鼎鼎的CustomScrollView
,它当然也是scrollview
啦,而我们传给它的controller
也是一个_NestedScrollController
,不过叫做_outerController
,和body
中的不是同一个罢了,那么自然这个父scrollview
的position
也是_NestedScrollPosition
。
下面我们按照之前的逻辑,当拖动开始时,就会调用position.drag
方法
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
return coordinator.drag(details, dragCancelCallback);
}
可以看到,实际上吧方法交给了我们之前多次见到的coordinator
来完成,那我们就简单看一下吧
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
final ScrollDragController drag = ScrollDragController(
delegate: this,//
details: details,
onDragCanceled: dragCancelCallback,
);
beginActivity(
DragScrollActivity(_outerPosition, drag),
(_NestedScrollPosition position) => DragScrollActivity(position, drag),
);
assert(_currentDrag == null);
_currentDrag = drag;
return drag;
}
这里可以看到,他把返回的ScrollDragController
的委托者设成了自己
那么自然在拖动的时候,调用的就是coordinator
的applyUseroffset
方法了 我们分析一下
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) {
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);
}
}
}
可以看到,在需要子列表滚动时,是对innerPositions
中的所有position
调用滑动方法的
而这innerPositions
中的position
是怎么来的呢?跟踪一下可以发现是在调用NestedScrollController
的attach
时添加进来的,如下
/// Register the given position with this controller.
///
/// After this function returns, the [animateTo] and [jumpTo] methods on this
/// controller will manipulate the given position.
void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
}
/// Unregister the given position with this controller.
///
/// After this function returns, the [animateTo] and [jumpTo] methods on this
/// controller will not manipulate the given position.
void detach(ScrollPosition position) {
assert(_positions.contains(position));
position.removeListener(notifyListeners);
_positions.remove(position);
}
因为之前我们看到过,子scrollable
中的controller
就是这个NestedScrollController
,所以在updateopsition
时会把旧的detach
调,把新生成的position
attach
进来
void _updatePosition() {
_configuration = ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null)
_physics = widget.physics.applyTo(_physics);
final ScrollController controller = widget.controller;
final ScrollPosition oldPosition = position;
if (oldPosition != null) {
controller?.detach(oldPosition);//这就是最外层的NestedScrollController
// It's important that we not dispose the old position until after the
// viewport has had a chance to unregister its listeners from the old
// position. So, schedule a microtask to do it.
scheduleMicrotask(oldPosition.dispose);
}
_position = controller?.createScrollPosition(_physics, this, oldPosition)
?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
assert(position != null);
controller?.attach(position);
}
另外,在dispose
中也会detach
void dispose() {
widget.controller?.detach(position);
position.dispose();
super.dispose();
}
由此我们就知道啦,因为开启了缓存后就不会调用划出屏幕的页面的dispose
,自然所有子scrollable
的position
都存在nestedScrollController
里面了,当发生滑动时,遍历调用positions
数组,就导致屏幕外的列表也跟着滑动了~
最后呼应一下主题,谈一谈我的解决办法
既然开启了缓存,手动dispose
肯定是没啥意义的,实际上我们只要在页面切换过后把未显示的position
给detach
掉就好了。
然鹅,因为flutter
不支持反射,子布局传递的position
我们拿不到,nestedScrollController
我们也不能直接拿到=。=
不过有一个对象我们之前见到过,scrollable
就是通过他获取controller
的,而position
则是传给了获取到的controller 就是PrimaryScrollController
了,所以我打算在中间第三者插足,对传递Position
的PrimaryScrollController
进行Hook
import 'package:flutter/cupertino.dart';
class PrimaryScrollContainer extends StatefulWidget {
final Widget child;
const PrimaryScrollContainer(
GlobalKey<PrimaryScrollContainerState> key,
this.child,
) : super(key: key);
State<StatefulWidget> createState() {
return PrimaryScrollContainerState();
}
}
class PrimaryScrollContainerState extends State<PrimaryScrollContainer> {
late ScrollControllerWrapper _scrollController;
get scrollController {
final PrimaryScrollController? primaryScrollController =
context.dependOnInheritedWidgetOfExactType<PrimaryScrollController>();
if (primaryScrollController != null) {
_scrollController.inner = primaryScrollController.controller!;
}
return _scrollController;
}
void initState() {
_scrollController = ScrollControllerWrapper();
super.initState();
}
Widget build(BuildContext context) {
return PrimaryScrollControllerWrapper(
scrollController: scrollController,
child: widget.child,
);
}
void onPageChange(bool show) {
_scrollController.onAttachChange(show);
}
}
class PrimaryScrollControllerWrapper extends InheritedWidget
implements PrimaryScrollController {
final ScrollController scrollController;
const PrimaryScrollControllerWrapper({
Key? key,
required Widget child,
required this.scrollController,
}) : super(key: key, child: child);
get runtimeType => PrimaryScrollController;
get controller => scrollController;
bool updateShouldNotify(PrimaryScrollControllerWrapper oldWidget) =>
controller != oldWidget.controller;
Set<TargetPlatform> get automaticallyInheritForPlatforms => getPlatforms();
Axis? get scrollDirection => Axis.vertical;
Set<TargetPlatform> getPlatforms() {
Set<TargetPlatform> platforms = {};
platforms.add(TargetPlatform.android);
platforms.add(TargetPlatform.iOS);
platforms.add(TargetPlatform.fuchsia);
platforms.add(TargetPlatform.linux);
platforms.add(TargetPlatform.macOS);
platforms.add(TargetPlatform.windows);
return platforms;
}
}
//代理
class ScrollControllerWrapper implements ScrollController {
static int a = 1;
late ScrollController inner;
int code = a++;
ScrollPosition? interceptedAttachPosition; //拦截的position
ScrollPosition? lastPosition;
bool showing = true;
void addListener(listener) => inner.addListener(listener);
Future<void> animateTo(double offset,
{required Duration duration, required Curve curve}) =>
inner.animateTo(offset, duration: duration, curve: curve);
void attach(ScrollPosition position) {
// print('{$code}:attach start {$showing}');
// if (position == interceptedAttachPosition) print("attach by inner");
position.hasListeners;
// print('{$code}:attach end {$showing}');
if (inner.positions.contains(position)) return;
if (showing) {
inner.attach(position);
lastPosition = position;
} else {
interceptedAttachPosition = position;
}
}
void detach(ScrollPosition position, {bool fake = false}) {
assert(() {
// print('{$code}:detach start {$showing}');
return true;
}.call());
if (inner.positions.contains(position)) {
inner.detach(position);
}
if (position == interceptedAttachPosition && !fake) {
interceptedAttachPosition = null;
}
if (position == lastPosition && !fake) {
lastPosition = null;
}
if (fake) {
interceptedAttachPosition = position;
}
assert(() {
// print('{$code}:detach end {$showing}');
return true;
}.call());
}
void onAttachChange(bool b) {
showing = b;
if (!showing) {
if (lastPosition != null) {
detach(lastPosition!, fake: true);
}
} else {
if (interceptedAttachPosition != null) {
attach(interceptedAttachPosition!);
}
}
}
void debugFillDescription(List<String> description) =>
inner.debugFillDescription(description);
String get debugLabel => inner.debugLabel ?? '';
void dispose() => inner.dispose();
bool get hasClients => inner.hasClients;
bool get hasListeners => inner.hasListeners;
double get initialScrollOffset => inner.initialScrollOffset;
void jumpTo(double value) => inner.jumpTo(value);
bool get keepScrollOffset => inner.keepScrollOffset;
void notifyListeners() => inner.notifyListeners();
double get offset => inner.offset;
ScrollPosition get position => inner.position;
Iterable<ScrollPosition> get positions => inner.positions;
void removeListener(listener) => inner.removeListener(listener);
int get hashCode => inner.hashCode;
bool operator ==(other) {
return hashCode == (other.hashCode);
}
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return inner.createScrollPosition(physics, context, oldPosition);
}
}
在使用的时候把子列表添加进去,并设置对应的GlobalKey
。
body: TabBarView(
children: <Widget>[
PrimaryScrollContainer(
scrollChildKeys[0],//GlobalKey
NewArticle(),//内容列表
),
PrimaryScrollContainer(
scrollChildKeys[1],
NewProject(),
),
PrimaryScrollContainer(
scrollChildKeys[2],
TagSystemPage(),
),
],
controller: _tabController,
),
然后监听Tab切换
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
for (int i = 0; i < scrollChildKeys.length; i++) {
GlobalKey<PrimaryScrollContainerState> key = scrollChildKeys[i];
if (key.currentState != null) {
key.currentState.onPageChange(_tabController.index == i);//控制是否当前显示
}
}
});
以上是我的方案,有问题的话还希望老哥帮忙指正,也希望有其他思路的老哥指点一下~~
上一下Github项目地址 用Flutter写的WanAndroid 其中用到了这个方案
https://github.com/Doooyao/wanandroid-flutter
参考:
关于flutter NestedScrollView导致其body的tabbarview的多个list同步滚动的解决方案