nav bar: auto hide/show when fast scrolling

This commit is contained in:
Thibault Deckers 2022-05-11 16:30:22 +09:00
parent 22d82135f0
commit 6d410e7540
5 changed files with 135 additions and 66 deletions

View file

@ -11,6 +11,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
@ -44,6 +45,7 @@ class CollectionPage extends StatefulWidget {
class _CollectionPageState extends State<CollectionPage> { class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
late CollectionLens _collection; late CollectionLens _collection;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
@override @override
void initState() { void initState() {
@ -78,30 +80,36 @@ class _CollectionPageState extends State<CollectionPage> {
child: Selector<Settings, bool>( child: Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar, selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) { builder: (context, showBottomNavigationBar, child) {
return Scaffold( return NotificationListener<DraggableScrollBarNotification>(
body: SelectionProvider<AvesEntry>( onNotification: (notification) {
child: QueryProvider( _draggableScrollBarEventStreamController.add(notification.event);
initialQuery: liveFilter?.query, return false;
child: Builder( },
builder: (context) => WillPopScope( child: Scaffold(
onWillPop: () { body: SelectionProvider<AvesEntry>(
final selection = context.read<Selection<AvesEntry>>(); child: QueryProvider(
if (selection.isSelecting) { initialQuery: liveFilter?.query,
selection.browse(); child: Builder(
return SynchronousFuture(false); builder: (context) => WillPopScope(
} onWillPop: () {
return SynchronousFuture(true); final selection = context.read<Selection<AvesEntry>>();
}, if (selection.isSelecting) {
child: DoubleBackPopScope( selection.browse();
child: GestureAreaProtectorStack( return SynchronousFuture(false);
child: SafeArea( }
bottom: false, return SynchronousFuture(true);
child: ChangeNotifierProvider<CollectionLens>.value( },
value: _collection, child: DoubleBackPopScope(
child: const CollectionGrid( child: GestureAreaProtectorStack(
// key is expected by test driver child: SafeArea(
key: Key('collection-grid'), bottom: false,
settingsRouteKey: CollectionPage.routeName, child: ChangeNotifierProvider<CollectionLens>.value(
value: _collection,
child: const CollectionGrid(
// key is expected by test driver
key: Key('collection-grid'),
settingsRouteKey: CollectionPage.routeName,
),
), ),
), ),
), ),
@ -110,11 +118,16 @@ class _CollectionPageState extends State<CollectionPage> {
), ),
), ),
), ),
drawer: AppDrawer(currentCollection: _collection),
bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar(
events: _draggableScrollBarEventStreamController.stream,
currentCollection: _collection,
)
: null,
resizeToAvoidBottomInset: false,
extendBody: true,
), ),
drawer: AppDrawer(currentCollection: _collection),
bottomNavigationBar: showBottomNavigationBar ? AppBottomNavBar(currentCollection: _collection) : null,
resizeToAvoidBottomInset: false,
extendBody: true,
); );
}, },
), ),

View file

@ -303,6 +303,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
} }
void _onVerticalDragStart() { void _onVerticalDragStart() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragStart).dispatch(context);
_labelAnimationController.forward(); _labelAnimationController.forward();
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
_showThumb(); _showThumb();
@ -324,6 +325,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
} }
void _onVerticalDragEnd() { void _onVerticalDragEnd() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context);
_scheduleFadeout(); _scheduleFadeout();
setState(() => _isDragInProcess = false); setState(() => _isDragInProcess = false);
} }
@ -454,3 +456,12 @@ class SlideFadeTransition extends StatelessWidget {
); );
} }
} }
@immutable
class DraggableScrollBarNotification extends Notification {
final DraggableScrollBarEvent event;
const DraggableScrollBarNotification(this.event);
}
enum DraggableScrollBarEvent { dragStart, dragEnd }

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
@ -50,8 +52,9 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final QueryTest<T>? applyQuery; final QueryTest<T>? applyQuery;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final HeroType heroType; final HeroType heroType;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
const FilterGridPage({ FilterGridPage({
Key? key, Key? key,
this.settingsRouteKey, this.settingsRouteKey,
required this.appBar, required this.appBar,
@ -73,44 +76,54 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
child: Selector<Settings, bool>( child: Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar, selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) { builder: (context, showBottomNavigationBar, child) {
return Scaffold( return NotificationListener<DraggableScrollBarNotification>(
body: WillPopScope( onNotification: (notification) {
onWillPop: () { _draggableScrollBarEventStreamController.add(notification.event);
final selection = context.read<Selection<FilterGridItem<T>>>(); return false;
if (selection.isSelecting) { },
selection.browse(); child: Scaffold(
return SynchronousFuture(false); body: WillPopScope(
} onWillPop: () {
return SynchronousFuture(true); final selection = context.read<Selection<FilterGridItem<T>>>();
}, if (selection.isSelecting) {
child: DoubleBackPopScope( selection.browse();
child: GestureAreaProtectorStack( return SynchronousFuture(false);
child: SafeArea( }
bottom: false, return SynchronousFuture(true);
child: FilterGrid<T>( },
// key is expected by test driver child: DoubleBackPopScope(
key: const Key('filter-grid'), child: GestureAreaProtectorStack(
settingsRouteKey: settingsRouteKey, child: SafeArea(
appBar: appBar, bottom: false,
appBarHeight: appBarHeight, child: FilterGrid<T>(
sections: sections, // key is expected by test driver
newFilters: newFilters, key: const Key('filter-grid'),
sortFactor: sortFactor, settingsRouteKey: settingsRouteKey,
showHeaders: showHeaders, appBar: appBar,
selectable: selectable, appBarHeight: appBarHeight,
queryNotifier: queryNotifier, sections: sections,
applyQuery: applyQuery, newFilters: newFilters,
emptyBuilder: emptyBuilder, sortFactor: sortFactor,
heroType: heroType, showHeaders: showHeaders,
selectable: selectable,
queryNotifier: queryNotifier,
applyQuery: applyQuery,
emptyBuilder: emptyBuilder,
heroType: heroType,
),
), ),
), ),
), ),
), ),
drawer: const AppDrawer(),
bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar(
events: _draggableScrollBarEventStreamController.stream,
)
: null,
resizeToAvoidBottomInset: false,
extendBody: true,
), ),
drawer: const AppDrawer(),
bottomNavigationBar: showBottomNavigationBar ? const AppBottomNavBar() : null,
resizeToAvoidBottomInset: false,
extendBody: true,
); );
}, },
), ),

View file

@ -1,12 +1,17 @@
import 'dart:async';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FloatingNavBar extends StatefulWidget { class FloatingNavBar extends StatefulWidget {
final ScrollController? scrollController; final ScrollController? scrollController;
final Stream<DraggableScrollBarEvent> events;
final Widget child; final Widget child;
const FloatingNavBar({ const FloatingNavBar({
Key? key, Key? key,
required this.scrollController, required this.scrollController,
required this.events,
required this.child, required this.child,
}) : super(key: key); }) : super(key: key);
@ -15,10 +20,12 @@ class FloatingNavBar extends StatefulWidget {
} }
class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProviderStateMixin { class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = [];
late AnimationController _controller; late AnimationController _controller;
late Animation<Offset> _offsetAnimation; late Animation<Offset> _offsetAnimation;
double? _lastOffset; double? _lastOffset;
double _delta = 0; double _delta = 0;
bool _isDragging = false;
static const double _deltaThreshold = 50; static const double _deltaThreshold = 50;
@ -64,10 +71,14 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
_lastOffset = null; _lastOffset = null;
_delta = 0; _delta = 0;
widget.scrollController?.addListener(_onScrollChange); widget.scrollController?.addListener(_onScrollChange);
_subscriptions.add(widget.events.listen(_onDraggableScrollBarEvent));
} }
void _unregisterWidget(FloatingNavBar widget) { void _unregisterWidget(FloatingNavBar widget) {
widget.scrollController?.removeListener(_onScrollChange); widget.scrollController?.removeListener(_onScrollChange);
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
} }
@override @override
@ -79,6 +90,8 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
} }
void _onScrollChange() { void _onScrollChange() {
if (_isDragging) return;
final scrollController = widget.scrollController; final scrollController = widget.scrollController;
if (scrollController == null) return; if (scrollController == null) return;
@ -88,13 +101,28 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
if (_delta.abs() > _deltaThreshold) { if (_delta.abs() > _deltaThreshold) {
if (_delta > 0) { if (_delta > 0) {
// hide _hide();
_controller.forward();
} else { } else {
// show _show();
_controller.reverse();
} }
_delta = 0; _delta = 0;
} }
} }
void _onDraggableScrollBarEvent(DraggableScrollBarEvent event) {
switch (event) {
case DraggableScrollBarEvent.dragStart:
_isDragging = true;
_hide();
break;
case DraggableScrollBarEvent.dragEnd:
_isDragging = false;
_show();
break;
}
}
void _show() => _controller.reverse();
void _hide() => _controller.forward();
} }

View file

@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
@ -14,11 +15,13 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class AppBottomNavBar extends StatelessWidget { class AppBottomNavBar extends StatelessWidget {
final Stream<DraggableScrollBarEvent> events;
// collection loaded in the `CollectionPage`, if any // collection loaded in the `CollectionPage`, if any
final CollectionLens? currentCollection; final CollectionLens? currentCollection;
const AppBottomNavBar({ const AppBottomNavBar({
Key? key, Key? key,
required this.events,
this.currentCollection, this.currentCollection,
}) : super(key: key); }) : super(key: key);
@ -73,6 +76,7 @@ class AppBottomNavBar extends StatelessWidget {
}, },
child: FloatingNavBar( child: FloatingNavBar(
scrollController: PrimaryScrollController.of(context), scrollController: PrimaryScrollController.of(context),
events: events,
child: SafeArea( child: SafeArea(
child: child, child: child,
), ),