2024的元旦,从滑动组件开始~
平时开发中,最常用的滑动组件差不多以下几种:ListView组件一族、NestedScrollView组件、SingleChildScrollView组件、PageView组件、ListWheelScrollView组件、GridView、CustomScrollView……
滑动的构成
在flutter中,其实所有的滑动组件都是基于三个部分:滑动处理组件 Scrollable
和 视口组件 Viewport
和 滑动内容 sliver 列表
。
从外至内 ListView 1、先来看下ListView
,首先,ListView
继承于BoxScrollView
,而BoxScrollView
继承于Scrollview
,Scrollview
继承于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
。 里面会根据itemExtent
和prototypeItem
参数来做判断使用哪种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
:
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
只有一种构造方法,其中,只有只做一下padding
,padding
参数是由子类传过来的,其余参数均由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
。
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
、Image
、Stack
…… RenderBox
基于RenderObject
,扩展了两个重要的属性:size
属性:Size
类型,表示该渲染对象的尺寸。constraints
属性:BoxConstraints
类型,表示该渲染对象受到的布局约束。
RenderSliver 开发中,使用到的以Sliver
为开头的组件都是依赖于RenderSliver
实现的。如SliverList
、SliverOpacity
、SliverPadding
…..RenderSliver
基于RenderObject
,扩展了两个重要的属性:geometry
属性:SliverGeometry 类型,表示该渲染对象的几何信息。constraints
属性:SliverConstraints 类型,表示该渲染对象受到的滑动布局约束。RenderViewport
中限定了其子渲染对象们必须是RenderSliver
,所以很多时候会发现组件的子组件不接受RenderBox
。
视口组件 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, ); .....
其中,必须传入一个ViewportOffset
类型的offset
参数。 在这些参数当中,有两个比较重要,那就是cacheExtent
和cacheExtentStyle
。这两个值共同决定了缓存空间的大小。 1、cacheExtent
该值表示缓存区域的大小,是一个double
。 2、cacheExtentStyle
该值表示缓存的单位,是一个枚举:pixel
默认值,表示单位是逻辑像素,viewport
表示单位是视口主轴方向尺寸。
1 2 3 4 enum CacheExtentStyle { pixel, // 默认 viewport, }
前面提到了offset
参数,offset
参数是外面的滑动处理组件 Scrollable
确定后传给Viewport
使用的。
在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 { ....
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
负责滑动内容。
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
也是如此。 ……