extents_page_view.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import 'package:flutter/gestures.dart';
  2. import 'package:flutter/rendering.dart';
  3. import 'package:flutter/widgets.dart' hide PageView;
  4. /// This is copy-pasted from the Flutter framework with a support added for building
  5. /// pages off screen using [Viewport.cacheExtents] and a [LayoutBuilder]
  6. ///
  7. /// Based on commit 3932ffb1cd5dfa0c3891c60977ee4f9cd70ade66 on channel dev
  8. // Having this global (mutable) page controller is a bit of a hack. We need it
  9. // to plumb in the factory for _PagePosition, but it will end up accumulating
  10. // a large list of scroll positions. As long as you don't try to actually
  11. // control the scroll positions, everything should be fine.
  12. final PageController _defaultPageController = PageController();
  13. const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
  14. /// A scrollable list that works page by page.
  15. ///
  16. /// Each child of a page view is forced to be the same size as the viewport.
  17. ///
  18. /// You can use a [PageController] to control which page is visible in the view.
  19. /// In addition to being able to control the pixel offset of the content inside
  20. /// the [PageView], a [PageController] also lets you control the offset in terms
  21. /// of pages, which are increments of the viewport size.
  22. ///
  23. /// The [PageController] can also be used to control the
  24. /// [PageController.initialPage], which determines which page is shown when the
  25. /// [PageView] is first constructed, and the [PageController.viewportFraction],
  26. /// which determines the size of the pages as a fraction of the viewport size.
  27. ///
  28. /// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
  29. ///
  30. /// See also:
  31. ///
  32. /// * [PageController], which controls which page is visible in the view.
  33. /// * [SingleChildScrollView], when you need to make a single child scrollable.
  34. /// * [ListView], for a scrollable list of boxes.
  35. /// * [GridView], for a scrollable grid of boxes.
  36. /// * [ScrollNotification] and [NotificationListener], which can be used to watch
  37. /// the scroll position without using a [ScrollController].
  38. class ExtentsPageView extends StatefulWidget {
  39. /// Creates a scrollable list that works page by page from an explicit [List]
  40. /// of widgets.
  41. ///
  42. /// This constructor is appropriate for page views with a small number of
  43. /// children because constructing the [List] requires doing work for every
  44. /// child that could possibly be displayed in the page view, instead of just
  45. /// those children that are actually visible.
  46. ExtentsPageView({
  47. Key? key,
  48. this.scrollDirection = Axis.horizontal,
  49. this.reverse = false,
  50. PageController? controller,
  51. this.physics,
  52. this.pageSnapping = true,
  53. this.onPageChanged,
  54. List<Widget> children = const <Widget>[],
  55. this.dragStartBehavior = DragStartBehavior.start,
  56. this.openDrawer,
  57. }) : controller = controller ?? _defaultPageController,
  58. childrenDelegate = SliverChildListDelegate(children),
  59. extents = children.length,
  60. super(key: key);
  61. /// Creates a scrollable list that works page by page using widgets that are
  62. /// created on demand.
  63. ///
  64. /// This constructor is appropriate for page views with a large (or infinite)
  65. /// number of children because the builder is called only for those children
  66. /// that are actually visible.
  67. ///
  68. /// Providing a non-null [itemCount] lets the [PageView] compute the maximum
  69. /// scroll extent.
  70. ///
  71. /// [itemBuilder] will be called only with indices greater than or equal to
  72. /// zero and less than [itemCount].
  73. ///
  74. /// [PageView.builder] by default does not support child reordering. If
  75. /// you are planning to change child order at a later time, consider using
  76. /// [PageView] or [PageView.custom].
  77. ExtentsPageView.builder({
  78. Key? key,
  79. this.scrollDirection = Axis.horizontal,
  80. this.reverse = false,
  81. PageController? controller,
  82. this.physics,
  83. this.pageSnapping = true,
  84. this.onPageChanged,
  85. required IndexedWidgetBuilder itemBuilder,
  86. int? itemCount,
  87. this.dragStartBehavior = DragStartBehavior.start,
  88. this.openDrawer,
  89. }) : controller = controller ?? _defaultPageController,
  90. childrenDelegate =
  91. SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
  92. extents = 0,
  93. super(key: key);
  94. ExtentsPageView.extents({
  95. Key? key,
  96. this.extents = 1,
  97. this.scrollDirection = Axis.horizontal,
  98. this.reverse = false,
  99. PageController? controller,
  100. this.physics,
  101. this.pageSnapping = true,
  102. this.onPageChanged,
  103. required IndexedWidgetBuilder itemBuilder,
  104. int? itemCount,
  105. this.dragStartBehavior = DragStartBehavior.start,
  106. this.openDrawer,
  107. }) : controller = controller ?? _defaultPageController,
  108. childrenDelegate = SliverChildBuilderDelegate(
  109. itemBuilder,
  110. childCount: itemCount,
  111. addAutomaticKeepAlives: false,
  112. addRepaintBoundaries: false,
  113. ),
  114. super(key: key);
  115. /// Creates a scrollable list that works page by page with a custom child
  116. /// model.
  117. ///
  118. /// {@tool sample}
  119. ///
  120. /// This [PageView] uses a custom [SliverChildBuilderDelegate] to support child
  121. /// reordering.
  122. ///
  123. /// ```dart
  124. /// class MyPageView extends StatefulWidget {
  125. /// @override
  126. /// _MyPageView> createState() => _MyPageViewState();
  127. /// }
  128. ///
  129. /// class _MyPageViewState extends State<MyPageView> {
  130. /// List<String> items = <String>['1', '2', '3', '4', '5'];
  131. ///
  132. /// void _reverse() {
  133. /// setState(() {
  134. /// items = items.reversed.toList();
  135. /// });
  136. /// }
  137. ///
  138. /// @override
  139. /// Widget build(BuildContext context) {
  140. /// return Scaffold(
  141. /// body: SafeArea(
  142. /// child: PageView.custom(
  143. /// childrenDelegate: SliverChildBuilderDelegate(
  144. /// (BuildContext context, int index) {
  145. /// return KeepAlive(
  146. /// data: items[index],
  147. /// key: ValueKey<String>(items[index]),
  148. /// );
  149. /// },
  150. /// childCount: items.length,
  151. /// findChildIndexCallback: (Key key) {
  152. /// final ValueKey valueKey = key;
  153. /// final String data = valueKey.value;
  154. /// return items.indexOf(data);
  155. /// }
  156. /// ),
  157. /// ),
  158. /// ),
  159. /// bottomNavigationBar: BottomAppBar(
  160. /// child: Row(
  161. /// mainAxisAlignment: MainAxisAlignment.center,
  162. /// children: <Widget>[
  163. /// FlatButton(
  164. /// onPressed: () => _reverse(),
  165. /// child: Text('Reverse items'),
  166. /// ),
  167. /// ],
  168. /// ),
  169. /// ),
  170. /// );
  171. /// }
  172. /// }
  173. ///
  174. /// class KeepAlive extends StatefulWidget {
  175. /// const KeepAlive({Key key, this.data}) : super(key: key);
  176. ///
  177. /// final String data;
  178. ///
  179. /// @override
  180. /// _KeepAlive> createState() => _KeepAliveState();
  181. /// }
  182. ///
  183. /// class _KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin{
  184. /// @override
  185. /// bool get wantKeepAlive => true;
  186. ///
  187. /// @override
  188. /// Widget build(BuildContext context) {
  189. /// super.build(context);
  190. /// return Text(widget.data);
  191. /// }
  192. /// }
  193. /// ```
  194. /// {@end-tool}
  195. ExtentsPageView.custom({
  196. Key? key,
  197. this.scrollDirection = Axis.horizontal,
  198. this.reverse = false,
  199. PageController? controller,
  200. this.physics,
  201. this.pageSnapping = true,
  202. this.onPageChanged,
  203. required this.childrenDelegate,
  204. this.dragStartBehavior = DragStartBehavior.start,
  205. this.openDrawer,
  206. }) : extents = 0,
  207. controller = controller ?? _defaultPageController,
  208. super(key: key);
  209. /// The number of pages to build off screen.
  210. ///
  211. /// For example, a value of `1` builds one page ahead and one page behind,
  212. /// for a total of three built pages.
  213. ///
  214. /// This is especially useful for making sure heavyweight widgets have a chance
  215. /// to load off-screen before the user pulls it into the viewport.
  216. final int extents;
  217. /// The axis along which the page view scrolls.
  218. ///
  219. /// Defaults to [Axis.horizontal].
  220. final Axis scrollDirection;
  221. /// Whether the page view scrolls in the reading direction.
  222. ///
  223. /// For example, if the reading direction is left-to-right and
  224. /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
  225. /// left to right when [reverse] is false and from right to left when
  226. /// [reverse] is true.
  227. ///
  228. /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
  229. /// scrolls from top to bottom when [reverse] is false and from bottom to top
  230. /// when [reverse] is true.
  231. ///
  232. /// Defaults to false.
  233. final bool reverse;
  234. /// An object that can be used to control the position to which this page
  235. /// view is scrolled.
  236. final PageController controller;
  237. /// How the page view should respond to user input.
  238. ///
  239. /// For example, determines how the page view continues to animate after the
  240. /// user stops dragging the page view.
  241. ///
  242. /// The physics are modified to snap to page boundaries using
  243. /// [PageScrollPhysics] prior to being used.
  244. ///
  245. /// Defaults to matching platform conventions.
  246. final ScrollPhysics? physics;
  247. /// Set to false to disable page snapping, useful for custom scroll behavior.
  248. final bool pageSnapping;
  249. /// Called whenever the page in the center of the viewport changes.
  250. final ValueChanged<int>? onPageChanged;
  251. /// A delegate that provides the children for the [PageView].
  252. ///
  253. /// The [PageView.custom] constructor lets you specify this delegate
  254. /// explicitly. The [PageView] and [PageView.builder] constructors create a
  255. /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
  256. /// respectively.
  257. final SliverChildDelegate childrenDelegate;
  258. /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  259. final DragStartBehavior dragStartBehavior;
  260. final Function? openDrawer; //nullable
  261. @override
  262. State<ExtentsPageView> createState() => _PageViewState();
  263. }
  264. class _PageViewState extends State<ExtentsPageView> {
  265. int _lastReportedPage = 0;
  266. @override
  267. void initState() {
  268. super.initState();
  269. _lastReportedPage = widget.controller.initialPage;
  270. widget.openDrawer != null
  271. ? widget.controller.addListener(() {
  272. if (widget.controller.offset < -45) {
  273. widget.openDrawer!();
  274. }
  275. })
  276. : null;
  277. }
  278. @override
  279. void dispose() {
  280. widget.controller.dispose();
  281. super.dispose();
  282. }
  283. AxisDirection? _getDirection(BuildContext context) {
  284. switch (widget.scrollDirection) {
  285. case Axis.horizontal:
  286. assert(debugCheckHasDirectionality(context));
  287. final TextDirection textDirection = Directionality.of(context);
  288. final AxisDirection axisDirection =
  289. textDirectionToAxisDirection(textDirection);
  290. return widget.reverse
  291. ? flipAxisDirection(axisDirection)
  292. : axisDirection;
  293. case Axis.vertical:
  294. return widget.reverse ? AxisDirection.up : AxisDirection.down;
  295. }
  296. }
  297. @override
  298. Widget build(BuildContext context) {
  299. final AxisDirection axisDirection = _getDirection(context)!;
  300. final ScrollPhysics? physics = widget.pageSnapping
  301. ? _kPagePhysics.applyTo(widget.physics)
  302. : widget.physics;
  303. return NotificationListener<ScrollNotification>(
  304. onNotification: (ScrollNotification notification) {
  305. if (notification.depth == 0 &&
  306. widget.onPageChanged != null &&
  307. notification is ScrollUpdateNotification) {
  308. final PageMetrics metrics = notification.metrics as PageMetrics;
  309. final int currentPage = metrics.page!.round();
  310. if (currentPage != _lastReportedPage) {
  311. _lastReportedPage = currentPage;
  312. widget.onPageChanged!(currentPage);
  313. }
  314. }
  315. return false;
  316. },
  317. child: Scrollable(
  318. dragStartBehavior: widget.dragStartBehavior,
  319. axisDirection: axisDirection,
  320. controller: widget.controller,
  321. physics: physics,
  322. viewportBuilder: (BuildContext context, ViewportOffset position) {
  323. return LayoutBuilder(
  324. builder: (context, constraints) {
  325. assert(constraints.hasBoundedHeight);
  326. assert(constraints.hasBoundedWidth);
  327. double cacheExtent;
  328. switch (widget.scrollDirection) {
  329. case Axis.vertical:
  330. cacheExtent = constraints.maxHeight * widget.extents;
  331. break;
  332. case Axis.horizontal:
  333. default:
  334. cacheExtent = constraints.maxWidth * widget.extents;
  335. break;
  336. }
  337. return Viewport(
  338. cacheExtent: cacheExtent,
  339. axisDirection: axisDirection,
  340. offset: position,
  341. slivers: <Widget>[
  342. SliverFillViewport(
  343. viewportFraction: widget.controller.viewportFraction,
  344. delegate: widget.childrenDelegate,
  345. ),
  346. ],
  347. );
  348. },
  349. );
  350. },
  351. ),
  352. );
  353. }
  354. @override
  355. void debugFillProperties(DiagnosticPropertiesBuilder description) {
  356. super.debugFillProperties(description);
  357. description
  358. .add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
  359. description.add(
  360. FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'),
  361. );
  362. description.add(
  363. DiagnosticsProperty<PageController>(
  364. 'controller',
  365. widget.controller,
  366. showName: false,
  367. ),
  368. );
  369. description.add(
  370. DiagnosticsProperty<ScrollPhysics>(
  371. 'physics',
  372. widget.physics,
  373. showName: false,
  374. ),
  375. );
  376. description.add(
  377. FlagProperty(
  378. 'pageSnapping',
  379. value: widget.pageSnapping,
  380. ifFalse: 'snapping disabled',
  381. ),
  382. );
  383. }
  384. }