From 9fb790eefc5212b0ccf0658ec9b7780efe5169d6 Mon Sep 17 00:00:00 2001 From: Vishnu Mohandas Date: Fri, 17 Apr 2020 13:47:37 +0530 Subject: [PATCH] Make image view swipable --- lib/face_search_manager.dart | 17 +- lib/ui/detail_page.dart | 58 ++-- lib/ui/extents_page_view.dart | 381 +++++++++++++++++++++++++++ lib/ui/face_search_results_page.dart | 43 +-- lib/ui/gallery.dart | 2 +- 5 files changed, 443 insertions(+), 58 deletions(-) create mode 100644 lib/ui/extents_page_view.dart diff --git a/lib/face_search_manager.dart b/lib/face_search_manager.dart index aed8252416..979f1b7346 100644 --- a/lib/face_search_manager.dart +++ b/lib/face_search_manager.dart @@ -1,8 +1,10 @@ import 'package:dio/dio.dart'; import 'package:logger/logger.dart'; import 'package:myapp/core/constants.dart' as Constants; +import 'package:myapp/db/db_helper.dart'; import 'models/face.dart'; +import 'models/photo.dart'; import 'models/search_result.dart'; class FaceSearchManager { @@ -23,14 +25,13 @@ class FaceSearchManager { .catchError(_onError); } - Future> getFaceSearchResults(Face face) { - return _dio - .get(Constants.ENDPOINT + "/search/face/" + face.faceID.toString(), - queryParameters: {"user": Constants.USER}) - .then((response) => (response.data["results"] as List) - .map((result) => SearchResult(result)) - .toList()) - .catchError(_onError); + Future> getFaceSearchResults(Face face) async { + var futures = _dio.get( + Constants.ENDPOINT + "/search/face/" + face.faceID.toString(), + queryParameters: {"user": Constants.USER}).then((response) => (response + .data["results"] as List) + .map((result) => (DatabaseHelper.instance.getPhotoByPath(result)))); + return Future.wait(await futures); } void _onError(error) { diff --git a/lib/ui/detail_page.dart b/lib/ui/detail_page.dart index a82d656bc1..013343113c 100644 --- a/lib/ui/detail_page.dart +++ b/lib/ui/detail_page.dart @@ -1,20 +1,27 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:logger/logger.dart'; import 'package:myapp/core/lru_map.dart'; import 'package:myapp/models/photo.dart'; import 'package:photo_view/photo_view.dart'; import 'package:share_extend/share_extend.dart'; +import 'extents_page_view.dart'; -class DetailPage extends StatelessWidget { - final Photo photo; +class DetailPage extends StatefulWidget { + final List photos; + int selectedIndex; - const DetailPage(this.photo, {Key key}) : super(key: key); + DetailPage(this.photos, this.selectedIndex, {Key key}) : super(key: key); + + @override + _DetailPageState createState() => _DetailPageState(); +} + +class _DetailPageState extends State { + bool _shouldDisableScroll = false; @override Widget build(BuildContext context) { - Logger().i(photo.localPath); return Scaffold( appBar: AppBar( actions: [ @@ -22,32 +29,53 @@ class DetailPage extends StatelessWidget { IconButton( icon: Icon(Icons.share), onPressed: () { - ShareExtend.share(photo.localPath, "image"); + ShareExtend.share( + widget.photos[widget.selectedIndex].localPath, "image"); }, ) ], ), body: Center( child: Container( - child: _buildContent(context), + child: ExtentsPageView.extents( + itemBuilder: (context, index) { + return _buildItem(context, widget.photos[index]); + }, + onPageChanged: (int index) { + widget.selectedIndex = index; + }, + physics: _shouldDisableScroll + ? NeverScrollableScrollPhysics() + : PageScrollPhysics(), + ), ), ), ); } - Widget _buildContent(BuildContext context) { + Widget _buildItem(BuildContext context, Photo photo) { var image = ImageLruCache.getData(photo.localPath) == null ? Image.file( File(photo.localPath), filterQuality: FilterQuality.low, ) : ImageLruCache.getData(photo.localPath); - return GestureDetector( - onVerticalDragUpdate: (details) { - Navigator.pop(context); - }, - child: PhotoView( - imageProvider: image.image, - )); + ValueChanged scaleStateChangedCallback = (value) { + var shouldDisableScroll; + if (value == PhotoViewScaleState.initial) { + shouldDisableScroll = false; + } else { + shouldDisableScroll = true; + } + if (shouldDisableScroll != _shouldDisableScroll) { + setState(() { + _shouldDisableScroll = shouldDisableScroll; + }); + } + }; + return PhotoView( + imageProvider: image.image, + scaleStateChangedCallback: scaleStateChangedCallback, + ); } } diff --git a/lib/ui/extents_page_view.dart b/lib/ui/extents_page_view.dart new file mode 100644 index 0000000000..6388898405 --- /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/face_search_results_page.dart b/lib/ui/face_search_results_page.dart index 5aeb3474a9..d902e7d36b 100644 --- a/lib/ui/face_search_results_page.dart +++ b/lib/ui/face_search_results_page.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:myapp/db/db_helper.dart'; import 'package:myapp/face_search_manager.dart'; import 'package:myapp/models/face.dart'; import 'package:myapp/models/photo.dart'; -import 'package:myapp/models/search_result.dart'; import 'package:myapp/ui/circular_network_image_widget.dart'; import 'package:myapp/core/constants.dart' as Constants; import 'package:myapp/ui/image_widget.dart'; -import 'package:myapp/ui/network_image_detail_page.dart'; import 'detail_page.dart'; @@ -38,14 +35,14 @@ class FaceSearchResultsPage extends StatelessWidget { ); } - FutureBuilder> _getBody() { - return FutureBuilder>( + FutureBuilder> _getBody() { + return FutureBuilder>( future: _faceSearchManager.getFaceSearchResults(_face), builder: (context, snapshot) { if (snapshot.hasData) { return GridView.builder( itemBuilder: (_, index) => - _buildItem(context, snapshot.data[index].path), + _buildItem(context, snapshot.data, index), itemCount: snapshot.data.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, @@ -57,40 +54,18 @@ class FaceSearchResultsPage extends StatelessWidget { ); } - Widget _buildItem(BuildContext context, String path) { + Widget _buildItem(BuildContext context, List photos, int index) { return GestureDetector( onTap: () async { - _routeToDetailPage(path, context); + _routeToDetailPage(photos, index, context); }, - child: _getImage(path), + child: ImageWidget(photos[index]), ); } - Widget _getImage(String path) { - return FutureBuilder( - future: DatabaseHelper.instance.getPhotoByPath(path), - builder: (_, snapshot) { - if (snapshot.hasData) { - return ImageWidget(snapshot.data); - } else if (snapshot.hasError) { - return Container( - margin: EdgeInsets.all(2), - child: Image.network(Constants.ENDPOINT + "/" + path, - height: 124, width: 124, fit: BoxFit.cover), - ); - } else { - return Text("Loading..."); - } - }, - ); - } - - void _routeToDetailPage(String path, BuildContext context) async { - Widget page = NetworkImageDetailPage(path); - var photo = await DatabaseHelper.instance.getPhotoByPath(path); - if (photo != null) { - page = DetailPage(photo); - } + void _routeToDetailPage( + List photos, int index, BuildContext context) async { + var page = DetailPage(photos, index); Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { diff --git a/lib/ui/gallery.dart b/lib/ui/gallery.dart index fd001c6a6f..20ee3e7815 100644 --- a/lib/ui/gallery.dart +++ b/lib/ui/gallery.dart @@ -160,7 +160,7 @@ class _GalleryState extends State { } void routeToDetailPage(Photo photo, BuildContext context) { - final page = DetailPage(photo); + final page = DetailPage(photoLoader.photos, photoLoader.photos.indexOf(photo)); Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) {