#31 prevent scrolling when swiping from bottom (Android Q gesture nav)

This commit is contained in:
Thibault Deckers 2021-01-12 18:22:34 +09:00
parent c1face0f0f
commit 229b2e7b2b
6 changed files with 145 additions and 94 deletions

View file

@ -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(

View file

@ -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(),
],
);
}
}

View file

@ -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<T extends CollectionFilter> 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<double>(
valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) {
final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent);
return ValueListenableBuilder<double>(
valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) {
final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent);
return ValueListenableBuilder<String>(
valueListenable: queryNotifier,
builder: (context, query, child) {
final allFilters = filterEntries.keys;
final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList();
return ValueListenableBuilder<String>(
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<FilterGridItem>(
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<FilterGridItem>(
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<HighlightInfo>(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<HighlightInfo>(context, listen: false).add(item.filter),
child: scrollView,
);
},
);
},
);
},
),
),
),
),

View file

@ -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<EntryViewerStack> with SingleTickerPr
),
_buildTopOverlay(),
_buildBottomOverlay(),
BottomGestureAreaProtector(),
],
),
),

View file

@ -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<InfoPage> {
Widget build(BuildContext context) {
return MediaQueryDataProvider(
child: Scaffold(
body: SafeArea(
child: NotificationListener(
onNotification: _handleTopScroll,
child: Selector<MediaQueryData, Tuple2<double, double>>(
selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom),
builder: (c, mq, child) {
final mqWidth = mq.item1;
final mqViewInsetsBottom = mq.item2;
return ValueListenableBuilder<ImageEntry>(
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<MediaQueryData, Tuple2<double, double>>(
selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom),
builder: (c, mq, child) {
final mqWidth = mq.item1;
final mqViewInsetsBottom = mq.item2;
return ValueListenableBuilder<ImageEntry>(
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();
},
);
},
),
),
),
),

View file

@ -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<PanoramaPage> {
),
),
),
BottomGestureAreaProtector(),
],
),
resizeToAvoidBottomInset: false,