├── doc
└── images
│ ├── 1581152212.gif
│ └── 1581152222.gif
├── README.md
├── .metadata
├── lib
├── page3.dart
├── page2.dart
├── page1.dart
├── shop
│ ├── shop_scroll_coordinator.dart
│ ├── shop_scroll_controller.dart
│ └── shop_scroll_position.dart
└── main.dart
├── .gitignore
├── test
└── widget_test.dart
├── pubspec.yaml
└── pubspec.lock
/doc/images/1581152212.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyjaysong/flutter_meituan_shop/HEAD/doc/images/1581152212.gif
--------------------------------------------------------------------------------
/doc/images/1581152222.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyjaysong/flutter_meituan_shop/HEAD/doc/images/1581152222.gif
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |

4 |
5 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 659dc8129d4edb9166e9a0d600439d135740933f
8 | channel: beta
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/lib/page3.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'shop/shop_scroll_controller.dart';
4 | import 'shop/shop_scroll_coordinator.dart';
5 |
6 | class Page3 extends StatefulWidget {
7 | const Page3({Key key}) : super(key: key);
8 |
9 | @override
10 | _Page3State createState() => _Page3State();
11 | }
12 |
13 | class _Page3State extends State {
14 | @override
15 | Widget build(BuildContext context) {
16 | return Center(
17 | child: Text("店铺信息页面"),
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 | .packages
29 | .pub-cache/
30 | .pub/
31 | /build/
32 |
33 | # Web related
34 | lib/generated_plugin_registrant.dart
35 |
36 | # Exceptions to above rules.
37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
38 |
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:flutter_meituan_shop/main.dart';
12 |
13 | void main() {
14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(MyApp());
17 |
18 | // Verify that our counter starts at 0.
19 | expect(find.text('0'), findsOneWidget);
20 | expect(find.text('1'), findsNothing);
21 |
22 | // Tap the '+' icon and trigger a frame.
23 | await tester.tap(find.byIcon(Icons.add));
24 | await tester.pump();
25 |
26 | // Verify that our counter has incremented.
27 | expect(find.text('0'), findsNothing);
28 | expect(find.text('1'), findsOneWidget);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/lib/page2.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'shop/shop_scroll_controller.dart';
4 | import 'shop/shop_scroll_coordinator.dart';
5 |
6 | class Page2 extends StatefulWidget {
7 | final ShopScrollCoordinator shopCoordinator;
8 |
9 | const Page2({@required this.shopCoordinator, Key key}) : super(key: key);
10 |
11 | @override
12 | _Page2State createState() => _Page2State();
13 | }
14 |
15 | class _Page2State extends State {
16 | ShopScrollCoordinator _shopCoordinator;
17 | ShopScrollController _listScrollController;
18 |
19 | @override
20 | void initState() {
21 | _shopCoordinator = widget.shopCoordinator;
22 | _listScrollController = _shopCoordinator.newChildScrollController();
23 | super.initState();
24 | }
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | return ListView.builder(
29 | padding: EdgeInsets.all(0),
30 | physics: ClampingScrollPhysics(),
31 | controller: _listScrollController,
32 | itemExtent: 150,
33 | itemBuilder: (context, index) => Container(
34 | child: Material(
35 | elevation: 4.0,
36 | borderRadius: BorderRadius.circular(5.0),
37 | color: index % 2 == 0 ? Colors.cyan : Colors.deepOrange,
38 | child: Center(child: Text(index.toString())),
39 | ),
40 | ),
41 | );
42 | }
43 |
44 | @override
45 | void dispose() {
46 | _listScrollController?.dispose();
47 | _listScrollController = null;
48 | super.dispose();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/page1.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'shop/shop_scroll_controller.dart';
4 | import 'shop/shop_scroll_coordinator.dart';
5 |
6 | class Page1 extends StatefulWidget {
7 | final ShopScrollCoordinator shopCoordinator;
8 |
9 | const Page1({@required this.shopCoordinator, Key key}) : super(key: key);
10 |
11 | @override
12 | _Page1State createState() => _Page1State();
13 | }
14 |
15 | class _Page1State extends State {
16 | ShopScrollCoordinator _shopCoordinator;
17 | ShopScrollController _listScrollController1;
18 | ShopScrollController _listScrollController2;
19 |
20 | @override
21 | void initState() {
22 | _shopCoordinator = widget.shopCoordinator;
23 | _listScrollController1 = _shopCoordinator.newChildScrollController();
24 | _listScrollController2 = _shopCoordinator.newChildScrollController();
25 | super.initState();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return Row(
31 | children: [
32 | Expanded(
33 | child: ListView.builder(
34 | padding: EdgeInsets.all(0),
35 | physics: AlwaysScrollableScrollPhysics(),
36 | controller: _listScrollController1,
37 | itemExtent: 50.0,
38 | itemCount: 20,
39 | itemBuilder: (context, index) => Container(
40 | child: Material(
41 | elevation: 4.0,
42 | borderRadius: BorderRadius.circular(5.0),
43 | color: index % 2 == 0 ? Colors.cyan : Colors.deepOrange,
44 | child: Center(child: Text(index.toString())),
45 | ),
46 | ),
47 | ),
48 | ),
49 | Expanded(
50 | flex: 4,
51 | child: ListView.builder(
52 | padding: EdgeInsets.all(0),
53 | physics: AlwaysScrollableScrollPhysics(),
54 | controller: _listScrollController2,
55 | itemExtent: 200.0,
56 | itemCount: 30,
57 | itemBuilder: (context, index) => Container(
58 | padding: EdgeInsets.symmetric(horizontal: 1),
59 | child: Material(
60 | elevation: 4.0,
61 | borderRadius: BorderRadius.circular(5.0),
62 | color: index % 2 == 0 ? Colors.cyan : Colors.deepOrange,
63 | child: Center(child: Text(index.toString())),
64 | ),
65 | ),
66 | ),
67 | ),
68 | ],
69 | );
70 | }
71 |
72 | @override
73 | void dispose() {
74 | _listScrollController1?.dispose();
75 | _listScrollController2?.dispose();
76 | _listScrollController1 = _listScrollController2 = null;
77 | super.dispose();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flutter_meituan_shop
2 |
3 | description: A new Flutter project.
4 |
5 | # The following defines the version and build number for your application.
6 | # A version number is three numbers separated by dots, like 1.2.43
7 | # followed by an optional build number separated by a +.
8 | # Both the version and the builder number may be overridden in flutter
9 | # build by specifying --build-name and --build-number, respectively.
10 | # In Android, build-name is used as versionName while build-number used as versionCode.
11 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
12 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
13 | # Read more about iOS versioning at
14 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
15 | version: 1.0.0+1
16 |
17 | environment:
18 | sdk: ">=2.6.0 <3.0.0"
19 |
20 | dependencies:
21 | flutter:
22 | sdk: flutter
23 |
24 | # The following adds the Cupertino Icons font to your application.
25 | # Use with the CupertinoIcons class for iOS style icons.
26 | cupertino_icons: ^0.1.2
27 | # provider插件 https://pub.flutter-io.cn/packages/provider
28 | provider: ^4.0.2
29 |
30 | dev_dependencies:
31 | flutter_test:
32 | sdk: flutter
33 |
34 |
35 | # For information on the generic Dart part of this file, see the
36 | # following page: https://dart.dev/tools/pub/pubspec
37 |
38 | # The following section is specific to Flutter.
39 | flutter:
40 |
41 | # The following line ensures that the Material Icons font is
42 | # included with your application, so that you can use the icons in
43 | # the material Icons class.
44 | uses-material-design: true
45 |
46 | # To add assets to your application, add an assets section, like this:
47 | # assets:
48 | # - bg.jpg
49 | # - images/a_dot_ham.jpeg
50 |
51 | # An image asset can refer to one or more resolution-specific "variants", see
52 | # https://flutter.dev/assets-and-images/#resolution-aware.
53 |
54 | # For details regarding adding assets from package dependencies, see
55 | # https://flutter.dev/assets-and-images/#from-packages
56 |
57 | # To add custom fonts to your application, add a fonts section here,
58 | # in this "flutter" section. Each entry in this list should have a
59 | # "family" key with the font family name, and a "fonts" key with a
60 | # list giving the asset and other descriptors for the font. For
61 | # example:
62 | # fonts:
63 | # - family: Schyler
64 | # fonts:
65 | # - asset: fonts/Schyler-Regular.ttf
66 | # - asset: fonts/Schyler-Italic.ttf
67 | # style: italic
68 | # - family: Trajan Pro
69 | # fonts:
70 | # - asset: fonts/TrajanPro.ttf
71 | # - asset: fonts/TrajanPro_Bold.ttf
72 | # weight: 700
73 | #
74 | # For details regarding fonts from package dependencies,
75 | # see https://flutter.dev/custom-fonts/#from-packages
76 |
--------------------------------------------------------------------------------
/lib/shop/shop_scroll_coordinator.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as math;
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/rendering.dart';
4 |
5 | import 'shop_scroll_controller.dart';
6 | import 'shop_scroll_position.dart';
7 |
8 | enum PageExpandState { NotExpand, Expanding, Expanded }
9 |
10 | /// 协调器
11 | ///
12 | /// 页面 Primary [CustomScrollView] 控制
13 | class ShopScrollCoordinator {
14 | final String pageLabel = 'page';
15 |
16 | ShopScrollController _pageScrollController;
17 | double Function() pinnedHeaderSliverHeightBuilder;
18 |
19 | ShopScrollPosition get _pageScrollPosition => _pageScrollController.position;
20 |
21 | ScrollDragController scrollDragController;
22 |
23 | /// 主页面滑动部件默认位置
24 | double _pageInitialOffset;
25 |
26 | /// 获取主页面滑动控制器
27 | ShopScrollController pageScrollController([double initialOffset = 0.0]) {
28 | assert(initialOffset != null, initialOffset >= 0.0);
29 | _pageInitialOffset = initialOffset;
30 | _pageScrollController = ShopScrollController(
31 | this,
32 | debugLabel: pageLabel,
33 | initialScrollOffset: initialOffset,
34 | );
35 | return _pageScrollController;
36 | }
37 |
38 | /// 创建并获取一个子滑动控制器
39 | ShopScrollController newChildScrollController([String debugLabel]) =>
40 | ShopScrollController(this, debugLabel: debugLabel);
41 |
42 | /// 子部件滑动数据协调
43 | ///
44 | /// [userScrollDirection] 用户滑动方向
45 | /// [position] 被滑动的子部件的位置信息
46 | void applyUserOffset(
47 | double delta, [
48 | ScrollDirection userScrollDirection,
49 | ShopScrollPosition position,
50 | ]) {
51 | if (userScrollDirection == ScrollDirection.reverse) {
52 | updateUserScrollDirection(_pageScrollPosition, userScrollDirection);
53 | final double innerDelta =
54 | _pageScrollPosition.applyClampedDragUpdate(delta);
55 | if (innerDelta != 0.0) {
56 | updateUserScrollDirection(position, userScrollDirection);
57 | position.applyFullDragUpdate(innerDelta);
58 | }
59 | } else {
60 | updateUserScrollDirection(position, userScrollDirection);
61 | final double outerDelta = position.applyClampedDragUpdate(delta);
62 | if (outerDelta != 0.0) {
63 | updateUserScrollDirection(_pageScrollPosition, userScrollDirection);
64 | _pageScrollPosition.applyFullDragUpdate(outerDelta);
65 | }
66 | }
67 | }
68 |
69 | bool applyContentDimensions(double minScrollExtent, double maxScrollExtent,
70 | ShopScrollPosition position) {
71 | if (pinnedHeaderSliverHeightBuilder != null) {
72 | maxScrollExtent = maxScrollExtent - pinnedHeaderSliverHeightBuilder();
73 | maxScrollExtent = math.max(0.0, maxScrollExtent);
74 | }
75 | return position.applyContentDimensions(
76 | minScrollExtent, maxScrollExtent, true);
77 | }
78 |
79 | /// 当默认位置不为0时,主部件已下拉距离超过默认位置,但超过的距离不大于该值时,
80 | /// 若手指离开屏幕,主部件头部会回弹至默认位置
81 | final double _scrollRedundancy = 80;
82 |
83 | /// 当前页面Header最大程度展开状态
84 | PageExpandState pageExpand = PageExpandState.NotExpand;
85 |
86 | /// 当手指离开屏幕
87 | void onPointerUp(PointerUpEvent event) {
88 | final double _pagePixels = _pageScrollPosition.pixels;
89 | if (0.0 < _pagePixels && _pagePixels < _pageInitialOffset) {
90 | if (pageExpand == PageExpandState.NotExpand &&
91 | _pageInitialOffset - _pagePixels > _scrollRedundancy) {
92 | _pageScrollPosition
93 | .animateTo(
94 | 0.0,
95 | duration: const Duration(milliseconds: 400),
96 | curve: Curves.ease,
97 | )
98 | .then((_) => pageExpand = PageExpandState.Expanded);
99 | } else {
100 | pageExpand = PageExpandState.Expanding;
101 | _pageScrollPosition
102 | .animateTo(
103 | _pageInitialOffset,
104 | duration: const Duration(milliseconds: 400),
105 | curve: Curves.ease,
106 | )
107 | .then((_) => pageExpand = PageExpandState.NotExpand);
108 | }
109 | }
110 | }
111 |
112 | /// 更新用户滑动方向
113 | void updateUserScrollDirection(
114 | ShopScrollPosition position, ScrollDirection value) {
115 | assert(position != null && value != null);
116 | position.didUpdateScrollDirection(value);
117 | }
118 |
119 | /// 以特定的速度开始一个物理驱动的模拟,该模拟确定 [pixels] 位置。
120 | ///
121 | /// 此方法遵从 [ScrollPhysics.createBallisticSimulation],通常在当前位置超出范围时
122 | /// 提供滑动模拟,而在当前位置超出范围但具有非零速度时提供摩擦模拟。
123 | ///
124 | /// 速度应以 逻辑像素/秒 为单位。
125 | void goBallistic(double velocity) =>
126 | _pageScrollPosition.goBallistic(velocity);
127 | }
128 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | async:
5 | dependency: transitive
6 | description:
7 | name: async
8 | url: "https://pub.flutter-io.cn"
9 | source: hosted
10 | version: "2.5.0-nullsafety.1"
11 | boolean_selector:
12 | dependency: transitive
13 | description:
14 | name: boolean_selector
15 | url: "https://pub.flutter-io.cn"
16 | source: hosted
17 | version: "2.1.0-nullsafety.1"
18 | characters:
19 | dependency: transitive
20 | description:
21 | name: characters
22 | url: "https://pub.flutter-io.cn"
23 | source: hosted
24 | version: "1.1.0-nullsafety.3"
25 | charcode:
26 | dependency: transitive
27 | description:
28 | name: charcode
29 | url: "https://pub.flutter-io.cn"
30 | source: hosted
31 | version: "1.2.0-nullsafety.1"
32 | clock:
33 | dependency: transitive
34 | description:
35 | name: clock
36 | url: "https://pub.flutter-io.cn"
37 | source: hosted
38 | version: "1.1.0-nullsafety.1"
39 | collection:
40 | dependency: transitive
41 | description:
42 | name: collection
43 | url: "https://pub.flutter-io.cn"
44 | source: hosted
45 | version: "1.15.0-nullsafety.3"
46 | cupertino_icons:
47 | dependency: "direct main"
48 | description:
49 | name: cupertino_icons
50 | url: "https://pub.flutter-io.cn"
51 | source: hosted
52 | version: "0.1.3"
53 | fake_async:
54 | dependency: transitive
55 | description:
56 | name: fake_async
57 | url: "https://pub.flutter-io.cn"
58 | source: hosted
59 | version: "1.2.0-nullsafety.1"
60 | flutter:
61 | dependency: "direct main"
62 | description: flutter
63 | source: sdk
64 | version: "0.0.0"
65 | flutter_test:
66 | dependency: "direct dev"
67 | description: flutter
68 | source: sdk
69 | version: "0.0.0"
70 | matcher:
71 | dependency: transitive
72 | description:
73 | name: matcher
74 | url: "https://pub.flutter-io.cn"
75 | source: hosted
76 | version: "0.12.10-nullsafety.1"
77 | meta:
78 | dependency: transitive
79 | description:
80 | name: meta
81 | url: "https://pub.flutter-io.cn"
82 | source: hosted
83 | version: "1.3.0-nullsafety.3"
84 | nested:
85 | dependency: transitive
86 | description:
87 | name: nested
88 | url: "https://pub.flutter-io.cn"
89 | source: hosted
90 | version: "0.0.4"
91 | path:
92 | dependency: transitive
93 | description:
94 | name: path
95 | url: "https://pub.flutter-io.cn"
96 | source: hosted
97 | version: "1.8.0-nullsafety.1"
98 | provider:
99 | dependency: "direct main"
100 | description:
101 | name: provider
102 | url: "https://pub.flutter-io.cn"
103 | source: hosted
104 | version: "4.0.2"
105 | sky_engine:
106 | dependency: transitive
107 | description: flutter
108 | source: sdk
109 | version: "0.0.99"
110 | source_span:
111 | dependency: transitive
112 | description:
113 | name: source_span
114 | url: "https://pub.flutter-io.cn"
115 | source: hosted
116 | version: "1.8.0-nullsafety.2"
117 | stack_trace:
118 | dependency: transitive
119 | description:
120 | name: stack_trace
121 | url: "https://pub.flutter-io.cn"
122 | source: hosted
123 | version: "1.10.0-nullsafety.1"
124 | stream_channel:
125 | dependency: transitive
126 | description:
127 | name: stream_channel
128 | url: "https://pub.flutter-io.cn"
129 | source: hosted
130 | version: "2.1.0-nullsafety.1"
131 | string_scanner:
132 | dependency: transitive
133 | description:
134 | name: string_scanner
135 | url: "https://pub.flutter-io.cn"
136 | source: hosted
137 | version: "1.1.0-nullsafety.1"
138 | term_glyph:
139 | dependency: transitive
140 | description:
141 | name: term_glyph
142 | url: "https://pub.flutter-io.cn"
143 | source: hosted
144 | version: "1.2.0-nullsafety.1"
145 | test_api:
146 | dependency: transitive
147 | description:
148 | name: test_api
149 | url: "https://pub.flutter-io.cn"
150 | source: hosted
151 | version: "0.2.19-nullsafety.2"
152 | typed_data:
153 | dependency: transitive
154 | description:
155 | name: typed_data
156 | url: "https://pub.flutter-io.cn"
157 | source: hosted
158 | version: "1.3.0-nullsafety.3"
159 | vector_math:
160 | dependency: transitive
161 | description:
162 | name: vector_math
163 | url: "https://pub.flutter-io.cn"
164 | source: hosted
165 | version: "2.1.0-nullsafety.3"
166 | sdks:
167 | dart: ">=2.10.0-110 <2.11.0"
168 | flutter: ">=1.12.1"
169 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 | import 'package:flutter/material.dart';
3 | import 'page1.dart';
4 | import 'page2.dart';
5 | import 'page3.dart';
6 | import 'shop/shop_scroll_coordinator.dart';
7 |
8 | import 'shop/shop_scroll_controller.dart';
9 |
10 | void main() => runApp(MyApp());
11 | MediaQueryData mediaQuery;
12 | double statusBarHeight;
13 | double screenHeight;
14 |
15 | class MyApp extends StatelessWidget {
16 | @override
17 | Widget build(BuildContext context) {
18 | return MaterialApp(
19 | debugShowCheckedModeBanner: false,
20 | title: 'ShopScroll',
21 | home: ShopPage(),
22 | theme: ThemeData(primarySwatch: Colors.blue),
23 | );
24 | }
25 | }
26 |
27 | class ShopPage extends StatefulWidget {
28 | ShopPage({Key key}) : super(key: key);
29 |
30 | @override
31 | _ShopPageState createState() => _ShopPageState();
32 | }
33 |
34 | class _ShopPageState extends State
35 | with SingleTickerProviderStateMixin {
36 | ///页面滑动协调器
37 | ShopScrollCoordinator _shopCoordinator;
38 | ShopScrollController _pageScrollController;
39 |
40 | TabController _tabController;
41 |
42 | final double _sliverAppBarInitHeight = 200;
43 | final double _tabBarHeight = 50;
44 | double _sliverAppBarMaxHeight;
45 |
46 | @override
47 | void initState() {
48 | super.initState();
49 | _shopCoordinator = ShopScrollCoordinator();
50 | _tabController = TabController(vsync: this, length: 3);
51 | }
52 |
53 | @override
54 | Widget build(BuildContext context) {
55 | mediaQuery ??= MediaQuery.of(context);
56 | screenHeight ??= mediaQuery.size.height;
57 | statusBarHeight ??= mediaQuery.padding.top;
58 |
59 | _sliverAppBarMaxHeight ??= screenHeight;
60 | _pageScrollController ??= _shopCoordinator
61 | .pageScrollController(_sliverAppBarMaxHeight - _sliverAppBarInitHeight);
62 |
63 | _shopCoordinator.pinnedHeaderSliverHeightBuilder ??= () {
64 | return statusBarHeight + kToolbarHeight + _tabBarHeight;
65 | };
66 | return Scaffold(
67 | body: Listener(
68 | onPointerUp: _shopCoordinator.onPointerUp,
69 | child: CustomScrollView(
70 | controller: _pageScrollController,
71 | physics: ClampingScrollPhysics(),
72 | slivers: [
73 | SliverAppBar(
74 | pinned: true,
75 | title: Text("店铺首页", style: TextStyle(color: Colors.white)),
76 | backgroundColor: Colors.blue,
77 | expandedHeight: _sliverAppBarMaxHeight,
78 | ),
79 | SliverPersistentHeader(
80 | pinned: false,
81 | floating: true,
82 | delegate: _SliverAppBarDelegate(
83 | maxHeight: 100,
84 | minHeight: 100,
85 | child: Center(child: Text("我是活动Header")),
86 | ),
87 | ),
88 | SliverPersistentHeader(
89 | pinned: true,
90 | floating: false,
91 | delegate: _SliverAppBarDelegate(
92 | maxHeight: _tabBarHeight,
93 | minHeight: _tabBarHeight,
94 | child: Container(
95 | color: Colors.white,
96 | child: TabBar(
97 | labelColor: Colors.black,
98 | controller: _tabController,
99 | tabs: [
100 | Tab(text: "商品"),
101 | Tab(text: "评价"),
102 | Tab(text: "商家"),
103 | ],
104 | ),
105 | ),
106 | ),
107 | ),
108 | SliverFillRemaining(
109 | child: TabBarView(
110 | controller: _tabController,
111 | children: [
112 | Page1(shopCoordinator: _shopCoordinator),
113 | Page2(shopCoordinator: _shopCoordinator),
114 | Page3(),
115 | ],
116 | ),
117 | )
118 | ],
119 | ),
120 | ),
121 | );
122 | }
123 |
124 | @override
125 | void dispose() {
126 | _tabController?.dispose();
127 | _pageScrollController?.dispose();
128 | super.dispose();
129 | }
130 | }
131 |
132 | class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
133 | _SliverAppBarDelegate({
134 | @required this.minHeight,
135 | @required this.maxHeight,
136 | @required this.child,
137 | });
138 |
139 | final double minHeight;
140 | final double maxHeight;
141 | final Widget child;
142 |
143 | @override
144 | double get minExtent => this.minHeight;
145 |
146 | @override
147 | double get maxExtent => max(maxHeight, minHeight);
148 |
149 | @override
150 | Widget build(
151 | BuildContext context, double shrinkOffset, bool overlapsContent) {
152 | return SizedBox.expand(child: child);
153 | }
154 |
155 | @override
156 | bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
157 | return maxHeight != oldDelegate.maxHeight ||
158 | minHeight != oldDelegate.minHeight ||
159 | child != oldDelegate.child;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/lib/shop/shop_scroll_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter/widgets.dart';
3 |
4 | import 'shop_scroll_coordinator.dart';
5 | import 'shop_scroll_position.dart';
6 |
7 | /// 滑动控制器。为可滚动小部件创建一个控制器。
8 | ///
9 | /// [initialScrollOffset] 和 [keepScrollOffset] 的值不能为 null。
10 | class ShopScrollController extends ScrollController {
11 | ShopScrollController(
12 | this.coordinator, {
13 | double initialScrollOffset = 0.0,
14 |
15 | /// 每次滚动完成时,请使用 [PageStorage] 保存当前滚动 [offset] ,如果重新创建了此
16 | /// 控制器的可滚动内容,则将其还原。
17 | ///
18 | /// 如果将此属性设置为false,则永远不会保存滚动偏移量,
19 | /// 并且始终使用 [initialScrollOffset] 来初始化滚动偏移量。如果为 true(默认值),
20 | /// 则第一次创建控制器的可滚动对象时将使用初始滚动偏移量,因为尚无要还原的滚动偏移量。
21 | /// 随后,将恢复保存的偏移,并且忽略[initialScrollOffset]。
22 | ///
23 | /// 也可以看看:
24 | /// * [PageStorageKey],当同一路径中出现多个滚动条时,应使用 [PageStorageKey]
25 | /// 来区分用于保存滚动偏移量的 [PageStorage] 位置。
26 | bool keepScrollOffset = true,
27 |
28 | /// [toString] 输出中使用的标签。帮助在调试输出中标识滚动控制器实例。
29 | String debugLabel,
30 | }) : assert(initialScrollOffset != null),
31 | assert(keepScrollOffset != null),
32 | _initialScrollOffset = initialScrollOffset,
33 | super(keepScrollOffset: keepScrollOffset, debugLabel: debugLabel);
34 |
35 | final ShopScrollCoordinator coordinator;
36 |
37 | /// 用于 [offset] 的初始值。
38 | /// 如果 [keepScrollOffset] 为 false 或尚未保存滚动偏移量,
39 | /// 则创建并附加到此控制器的新 [ShopScrollPosition] 对象的偏移量将初始化为该值。
40 | /// 默认为 0.0。
41 | @override
42 | double get initialScrollOffset => _initialScrollOffset;
43 | final double _initialScrollOffset;
44 |
45 | /// 当前附加的 [positions]。
46 | ///
47 | /// 不应直接突变。
48 | /// 可以使用 [attach] 和 [detach] 添加和删除 [ShopScrollPosition] 对象。
49 | @protected
50 | @override
51 | Iterable get positions => _positions;
52 | final List _positions = [];
53 |
54 | /// 是否有任何 [ShopScrollPosition] 对象已使用 [attach] 方法
55 | /// 将自身附加到 [ScrollController]。
56 | ///
57 | /// 如果为 false,则不得调用与 [ShopScrollPosition] 交互的成员,
58 | /// 例如 [position],[offset],[animateTo] 和 [jumpTo]。
59 | @override
60 | bool get hasClients => _positions.isNotEmpty;
61 |
62 | /// 返回附加的[ScrollPosition],可以从中获取[ScrollView]的实际滚动偏移量。
63 | ///
64 | /// 仅在仅连接一个 [position] 时调用此选项才有效。
65 | @override
66 | ShopScrollPosition get position {
67 | assert(_positions.isNotEmpty,
68 | 'ScrollController not attached to any scroll views.');
69 | assert(_positions.length == 1,
70 | 'ScrollController attached to multiple scroll views.');
71 | return _positions.single;
72 | }
73 |
74 | /// 可滚动小部件的当前滚动偏移量。要求控制器仅控制一个可滚动小部件。
75 | @override
76 | double get offset => position.pixels;
77 |
78 | /// 从当前位置到给定值的位置动画。
79 | /// 任何活动的动画都将被取消。 如果用户当前正在滚动,则该操作将被取消。
80 | /// 返回的 [Future] 将在动画结束时完成,无论它是否成功完成或是否被过早中断。
81 | ///
82 | /// 每当用户尝试手动滚动或启动其他活动,或者动画到达视口边缘并尝试过度滚动时,
83 | /// 动画都会中断。如果 [ShopScrollPosition] 不会过度滚动,而是允许滚动超出范围,
84 | /// 那么超出范围不会中断动画。
85 | ///
86 | /// 动画对视口或内容尺寸的更改无动于衷。
87 | ///
88 | /// 一旦动画完成,如果滚动位置的值不稳定,则滚动位置将尝试开始弹道活动。
89 | /// (例如,如果滚动超出范围,并且在这种情况下滚动位置通常会弹回)
90 | ///
91 | /// 持续时间不能为零。要在没有动画的情况下跳至特定值,请使用 [jumpTo]。
92 | @override
93 | Future animateTo(
94 | double offset, {
95 | @required Duration duration,
96 | @required Curve curve,
97 | }) {
98 | assert(
99 | _positions.isNotEmpty,
100 | 'ScrollController not attached to any scroll views.',
101 | );
102 | final List> animations = List>(_positions.length);
103 | for (int i = 0; i < _positions.length; i += 1)
104 | animations[i] = _positions[i].animateTo(
105 | offset,
106 | duration: duration,
107 | curve: curve,
108 | );
109 | return Future.wait(animations).then((List _) => null);
110 | }
111 |
112 | /// 将滚动位置从其当前值跳转到给定值,而不进行动画处理,也无需检查新值是否在范围内。
113 | /// 任何活动的动画都将被取消。 如果用户当前正在滚动,则该操作将被取消。
114 | ///
115 | /// 如果此方法更改了滚动位置,则将分派开始/更新/结束滚动通知的序列。
116 | /// 此方法不能生成过滚动通知。跳跃之后,如果数值超出范围,则立即开始弹道活动。
117 | @override
118 | void jumpTo(double value) {
119 | assert(
120 | _positions.isNotEmpty,
121 | 'ScrollController not attached to any scroll views.',
122 | );
123 | for (final ScrollPosition position in List.from(_positions))
124 | position.jumpTo(value);
125 | }
126 |
127 | /// 在此控制器上注册给定位置。
128 | /// 此函数返回后,此控制器上的 [animateTo] 和 [jumpTo] 方法将操纵给定位置。
129 | @override
130 | void attach(covariant ShopScrollPosition position) {
131 | assert(!_positions.contains(position));
132 | _positions.add(position);
133 | position.addListener(notifyListeners);
134 | }
135 |
136 | /// 用此控制器注销给定位置。
137 | /// 此函数返回后,此控制器上的 [animateTo] 和 [jumpTo] 方法将不会操纵给定位置。
138 | @override
139 | void detach(ScrollPosition position) {
140 | assert(_positions.contains(position));
141 | position.removeListener(notifyListeners);
142 | _positions.remove(position);
143 | }
144 |
145 | @override
146 | void dispose() {
147 | for (final ScrollPosition position in _positions)
148 | position.removeListener(notifyListeners);
149 | super.dispose();
150 | }
151 |
152 | /// 创建一个 [ShopScrollPosition] 供 [Scrollable] 小部件使用。
153 | ///
154 | /// 子类可以重写此功能,以自定义其控制的可滚动小部件使用的 [ShopScrollPosition]。
155 | /// 例如,[PageController] 重写此函数以返回面向页面的滚动位置子类,
156 | /// 该子类在可滚动窗口小部件调整大小时保持同一页面可见。
157 | ///
158 | /// 默认情况下,返回 [ScrollPositionWithSingleContext]。
159 | /// 参数通常传递给正在创建的 [ScrollPosition]:
160 | ///
161 | /// * [physics]:[ScrollPhysics] 的一个实例,它确定 [ScrollPosition] 对用户交互的
162 | /// 反应方式,释放或甩动时如何模拟滚动等。该值不会为null。它通常来自 [ScrollView]
163 | /// 或其他创建 [Scrollable]的小部件,
164 | /// 或者(如果未提供)来自环境的 [ScrollConfiguration]。
165 | /// * [context]:一个 [ScrollContext],用于与拥有 [ScrollPosition] 的对象进行通信
166 | /// (通常是 [Scrollable] 本身)。
167 | /// * [oldPosition]:如果这不是第一次为此 [Scrollable] 创建 [ScrollPosition],则
168 | /// 它将是前一个实例。当环境已更改并且 [Scrollable] 需要重新创建 [ScrollPosition]
169 | /// 对象时,将使用此方法。 第一次创建 [ScrollPosition] 时为 null。
170 | @override
171 | ShopScrollPosition createScrollPosition(
172 | ScrollPhysics physics,
173 | ScrollContext context,
174 | ScrollPosition oldPosition,
175 | ) {
176 | return ShopScrollPosition(
177 | coordinator: coordinator,
178 | physics: physics,
179 | context: context,
180 | initialPixels: initialScrollOffset,
181 | keepScrollOffset: keepScrollOffset,
182 | oldPosition: oldPosition,
183 | debugLabel: debugLabel,
184 | );
185 | }
186 |
187 | @override
188 | String toString() {
189 | final List description = [];
190 | debugFillDescription(description);
191 | return '${describeIdentity(this)}(${description.join(", ")})';
192 | }
193 |
194 | /// 在给定的描述中添加其他信息,以供 [toString] 使用。
195 | /// 此方法使子类更易于协调以提供高质量的 [toString] 实现。[ScrollController] 基类上
196 | /// 的 [toString] 实现调用 [debugFillDescription] 来从子类中收集有用的信息,以合并
197 | /// 到其返回值中。如果您重写了此方法,请确保通过调用
198 | /// `super.debugFillDescription)description)` 来启动方法。
199 | @override
200 | @mustCallSuper
201 | void debugFillDescription(List description) {
202 | super.debugFillDescription(description);
203 | if (debugLabel != null) {
204 | description.add(debugLabel);
205 | }
206 | if (initialScrollOffset != 0.0)
207 | description.add(
208 | 'initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
209 | if (_positions.isEmpty) {
210 | description.add('no clients');
211 | } else if (_positions.length == 1) {
212 | // 实际上不列出客户端本身,因为它的 toString 可能引用了我们。
213 | description.add('one client, offset ${offset?.toStringAsFixed(1)}');
214 | } else {
215 | description.add('${_positions.length} clients');
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/lib/shop/shop_scroll_position.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as math;
2 |
3 | import 'package:flutter/gestures.dart';
4 | import 'package:flutter/physics.dart';
5 | import 'package:flutter/rendering.dart';
6 | import 'package:flutter/widgets.dart';
7 |
8 | import 'shop_scroll_coordinator.dart';
9 |
10 | /// 滑动位置信息
11 | class ShopScrollPosition extends ScrollPosition
12 | implements ScrollActivityDelegate {
13 | ShopScrollPosition({
14 | @required ScrollPhysics physics,
15 | @required ScrollContext context,
16 | double initialPixels = 0.0,
17 | bool keepScrollOffset = true,
18 | ScrollPosition oldPosition,
19 | String debugLabel,
20 | @required this.coordinator,
21 | }) : super(
22 | physics: physics,
23 | context: context,
24 | keepScrollOffset: keepScrollOffset,
25 | oldPosition: oldPosition,
26 | debugLabel: debugLabel,
27 | ) {
28 | // 如果oldPosition不为null,则父级将首先调用Absorb(),它可以设置_pixels和_activity.
29 | if (pixels == null && initialPixels != null) {
30 | correctPixels(initialPixels);
31 | }
32 | if (activity == null) {
33 | goIdle();
34 | }
35 | assert(activity != null);
36 | }
37 |
38 | final ShopScrollCoordinator coordinator; // 协调器
39 | ScrollDragController _currentDrag;
40 | double _heldPreviousVelocity = 0.0;
41 |
42 | @override
43 | AxisDirection get axisDirection => context.axisDirection;
44 |
45 | @override
46 | double setPixels(double newPixels) {
47 | assert(activity.isScrolling);
48 | return super.setPixels(newPixels);
49 | }
50 |
51 | @override
52 | void absorb(ScrollPosition other) {
53 | super.absorb(other);
54 | if (other is! ShopScrollPosition) {
55 | goIdle();
56 | return;
57 | }
58 | activity.updateDelegate(this);
59 | final ShopScrollPosition typedOther = other as ShopScrollPosition;
60 | _userScrollDirection = typedOther._userScrollDirection;
61 | assert(_currentDrag == null);
62 | if (typedOther._currentDrag != null) {
63 | _currentDrag = typedOther._currentDrag;
64 | _currentDrag.updateDelegate(this);
65 | typedOther._currentDrag = null;
66 | }
67 | }
68 |
69 | @override
70 | void applyNewDimensions() {
71 | super.applyNewDimensions();
72 | context.setCanDrag(physics.shouldAcceptUserOffset(this));
73 | }
74 |
75 | /// 返回未使用的增量。
76 | ///
77 | /// 正增量表示下降(在上方显示内容),负增量向上(在下方显示内容)。
78 | double applyClampedDragUpdate(double delta) {
79 | assert(delta != 0.0);
80 | // 如果我们要朝向 maxScrollExtent(负滚动偏移),那么我们在 minScrollExtent 方向上
81 | // 可以达到的最大距离是负无穷大。例如,如果我们已经过度滚动,则滚动以减少过度滚动不应
82 | // 禁止过度滚动。如果我们要朝 minScrollExtent(正滚动偏移量)方向移动,那么我们在
83 | // minScrollExtent 方向上可以达到的最大距离是我们现在所处的位置。
84 | // 换句话说,我们不能通过 applyClampedDragUpdate 进入过滚动状态。
85 | // 尽管如此,可能通过多种方式进入了过度滚动的情况。一种是物理是否允许通过
86 | // applyFullDragUpdate(请参见下文)。
87 | // 可能会发生过度滚动的情况,例如,使用滚动控制器人工设置了滚动位置。
88 | final double min =
89 | delta < 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
90 | // max 的逻辑是等效的,但反向。
91 | final double max =
92 | delta > 0.0 ? double.infinity : math.max(maxScrollExtent, pixels);
93 | final double oldPixels = pixels;
94 | final double newPixels = (pixels - delta).clamp(min, max) as double;
95 | final double clampedDelta = newPixels - pixels;
96 | if (clampedDelta == 0.0) {
97 | return delta;
98 | }
99 | final double overScroll = physics.applyBoundaryConditions(this, newPixels);
100 | final double actualNewPixels = newPixels - overScroll;
101 | final double offset = actualNewPixels - oldPixels;
102 | if (offset != 0.0) {
103 | forcePixels(actualNewPixels);
104 | didUpdateScrollPositionBy(offset);
105 | }
106 | return delta + offset;
107 | }
108 |
109 | // 返回过度滚动。
110 | double applyFullDragUpdate(double delta) {
111 | assert(delta != 0.0);
112 | final double oldPixels = pixels;
113 | // Apply friction: 施加摩擦:
114 | final double newPixels =
115 | pixels - physics.applyPhysicsToUserOffset(this, delta);
116 | if (oldPixels == newPixels) {
117 | // 增量一定很小,我们在添加浮点数时将其删除了
118 | return 0.0;
119 | }
120 | // Check for overScroll: 检查过度滚动:
121 | final double overScroll = physics.applyBoundaryConditions(this, newPixels);
122 | final double actualNewPixels = newPixels - overScroll;
123 | if (actualNewPixels != oldPixels) {
124 | forcePixels(actualNewPixels);
125 | didUpdateScrollPositionBy(actualNewPixels - oldPixels);
126 | }
127 | return overScroll;
128 | }
129 |
130 | /// 当手指滑动时,该方法会获取到滑动距离。
131 | ///
132 | /// [delta] 滑动距离,正增量表示下滑,负增量向上滑。
133 | ///
134 | /// 我们需要把子部件的滑动数据交给协调器处理,主部件无干扰。
135 | @override
136 | void applyUserOffset(double delta) {
137 | final ScrollDirection userScrollDirection =
138 | delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse;
139 | if (debugLabel != coordinator.pageLabel) {
140 | return coordinator.applyUserOffset(delta, userScrollDirection, this);
141 | }
142 | updateUserScrollDirection(userScrollDirection);
143 | setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
144 | }
145 |
146 | @override
147 | void beginActivity(ScrollActivity newActivity) {
148 | _heldPreviousVelocity = 0.0;
149 | if (newActivity == null) {
150 | return;
151 | }
152 | assert(newActivity.delegate == this);
153 | super.beginActivity(newActivity);
154 | _currentDrag?.dispose();
155 | _currentDrag = null;
156 | if (!activity.isScrolling) {
157 | updateUserScrollDirection(ScrollDirection.idle);
158 | }
159 | }
160 |
161 | /// 将用户滚动方向设置为给定值。
162 | /// 如果更改了该值,则将分派 [User ScrollNotification]。
163 | @protected
164 | @visibleForTesting
165 | void updateUserScrollDirection(ScrollDirection value) {
166 | assert(value != null);
167 | if (userScrollDirection == value) {
168 | return;
169 | }
170 | _userScrollDirection = value;
171 | didUpdateScrollDirection(value);
172 | }
173 |
174 | @override
175 | ScrollHoldController hold(VoidCallback holdCancelCallback) {
176 | final double previousVelocity = activity.velocity;
177 | final HoldScrollActivity holdActivity = HoldScrollActivity(
178 | delegate: this,
179 | onHoldCanceled: holdCancelCallback,
180 | );
181 | beginActivity(holdActivity);
182 | _heldPreviousVelocity = previousVelocity;
183 | return holdActivity;
184 | }
185 |
186 | @override
187 | Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
188 | final ScrollDragController drag = ScrollDragController(
189 | delegate: this,
190 | details: details,
191 | onDragCanceled: dragCancelCallback,
192 | carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
193 | motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
194 | );
195 | beginActivity(DragScrollActivity(this, drag));
196 | assert(_currentDrag == null);
197 | _currentDrag = drag;
198 | return drag;
199 | }
200 |
201 | @override
202 | void goIdle() {
203 | beginActivity(IdleScrollActivity(this));
204 | }
205 |
206 | /// 以特定的速度开始一个物理驱动的模拟,该模拟确定 [pixels] 位置。
207 | /// 此方法遵从 [ScrollPhysics.createBallisticSimulation],该方法通常在当前位置超出
208 | /// 范围时提供滑动模拟,而在当前位置超出范围但具有非零速度时提供摩擦模拟。
209 | /// 速度应以逻辑像素/秒为单位。
210 | @override
211 | void goBallistic(double velocity, [bool fromCoordinator = false]) {
212 | if (debugLabel != coordinator.pageLabel) {
213 | if (velocity > 0.0) {
214 | coordinator.goBallistic(velocity);
215 | }
216 | } else {
217 | if (fromCoordinator && velocity <= 0.0) {
218 | return;
219 | }
220 | if (coordinator.pageExpand == PageExpandState.Expanding) {
221 | return;
222 | }
223 | }
224 | assert(pixels != null);
225 | final Simulation simulation =
226 | physics.createBallisticSimulation(this, velocity);
227 | if (simulation != null) {
228 | beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
229 | } else {
230 | goIdle();
231 | }
232 | }
233 |
234 | @override
235 | bool applyContentDimensions(double minScrollExtent, double maxScrollExtent,
236 | [bool fromCoordinator = false]) {
237 | if (debugLabel == coordinator.pageLabel && !fromCoordinator)
238 | return coordinator.applyContentDimensions(
239 | minScrollExtent, maxScrollExtent, this);
240 | return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
241 | }
242 |
243 | @override
244 | ScrollDirection get userScrollDirection => _userScrollDirection;
245 | ScrollDirection _userScrollDirection = ScrollDirection.idle;
246 |
247 | @override
248 | Future animateTo(
249 | double to, {
250 | @required Duration duration,
251 | @required Curve curve,
252 | }) {
253 | if (nearEqual(to, pixels, physics.tolerance.distance)) {
254 | // 跳过动画,直接移到我们已经靠近的位置。
255 | jumpTo(to);
256 | return Future.value();
257 | }
258 |
259 | final DrivenScrollActivity activity = DrivenScrollActivity(
260 | this,
261 | from: pixels,
262 | to: to,
263 | duration: duration,
264 | curve: curve,
265 | vsync: context.vsync,
266 | );
267 | beginActivity(activity);
268 | return activity.done;
269 | }
270 |
271 | @override
272 | void jumpTo(double value) {
273 | goIdle();
274 | if (pixels != value) {
275 | final double oldPixels = pixels;
276 | forcePixels(value);
277 | notifyListeners();
278 | didStartScroll();
279 | didUpdateScrollPositionBy(pixels - oldPixels);
280 | didEndScroll();
281 | }
282 | goBallistic(0.0);
283 | }
284 |
285 | @Deprecated('This will lead to bugs.')
286 | @override
287 | void jumpToWithoutSettling(double value) {
288 | goIdle();
289 | if (pixels != value) {
290 | final double oldPixels = pixels;
291 | forcePixels(value);
292 | notifyListeners();
293 | didStartScroll();
294 | didUpdateScrollPositionBy(pixels - oldPixels);
295 | didEndScroll();
296 | }
297 | }
298 |
299 | @override
300 | void dispose() {
301 | _currentDrag?.dispose();
302 | _currentDrag = null;
303 | super.dispose();
304 | }
305 |
306 | @override
307 | void debugFillDescription(List description) {
308 | super.debugFillDescription(description);
309 | description.add('${context.runtimeType}');
310 | description.add('$physics');
311 | description.add('$activity');
312 | description.add('$userScrollDirection');
313 | }
314 | }
315 |
--------------------------------------------------------------------------------