#31 prevent scrolling when swiping from bottom (Android Q gesture nav)
This commit is contained in:
parent
c1face0f0f
commit
229b2e7b2b
6 changed files with 145 additions and 94 deletions
|
@ -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(
|
||||
|
|
38
lib/widgets/common/gesture_area_protector.dart
Normal file
38
lib/widgets/common/gesture_area_protector.dart
Normal 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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue