2024的元旦,从滑动组件开始~

平时开发中,最常用的滑动组件差不多以下几种:ListView组件一族、NestedScrollView组件、SingleChildScrollView组件、PageView组件、ListWheelScrollView组件、GridView、CustomScrollView……

滑动的构成

滑动体系 滑动三元素

在flutter中,其实所有的滑动组件都是基于三个部分:滑动处理组件 Scrollable 视口组件 Viewport滑动内容 sliver 列表

scrollView继承关系

从外至内

ListView

1、先来看下ListView,首先,ListView继承于BoxScrollView,而BoxScrollView继承于ScrollviewScrollview继承于StatelessWidget,其中,ListView是一个普通类,BoxScrollView Scrollview StatelessWidget 都是抽象类。我们知道抽象类的子类都需要实现父类的抽象方法。所以很多的参数都是交给父类去初始化的,各个抽象类都只需要完成各自的工作即可。

1
2
3
4
5
class ListView extends BoxScrollView {
abstract class BoxScrollView extends ScrollView {
abstract class ScrollView extends StatelessWidget {
...

ListView一种有四种构造方法,ListView构造 ListView.builder构造 ListView.separated构造 ListView.custom构造。因为ListView 继承了
BoxScrollView 抽象类,所以它需要重写BoxScrollView 的抽象方法buildChildLayout
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent!,
);
} else if (prototypeItem != null) {
return SliverPrototypeExtentList(
delegate: childrenDelegate,
prototypeItem: prototypeItem!,
);
}
return SliverList(delegate: childrenDelegate);
}

其实ListView的底层里面,都是使用的Slivers

里面会根据itemExtentprototypeItem参数来做判断使用哪种Slivers

我用的最多的是SliverList(delegate: childrenDelegate)
从源码中可以看出来,里面的好多参数都是super.xxx的,从这里可以确定好多参数都是给父类们使用。

这里从源码可以看出来ListView会从StatelessWidget 继承而来,可以为何它却可以改变呢?

GridView

ListView 类似,GridView 的继承关系也是如此。

1
2
3
4
class GridView extends BoxScrollView {
abstract class BoxScrollView extends ScrollView {
abstract class ScrollView extends StatelessWidget {
...

GridView的源码体系中,也和ListView一样,需要重新抽象类的抽象方法。其中,不同的只有在GridView 源码中,其余的BoxScrollView ScrollView 等一模一样。

而在buildChildLayout的重写方法中,只有:

1
2
3
4
5
6
7
@override
Widget buildChildLayout(BuildContext context) {
return SliverGrid(
delegate: childrenDelegate,
gridDelegate: gridDelegate,
);
}

ok,接下来,研究下参数childrenDelegate gridDelegate

可以看出GridView 一共有5种构造方法。其中,不管是哪一种,要么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
GridView({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
super.padding,
required this.gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
super.cacheExtent,
List<Widget> children = const <Widget>[],
int? semanticChildCount,
super.dragStartBehavior,
super.clipBehavior,
super.keyboardDismissBehavior,
super.restorationId,
}) : assert(gridDelegate != null),
childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
semanticChildCount: semanticChildCount ?? children.length,
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
GridView.builder({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
super.padding,
required this.gridDelegate,
required IndexedWidgetBuilder itemBuilder,
ChildIndexGetter? findChildIndexCallback,
int? itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
super.cacheExtent,
int? semanticChildCount,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
}) : assert(gridDelegate != null),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
semanticChildCount: semanticChildCount ?? itemCount,
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const GridView.custom({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
super.padding,
required this.gridDelegate,
required this.childrenDelegate,
super.cacheExtent,
super.semanticChildCount,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
}) : assert(gridDelegate != null),
assert(childrenDelegate != null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
GridView.count({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
super.padding,
required int crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
super.cacheExtent,
List<Widget> children = const <Widget>[],
int? semanticChildCount,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
}) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
semanticChildCount: semanticChildCount ?? children.length,
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
GridView.extent({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
super.padding,
required double maxCrossAxisExtent,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
super.cacheExtent,
List<Widget> children = const <Widget>[],
int? semanticChildCount,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
}) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
semanticChildCount: semanticChildCount ?? children.length,
);

我们可以看出来,这几种构造方法中,要么gridDelegate childrenDelegate 均为必须,要么自己会在初始化后新建出来。

ok,那我们进入父类的BoxScrollView:

BoxScrollView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class BoxScrollView extends ScrollView {
/// Creates a [ScrollView] uses a single child layout model.
///
/// If the [primary] argument is true, the [controller] must be null.
const BoxScrollView({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
this.padding,
super.cacheExtent,
super.semanticChildCount,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
});
....

从源码来看,BoxScrollView 只有一种构造方法,其中,只有只做一下paddingpadding 参数是由子类传过来的,其余参数均由super.xxx传给其父类使用。因为其继承了抽象类ScrollView,所以,BoxScrollView 需要重写父类的抽象方法buildSlivers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@override
List<Widget> buildSlivers(BuildContext context) {
/// 调用自己的抽象方法(子类去实现)返回sliver
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry? effectivePadding = padding;
if (padding == null) {
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}

if (effectivePadding != null) {
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
}
return <Widget>[ sliver ];
}

// 抽象方法
@protected
Widget buildChildLayout(BuildContext context);

从源码中,可知:会对padding参数做非空判断,如果是空的话,就会取 final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);的值,就是mediaQuery,然后返回MediaQuery,其child 值就是sliver,也就是子类重写buildChildLayout而返回的widget。最后返回<Widget>[ sliver ]
OK,接下来,我们看最为关键的ScrollView

ScrollView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/// 很多子类传过来的值,将在这里使用到。
const ScrollView({
super.key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
ScrollPhysics? physics,
this.scrollBehavior,
this.shrinkWrap = false,
this.center,
this.anchor = 0.0,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : assert(scrollDirection != null),
assert(reverse != null),
assert(shrinkWrap != null),
assert(dragStartBehavior != null),
assert(clipBehavior != null),
assert(
!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance '
'from a PrimaryScrollController widget. You cannot both set primary to '
'true and pass an explicit controller.',
),
assert(!shrinkWrap || center == null),
assert(anchor != null),
assert(anchor >= 0.0 && anchor <= 1.0),
assert(semanticChildCount == null || semanticChildCount >= 0),
physics = physics ?? ((primary ?? false) || (primary == null && controller == null && identical(scrollDirection, Axis.vertical)) ? const AlwaysScrollableScrollPhysics() : null);

···

@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
assert(() {
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.down:
return debugCheckHasDirectionality(
context,
why: 'to determine the cross-axis direction of the scroll view',
hint: 'Vertical scroll views create Viewport widgets that try to determine their cross axis direction '
'from the ambient Directionality.',
);
case AxisDirection.left:
case AxisDirection.right:
return true;
}
}());
/// 根据shrinkWrap参数,返回 Viewport 或者 ShrinkWrappingViewport
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
clipBehavior: clipBehavior,
);
}
return Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
clipBehavior: clipBehavior,
);
}


···

@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context); // 首先,我们需要调用本类ScrollView的抽象方法(交由子类去实现),返回一个 List<Widget>
final AxisDirection axisDirection = getDirection(context);

final bool effectivePrimary = primary
?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection);

final ScrollController? scrollController = effectivePrimary
? PrimaryScrollController.of(context)
: controller;

final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
scrollBehavior: scrollBehavior,
semanticChildCount: semanticChildCount,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
clipBehavior: clipBehavior,
);

final Widget scrollableResult = effectivePrimary && scrollController != null
// Further descendant ScrollViews will not inherit the same PrimaryScrollController
? PrimaryScrollController.none(child: scrollable)
: scrollable;

if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
return NotificationListener<ScrollUpdateNotification>(
child: scrollableResult,
onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode focusScope = FocusScope.of(context);
if (notification.dragDetails != null && focusScope.hasFocus) {
focusScope.unfocus();
}
return false;
},
);
} else {
return scrollableResult;
}
}
...

ScrollView 继承自StatelessWidget ,因为StatelessWidget 是抽象类,我们需要重写它的抽象方法build

在子类ScrollView 重写build方法中,首先会调用自身的final List<Widget> slivers = buildSlivers(context);抽象方法,返回一组List<Widget>。然后就到了滑动处理组件 Scrollable, 在它的viewportBuilder 的回调里面,会返回一个buildViewport 方法,在buildViewport方法中,会根据shrinkWrap 的值来确定是返回ShrinkWrappingViewport还是Viewport,而前面的抽象方法buildSlivers 返回的slivers 就是在参数slivers 上返回了。而shrinkWrap 的值是子类返回的,如果子类没返回的话,默认是false ,返回的是Viewport

至此,我们知道了滑动模块的组成源码大致长这样:

其实所有的滑动组件都是基于三个部分:滑动处理组件 Scrollable 视口组件 Viewport滑动内容 sliver 列表

滑动内容 sliver列表

sliver可译小滑块。

Flutter有两种渲染类型:RenderBox RenderSliver ,它们都是RenderObject 对象之一。

RenderBox

开发中,使用到的部分组件都是依赖于RenderBox 实现的。如Text ImageStack……

RenderBox 基于RenderObject ,扩展了两个重要的属性:

size 属性:Size 类型,表示该渲染对象的尺寸。
constraints 属性:BoxConstraints 类型,表示该渲染对象受到的布局约束。

RenderSliver

开发中,使用到的以Sliver 为开头的组件都是依赖于RenderSliver 实现的。如SliverList SliverOpacity SliverPadding …..

RenderSliver 基于RenderObject ,扩展了两个重要的属性:

geometry 属性:SliverGeometry 类型,表示该渲染对象的几何信息。
constraints 属性:SliverConstraints 类型,表示该渲染对象受到的滑动布局约束。
RenderViewport 中限定了其子渲染对象们必须是RenderSliver ,所以很多时候会发现组件的子组件不接受RenderBox

视口组件 Viewport

Viewport继承关系

从源码中,可以看出Viewport 继承自MultiChildRenderObjectWidget ,表示它可以接收多个子组件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Viewport extends MultiChildRenderObjectWidget {

Viewport({
super.key,
this.axisDirection = AxisDirection.down,
this.crossAxisDirection,
this.anchor = 0.0,
required this.offset,
this.center,
this.cacheExtent,
this.cacheExtentStyle = CacheExtentStyle.pixel,
this.clipBehavior = Clip.hardEdge,
List<Widget> slivers = const <Widget>[],
}) : assert(offset != null),
assert(slivers != null),
assert(center == null || slivers.where((Widget child) => child.key == center).length == 1),
assert(cacheExtentStyle != null),
assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
assert(clipBehavior != null),
super(children: slivers);
.......
@override
RenderViewport createRenderObject(BuildContext context) {
return RenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
cacheExtentStyle: cacheExtentStyle,
clipBehavior: clipBehavior,
);
.....

Viewport属性

其中,必须传入一个ViewportOffset类型的offset 参数。

在这些参数当中,有两个比较重要,那就是cacheExtent cacheExtentStyle 。这两个值共同决定了缓存空间的大小。

VIewport构成

1、cacheExtent 该值表示缓存区域的大小,是一个double
2、cacheExtentStyle 该值表示缓存的单位,是一个枚举:pixel 默认值,表示单位是逻辑像素,viewport 表示单位是视口主轴方向尺寸。

1
2
3
4
enum CacheExtentStyle {
pixel, // 默认
viewport,
}

前面提到了offset参数,offset参数是外面的滑动处理组件 Scrollable 确定后传给Viewport 使用的。

滑动处理组件 Scrollable

Scrollview的源码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

.....


@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
assert(() {
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.down:
return debugCheckHasDirectionality(
context,
why: 'to determine the cross-axis direction of the scroll view',
hint: 'Vertical scroll views create Viewport widgets that try to determine their cross axis direction '
'from the ambient Directionality.',
);
case AxisDirection.left:
case AxisDirection.right:
return true;
}
}());
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
clipBehavior: clipBehavior,
);
}
return Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
clipBehavior: clipBehavior,
);
}

........

@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);

final bool effectivePrimary = primary
?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection);

final ScrollController? scrollController = effectivePrimary
? PrimaryScrollController.of(context)
: controller;

final Scrollable scrollable = Scrollable( // 从这里开始,Scrollable负责监听滑动
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
scrollBehavior: scrollBehavior,
semanticChildCount: semanticChildCount,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) { // tag viewportBuilder的回调函数中,返回一个
return buildViewport(context, offset, axisDirection, slivers);
},
clipBehavior: clipBehavior,
);

final Widget scrollableResult = effectivePrimary && scrollController != null
// Further descendant ScrollViews will not inherit the same PrimaryScrollController
? PrimaryScrollController.none(child: scrollable)
: scrollable;

if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
return NotificationListener<ScrollUpdateNotification>(
child: scrollableResult,
onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode focusScope = FocusScope.of(context);
if (notification.dragDetails != null && focusScope.hasFocus) {
focusScope.unfocus();
}
return false;
},
);
} else {
return scrollableResult;
}
}

首先,Scrollable 会监听着用户滑动屏幕,然后在 viewportBuilder 的回调方法里面,返回context offset ,该offset的返回值,就是Viewport当中使用的值。所以Viewport只是负责偏移窗口的显示,而真正负责滑动相关的其实是Scrollable

这里从源码可以看出来ListView会从StatelessWidget 继承而来,可以为何它却可以改变呢?
由上面可知,真正负责监听滑动的其实是Scrollable ,其余的都只是负责显示即可。

1
2
class Scrollable extends StatefulWidget {
....

其他滑动widget

CustomScrollView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CustomScrollView extends ScrollView {
/// Creates a [ScrollView] that creates custom scroll effects using slivers.
///
/// See the [ScrollView] constructor for more details on these arguments.
const CustomScrollView({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.scrollBehavior,
super.shrinkWrap,
super.center,
super.anchor,
super.cacheExtent,
this.slivers = const <Widget>[],
super.semanticChildCount,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
});

/// The slivers to place inside the viewport.
final List<Widget> slivers;

@override
List<Widget> buildSlivers(BuildContext context) => slivers;
}

CustomScrollView 是直接继承自ScrollView ,重写buildSlivers方法,返回slivers ,而该slivers 就是使用者自己使用的一个数组形式的Widget 即可。

pageView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class PageView extends StatefulWidget {
....

@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
);

return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: widget.controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}

它也是由Scrollable 负责监听,Viewport 负责视口组件,Slivers负责滑动内容。

ListWheelScrollView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ListWheelScrollView extends StatefulWidget {

....
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: _FixedExtentScrollable(
controller: scrollController,
physics: widget.physics,
itemExtent: widget.itemExtent,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return ListWheelViewport(
diameterRatio: widget.diameterRatio,
perspective: widget.perspective,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
overAndUnderCenterOpacity: widget.overAndUnderCenterOpacity,
itemExtent: widget.itemExtent,
squeeze: widget.squeeze,
renderChildrenOutsideViewport: widget.renderChildrenOutsideViewport,
offset: offset,
childDelegate: widget.childDelegate,
clipBehavior: widget.clipBehavior,
);
},
),
);
}
...

class _FixedExtentScrollable extends Scrollable {
const _FixedExtentScrollable({
super.controller,
super.physics,
required this.itemExtent,
required super.viewportBuilder,
super.restorationId,
super.scrollBehavior,
});

final double itemExtent;

@override
_FixedExtentScrollableState createState() => _FixedExtentScrollableState();
}

从源码可知ListWheelScrollView 也是如此。

……