From 229b2e7b2bfb6ce50e98cf98f63c7f653496a27e Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 12 Jan 2021 18:22:34 +0900 Subject: [PATCH] #31 prevent scrolling when swiping from bottom (Android Q gesture nav) --- lib/widgets/collection/collection_page.dart | 5 +- .../common/gesture_area_protector.dart | 38 +++++ .../filter_grids/common/filter_grid_page.dart | 139 +++++++++--------- lib/widgets/viewer/entry_viewer_stack.dart | 2 + lib/widgets/viewer/info/info_page.dart | 53 +++---- lib/widgets/viewer/panorama_page.dart | 2 + 6 files changed, 145 insertions(+), 94 deletions(-) create mode 100644 lib/widgets/common/gesture_area_protector.dart diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 7a52c08f2..ba71b34c9 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,6 +1,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail_collection.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; +import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:flutter/foundation.dart'; @@ -30,7 +31,9 @@ class CollectionPage extends StatelessWidget { return SynchronousFuture(true); }, child: DoubleBackPopScope( - child: ThumbnailCollection(), + child: GestureAreaProtectorStack( + child: ThumbnailCollection(), + ), ), ), drawer: AppDrawer( diff --git a/lib/widgets/common/gesture_area_protector.dart b/lib/widgets/common/gesture_area_protector.dart new file mode 100644 index 000000000..33a736d6b --- /dev/null +++ b/lib/widgets/common/gesture_area_protector.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +// This widget should be added on top of Scaffolds with: +// - `resizeToAvoidBottomInset` set to false, +// - a vertically scrollable body. +// It will prevent the body from scrolling when a user swipe from bottom to use Android Q style navigation gestures. +class BottomGestureAreaProtector extends StatelessWidget { + // as of Flutter v1.22.5, `systemGestureInsets` from `MediaQuery` mistakenly reports no bottom inset, + // so we use an empirical measurement instead + static const double systemGestureInsetsBottom = 32; + + @override + Widget build(BuildContext context) { + return Positioned( + left: 0, + right: 0, + bottom: 0, + height: systemGestureInsetsBottom, + child: AbsorbPointer(), + ); + } +} + +class GestureAreaProtectorStack extends StatelessWidget { + final Widget child; + + const GestureAreaProtectorStack({@required this.child}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + BottomGestureAreaProtector(), + ], + ); + } +} diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 84151ff22..c5d28b40e 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -8,6 +8,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; @@ -65,81 +66,83 @@ class FilterGridPage extends StatelessWidget { child: Scaffold( body: DoubleBackPopScope( child: HighlightInfoProvider( - child: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - final viewportSize = constraints.biggest; - assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); - if (viewportSize.isEmpty) return SizedBox.shrink(); + child: GestureAreaProtectorStack( + child: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final viewportSize = constraints.biggest; + assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); + if (viewportSize.isEmpty) return SizedBox.shrink(); - final tileExtentManager = TileExtentManager( - settingsRouteKey: settingsRouteKey ?? context.currentRouteName, - extentNotifier: _tileExtentNotifier, - columnCountDefault: columnCountDefault, - extentMin: extentMin, - spacing: spacing, - )..applyTileExtent(viewportSize: viewportSize); + final tileExtentManager = TileExtentManager( + settingsRouteKey: settingsRouteKey ?? context.currentRouteName, + extentNotifier: _tileExtentNotifier, + columnCountDefault: columnCountDefault, + extentMin: extentMin, + spacing: spacing, + )..applyTileExtent(viewportSize: viewportSize); - return ValueListenableBuilder( - valueListenable: _tileExtentNotifier, - builder: (context, tileExtent, child) { - final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); + return ValueListenableBuilder( + valueListenable: _tileExtentNotifier, + builder: (context, tileExtent, child) { + final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); - return ValueListenableBuilder( - valueListenable: queryNotifier, - builder: (context, query, child) { - final allFilters = filterEntries.keys; - final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList(); + return ValueListenableBuilder( + valueListenable: queryNotifier, + builder: (context, query, child) { + final allFilters = filterEntries.keys; + final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList(); - final scrollView = AnimationLimiter( - child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)), - ); + final scrollView = AnimationLimiter( + child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)), + ); - return GridScaleGestureDetector( - tileExtentManager: tileExtentManager, - scrollableKey: _scrollableKey, - appBarHeightNotifier: _appBarHeightNotifier, - viewportSize: viewportSize, - gridBuilder: (center, extent, child) => CustomPaint( - painter: GridPainter( - center: center, - extent: extent, - spacing: tileExtentManager.spacing, - color: Colors.grey.shade700, - ), - child: child, - ), - scaledBuilder: (item, extent) { - final filter = item.filter; - return SizedBox( - width: extent, - height: extent, - child: DecoratedFilterChip( - source: source, - filter: filter, - entry: item.entry, + return GridScaleGestureDetector( + tileExtentManager: tileExtentManager, + scrollableKey: _scrollableKey, + appBarHeightNotifier: _appBarHeightNotifier, + viewportSize: viewportSize, + gridBuilder: (center, extent, child) => CustomPaint( + painter: GridPainter( + center: center, extent: extent, - pinned: settings.pinnedFilters.contains(filter), - highlightable: false, + spacing: tileExtentManager.spacing, + color: Colors.grey.shade700, ), - ); - }, - getScaledItemTileRect: (context, item) { - final index = visibleFilters.indexOf(item.filter); - final column = index % columnCount; - final row = (index / columnCount).floor(); - final left = tileExtent * column + spacing * (column - 1); - final top = tileExtent * row + spacing * (row - 1); - return Rect.fromLTWH(left, top, tileExtent, tileExtent); - }, - onScaled: (item) => Provider.of(context, listen: false).add(item.filter), - child: scrollView, - ); - }, - ); - }, - ); - }, + child: child, + ), + scaledBuilder: (item, extent) { + final filter = item.filter; + return SizedBox( + width: extent, + height: extent, + child: DecoratedFilterChip( + source: source, + filter: filter, + entry: item.entry, + extent: extent, + pinned: settings.pinnedFilters.contains(filter), + highlightable: false, + ), + ); + }, + getScaledItemTileRect: (context, item) { + final index = visibleFilters.indexOf(item.filter); + final column = index % columnCount; + final row = (index / columnCount).floor(); + final left = tileExtent * column + spacing * (column - 1); + final top = tileExtent * row + spacing * (row - 1); + return Rect.fromLTWH(left, top, tileExtent, tileExtent); + }, + onScaled: (item) => Provider.of(context, listen: false).add(item.filter), + child: scrollView, + ); + }, + ); + }, + ); + }, + ), ), ), ), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 79d5510a2..116befdc2 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -8,6 +8,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/entry_scroller.dart'; @@ -185,6 +186,7 @@ class _EntryViewerStackState extends State with SingleTickerPr ), _buildTopOverlay(), _buildBottomOverlay(), + BottomGestureAreaProtector(), ], ), ), diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index df918e2a4..7f6bf741c 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -2,6 +2,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart'; import 'package:aves/widgets/viewer/info/info_app_bar.dart'; @@ -40,31 +41,33 @@ class _InfoPageState extends State { Widget build(BuildContext context) { return MediaQueryDataProvider( child: Scaffold( - body: SafeArea( - child: NotificationListener( - onNotification: _handleTopScroll, - child: Selector>( - selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom), - builder: (c, mq, child) { - final mqWidth = mq.item1; - final mqViewInsetsBottom = mq.item2; - return ValueListenableBuilder( - valueListenable: widget.entryNotifier, - builder: (context, entry, child) { - return entry != null - ? _InfoPageContent( - collection: collection, - entry: entry, - visibleNotifier: widget.visibleNotifier, - scrollController: _scrollController, - split: mqWidth > 400, - mqViewInsetsBottom: mqViewInsetsBottom, - goToViewer: _goToViewer, - ) - : SizedBox.shrink(); - }, - ); - }, + body: GestureAreaProtectorStack( + child: SafeArea( + child: NotificationListener( + onNotification: _handleTopScroll, + child: Selector>( + selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom), + builder: (c, mq, child) { + final mqWidth = mq.item1; + final mqViewInsetsBottom = mq.item2; + return ValueListenableBuilder( + valueListenable: widget.entryNotifier, + builder: (context, entry, child) { + return entry != null + ? _InfoPageContent( + collection: collection, + entry: entry, + visibleNotifier: widget.visibleNotifier, + scrollController: _scrollController, + split: mqWidth > 400, + mqViewInsetsBottom: mqViewInsetsBottom, + goToViewer: _goToViewer, + ) + : SizedBox.shrink(); + }, + ); + }, + ), ), ), ), diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 4391c3f83..d1e44385b 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -2,6 +2,7 @@ import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/foundation.dart'; @@ -120,6 +121,7 @@ class _PanoramaPageState extends State { ), ), ), + BottomGestureAreaProtector(), ], ), resizeToAvoidBottomInset: false,