diff --git a/lib/ui/detail_page.dart b/lib/ui/detail_page.dart index 9bd230704..ab8d1f037 100644 --- a/lib/ui/detail_page.dart +++ b/lib/ui/detail_page.dart @@ -2,28 +2,22 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:myapp/core/lru_map.dart'; -import 'package:myapp/db/db_helper.dart'; import 'package:myapp/models/photo.dart'; -import 'package:myapp/photo_loader.dart'; +import 'package:myapp/ui/extents_page_view.dart'; import 'package:myapp/ui/zoomable_image.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:provider/provider.dart'; import 'package:myapp/utils/share_util.dart'; class DetailPage extends StatefulWidget { final List photos; final int selectedIndex; - final Function(Photo) onPhotoDeleted; - DetailPage(this.photos, this.selectedIndex, {this.onPhotoDeleted, key}) - : super(key: key); + DetailPage(this.photos, this.selectedIndex, {key}) : super(key: key); @override _DetailPageState createState() => _DetailPageState(); } class _DetailPageState extends State { - PhotoLoader get photoLoader => Provider.of(context); bool _shouldDisableScroll = false; List _photos; int _selectedIndex = 0; @@ -56,9 +50,9 @@ class _DetailPageState extends State { ); } - PageView _buildPageView() { + Widget _buildPageView() { _pageController = PageController(initialPage: _selectedIndex); - return PageView.builder( + return ExtentsPageView.extents( itemBuilder: (context, index) { final photo = _photos[index]; if (_cachedImages.get(photo.generatedId) != null) { @@ -75,6 +69,7 @@ class _DetailPageState extends State { _cachedImages.put(photo.generatedId, image); return image; }, + extents: 1, onPageChanged: (int index) { Logger().i("onPageChanged to " + index.toString()); _selectedIndex = index; @@ -90,12 +85,6 @@ class _DetailPageState extends State { AppBar _buildAppBar() { return AppBar( actions: [ - IconButton( - icon: Icon(Icons.delete), - onPressed: () { - _showDeletePhotosSheet(context, _photos[_selectedIndex]); - }, - ), IconButton( icon: Icon(Icons.share), onPressed: () async { @@ -105,55 +94,4 @@ class _DetailPageState extends State { ], ); } - - void _showDeletePhotosSheet(BuildContext context, Photo photo) { - final action = CupertinoActionSheet( - actions: [ - CupertinoActionSheetAction( - child: Text("Delete on device"), - isDestructiveAction: true, - onPressed: () async { - await _deletePhoto(context, photo, false); - }, - ), - CupertinoActionSheetAction( - child: Text("Delete everywhere [WiP]"), - isDestructiveAction: true, - onPressed: () async { - await _deletePhoto(context, photo, true); - }, - ) - ], - cancelButton: CupertinoActionSheetAction( - child: Text("Cancel"), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }, - ), - ); - showCupertinoModalPopup(context: context, builder: (_) => action); - } - - Future _deletePhoto( - BuildContext context, Photo photo, bool deleteEverywhere) async { - await PhotoManager.editor.deleteWithIds([photo.localId]); - - deleteEverywhere - ? await DatabaseHelper.instance.markPhotoForDeletion(photo) - : await DatabaseHelper.instance.deletePhoto(photo); - - Navigator.of(context, rootNavigator: true).pop(); - - _pageController - .nextPage(duration: Duration(milliseconds: 250), curve: Curves.ease) - .then((value) { - if (widget.onPhotoDeleted != null) { - widget.onPhotoDeleted(photo); - } - _pageController.previousPage( - duration: Duration(milliseconds: 1), curve: Curves.linear); // h4ck - }); - - photoLoader.reloadPhotos(); - } } diff --git a/lib/ui/extents_page_view.dart b/lib/ui/extents_page_view.dart new file mode 100644 index 000000000..638889840 --- /dev/null +++ b/lib/ui/extents_page_view.dart @@ -0,0 +1,381 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart' hide PageView; + +/// This is copy-pasted from the Flutter framework with a support added for building +/// pages off screen using [Viewport.cacheExtents] and a [LayoutBuilder] +/// +/// Based on commit 3932ffb1cd5dfa0c3891c60977ee4f9cd70ade66 on channel dev + +// Having this global (mutable) page controller is a bit of a hack. We need it +// to plumb in the factory for _PagePosition, but it will end up accumulating +// a large list of scroll positions. As long as you don't try to actually +// control the scroll positions, everything should be fine. +final PageController _defaultPageController = PageController(); +const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); + +/// A scrollable list that works page by page. +/// +/// Each child of a page view is forced to be the same size as the viewport. +/// +/// You can use a [PageController] to control which page is visible in the view. +/// In addition to being able to control the pixel offset of the content inside +/// the [PageView], a [PageController] also lets you control the offset in terms +/// of pages, which are increments of the viewport size. +/// +/// The [PageController] can also be used to control the +/// [PageController.initialPage], which determines which page is shown when the +/// [PageView] is first constructed, and the [PageController.viewportFraction], +/// which determines the size of the pages as a fraction of the viewport size. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A} +/// +/// See also: +/// +/// * [PageController], which controls which page is visible in the view. +/// * [SingleChildScrollView], when you need to make a single child scrollable. +/// * [ListView], for a scrollable list of boxes. +/// * [GridView], for a scrollable grid of boxes. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +class ExtentsPageView extends StatefulWidget { + /// Creates a scrollable list that works page by page from an explicit [List] + /// of widgets. + /// + /// This constructor is appropriate for page views with a small number of + /// children because constructing the [List] requires doing work for every + /// child that could possibly be displayed in the page view, instead of just + /// those children that are actually visible. + ExtentsPageView({ + Key key, + this.scrollDirection = Axis.horizontal, + this.reverse = false, + PageController controller, + this.physics, + this.pageSnapping = true, + this.onPageChanged, + List children = const [], + this.dragStartBehavior = DragStartBehavior.start, + }) : controller = controller ?? _defaultPageController, + childrenDelegate = SliverChildListDelegate(children), + extents = 0, + super(key: key); + + /// Creates a scrollable list that works page by page using widgets that are + /// created on demand. + /// + /// This constructor is appropriate for page views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Providing a non-null [itemCount] lets the [PageView] compute the maximum + /// scroll extent. + /// + /// [itemBuilder] will be called only with indices greater than or equal to + /// zero and less than [itemCount]. + /// + /// [PageView.builder] by default does not support child reordering. If + /// you are planning to change child order at a later time, consider using + /// [PageView] or [PageView.custom]. + ExtentsPageView.builder({ + Key key, + this.scrollDirection = Axis.horizontal, + this.reverse = false, + PageController controller, + this.physics, + this.pageSnapping = true, + this.onPageChanged, + @required IndexedWidgetBuilder itemBuilder, + int itemCount, + this.dragStartBehavior = DragStartBehavior.start, + }) : controller = controller ?? _defaultPageController, + childrenDelegate = + SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), + extents = 0, + super(key: key); + + ExtentsPageView.extents({ + Key key, + this.extents = 1, + this.scrollDirection = Axis.horizontal, + this.reverse = false, + PageController controller, + this.physics, + this.pageSnapping = true, + this.onPageChanged, + @required IndexedWidgetBuilder itemBuilder, + int itemCount, + this.dragStartBehavior = DragStartBehavior.start, + }) : controller = controller ?? _defaultPageController, + childrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + ), + super(key: key); + + /// Creates a scrollable list that works page by page with a custom child + /// model. + /// + /// {@tool sample} + /// + /// This [PageView] uses a custom [SliverChildBuilderDelegate] to support child + /// reordering. + /// + /// ```dart + /// class MyPageView extends StatefulWidget { + /// @override + /// _MyPageViewState createState() => _MyPageViewState(); + /// } + /// + /// class _MyPageViewState extends State { + /// List items = ['1', '2', '3', '4', '5']; + /// + /// void _reverse() { + /// setState(() { + /// items = items.reversed.toList(); + /// }); + /// } + /// + /// @override + /// Widget build(BuildContext context) { + /// return Scaffold( + /// body: SafeArea( + /// child: PageView.custom( + /// childrenDelegate: SliverChildBuilderDelegate( + /// (BuildContext context, int index) { + /// return KeepAlive( + /// data: items[index], + /// key: ValueKey(items[index]), + /// ); + /// }, + /// childCount: items.length, + /// findChildIndexCallback: (Key key) { + /// final ValueKey valueKey = key; + /// final String data = valueKey.value; + /// return items.indexOf(data); + /// } + /// ), + /// ), + /// ), + /// bottomNavigationBar: BottomAppBar( + /// child: Row( + /// mainAxisAlignment: MainAxisAlignment.center, + /// children: [ + /// FlatButton( + /// onPressed: () => _reverse(), + /// child: Text('Reverse items'), + /// ), + /// ], + /// ), + /// ), + /// ); + /// } + /// } + /// + /// class KeepAlive extends StatefulWidget { + /// const KeepAlive({Key key, this.data}) : super(key: key); + /// + /// final String data; + /// + /// @override + /// _KeepAliveState createState() => _KeepAliveState(); + /// } + /// + /// class _KeepAliveState extends State with AutomaticKeepAliveClientMixin{ + /// @override + /// bool get wantKeepAlive => true; + /// + /// @override + /// Widget build(BuildContext context) { + /// super.build(context); + /// return Text(widget.data); + /// } + /// } + /// ``` + /// {@end-tool} + ExtentsPageView.custom({ + Key key, + this.scrollDirection = Axis.horizontal, + this.reverse = false, + PageController controller, + this.physics, + this.pageSnapping = true, + this.onPageChanged, + @required this.childrenDelegate, + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(childrenDelegate != null), + extents = 0, + controller = controller ?? _defaultPageController, + super(key: key); + + /// The number of pages to build off screen. + /// + /// For example, a value of `1` builds one page ahead and one page behind, + /// for a total of three built pages. + /// + /// This is especially useful for making sure heavyweight widgets have a chance + /// to load off-screen before the user pulls it into the viewport. + final int extents; + + /// The axis along which the page view scrolls. + /// + /// Defaults to [Axis.horizontal]. + final Axis scrollDirection; + + /// Whether the page view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + final bool reverse; + + /// An object that can be used to control the position to which this page + /// view is scrolled. + final PageController controller; + + /// How the page view should respond to user input. + /// + /// For example, determines how the page view continues to animate after the + /// user stops dragging the page view. + /// + /// The physics are modified to snap to page boundaries using + /// [PageScrollPhysics] prior to being used. + /// + /// Defaults to matching platform conventions. + final ScrollPhysics physics; + + /// Set to false to disable page snapping, useful for custom scroll behavior. + final bool pageSnapping; + + /// Called whenever the page in the center of the viewport changes. + final ValueChanged onPageChanged; + + /// A delegate that provides the children for the [PageView]. + /// + /// The [PageView.custom] constructor lets you specify this delegate + /// explicitly. The [PageView] and [PageView.builder] constructors create a + /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], + /// respectively. + final SliverChildDelegate childrenDelegate; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + @override + _PageViewState createState() => _PageViewState(); +} + +class _PageViewState extends State { + int _lastReportedPage = 0; + + @override + void initState() { + super.initState(); + _lastReportedPage = widget.controller.initialPage; + } + + AxisDirection _getDirection(BuildContext context) { + switch (widget.scrollDirection) { + case Axis.horizontal: + assert(debugCheckHasDirectionality(context)); + final TextDirection textDirection = Directionality.of(context); + final AxisDirection axisDirection = + textDirectionToAxisDirection(textDirection); + return widget.reverse + ? flipAxisDirection(axisDirection) + : axisDirection; + case Axis.vertical: + return widget.reverse ? AxisDirection.up : AxisDirection.down; + } + return null; + } + + @override + Widget build(BuildContext context) { + final AxisDirection axisDirection = _getDirection(context); + final ScrollPhysics physics = widget.pageSnapping + ? _kPagePhysics.applyTo(widget.physics) + : widget.physics; + + return NotificationListener( + onNotification: (ScrollNotification notification) { + if (notification.depth == 0 && + widget.onPageChanged != null && + notification is ScrollUpdateNotification) { + final PageMetrics metrics = notification.metrics; + 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, + viewportBuilder: (BuildContext context, ViewportOffset position) { + return LayoutBuilder( + builder: (context, constraints) { + assert(constraints.hasBoundedHeight); + assert(constraints.hasBoundedWidth); + + double cacheExtent; + + switch (widget.scrollDirection) { + case Axis.vertical: + cacheExtent = constraints.maxHeight * widget.extents; + break; + + case Axis.horizontal: + default: + cacheExtent = constraints.maxWidth * widget.extents; + break; + } + + return Viewport( + cacheExtent: cacheExtent, + axisDirection: axisDirection, + offset: position, + slivers: [ + SliverFillViewport( + viewportFraction: widget.controller.viewportFraction, + delegate: widget.childrenDelegate, + ), + ], + ); + }, + ); + }, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description + .add(EnumProperty('scrollDirection', widget.scrollDirection)); + description.add( + FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')); + description.add(DiagnosticsProperty( + 'controller', widget.controller, + showName: false)); + description.add(DiagnosticsProperty( + 'physics', widget.physics, + showName: false)); + description.add(FlagProperty('pageSnapping', + value: widget.pageSnapping, ifFalse: 'snapping disabled')); + } +} diff --git a/lib/ui/gallery.dart b/lib/ui/gallery.dart index 30aed1597..14caacff6 100644 --- a/lib/ui/gallery.dart +++ b/lib/ui/gallery.dart @@ -133,12 +133,6 @@ class _GalleryState extends State { final page = DetailPage( _photos, _photos.indexOf(photo), - onPhotoDeleted: (photo) { - setState(() { - Logger().i("Photo deleted! "); - _photos.remove(photo); - }); - }, ); Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/ui/gallery_app_bar_widget.dart b/lib/ui/gallery_app_bar_widget.dart index bcca6aa23..b91e7085b 100644 --- a/lib/ui/gallery_app_bar_widget.dart +++ b/lib/ui/gallery_app_bar_widget.dart @@ -25,7 +25,6 @@ class GalleryAppBarWidget extends StatefulWidget } class _GalleryAppBarWidgetState extends State { - PhotoLoader get photoLoader => Provider.of(context); @override Widget build(BuildContext context) { if (widget.selectedPhotos.isEmpty) { @@ -106,7 +105,7 @@ class _GalleryAppBarWidgetState extends State { : await DatabaseHelper.instance.deletePhoto(photo); } Navigator.of(context, rootNavigator: true).pop(); - photoLoader.reloadPhotos(); + PhotoLoader.instance.reloadPhotos(); if (widget.onPhotosDeleted != null) { widget.onPhotosDeleted(widget.selectedPhotos.toList()); }