From 6d410e7540225f64f9c37c00fefbc8a62cfd883a Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 11 May 2022 16:30:22 +0900 Subject: [PATCH] nav bar: auto hide/show when fast scrolling --- lib/widgets/collection/collection_page.dart | 69 +++++++++------- .../common/basic/draggable_scrollbar.dart | 11 +++ .../filter_grids/common/filter_grid_page.dart | 81 +++++++++++-------- lib/widgets/navigation/nav_bar/floating.dart | 36 ++++++++- lib/widgets/navigation/nav_bar/nav_bar.dart | 4 + 5 files changed, 135 insertions(+), 66 deletions(-) diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 99087891d..214192c84 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -11,6 +11,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.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/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -44,6 +45,7 @@ class CollectionPage extends StatefulWidget { class _CollectionPageState extends State { final List _subscriptions = []; late CollectionLens _collection; + final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast(); @override void initState() { @@ -78,30 +80,36 @@ class _CollectionPageState extends State { child: Selector( selector: (context, s) => s.showBottomNavigationBar, builder: (context, showBottomNavigationBar, child) { - return Scaffold( - body: SelectionProvider( - child: QueryProvider( - initialQuery: liveFilter?.query, - child: Builder( - builder: (context) => WillPopScope( - onWillPop: () { - final selection = context.read>(); - if (selection.isSelecting) { - selection.browse(); - return SynchronousFuture(false); - } - return SynchronousFuture(true); - }, - child: DoubleBackPopScope( - child: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: ChangeNotifierProvider.value( - value: _collection, - child: const CollectionGrid( - // key is expected by test driver - key: Key('collection-grid'), - settingsRouteKey: CollectionPage.routeName, + return NotificationListener( + onNotification: (notification) { + _draggableScrollBarEventStreamController.add(notification.event); + return false; + }, + child: Scaffold( + body: SelectionProvider( + child: QueryProvider( + initialQuery: liveFilter?.query, + child: Builder( + builder: (context) => WillPopScope( + onWillPop: () { + final selection = context.read>(); + if (selection.isSelecting) { + selection.browse(); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: DoubleBackPopScope( + child: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: ChangeNotifierProvider.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 { ), ), ), + 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, ); }, ), diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart index 23817bafb..e1fe56dc7 100644 --- a/lib/widgets/common/basic/draggable_scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -303,6 +303,7 @@ class _DraggableScrollbarState extends State with TickerProv } void _onVerticalDragStart() { + const DraggableScrollBarNotification(DraggableScrollBarEvent.dragStart).dispatch(context); _labelAnimationController.forward(); _fadeoutTimer?.cancel(); _showThumb(); @@ -324,6 +325,7 @@ class _DraggableScrollbarState extends State with TickerProv } void _onVerticalDragEnd() { + const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context); _scheduleFadeout(); 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 } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 06ff239ff..dd0de8f37 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/app_mode.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; @@ -50,8 +52,9 @@ class FilterGridPage extends StatelessWidget { final QueryTest? applyQuery; final Widget Function() emptyBuilder; final HeroType heroType; + final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast(); - const FilterGridPage({ + FilterGridPage({ Key? key, this.settingsRouteKey, required this.appBar, @@ -73,44 +76,54 @@ class FilterGridPage extends StatelessWidget { child: Selector( selector: (context, s) => s.showBottomNavigationBar, builder: (context, showBottomNavigationBar, child) { - return Scaffold( - body: WillPopScope( - onWillPop: () { - final selection = context.read>>(); - if (selection.isSelecting) { - selection.browse(); - return SynchronousFuture(false); - } - return SynchronousFuture(true); - }, - child: DoubleBackPopScope( - child: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: FilterGrid( - // key is expected by test driver - key: const Key('filter-grid'), - settingsRouteKey: settingsRouteKey, - appBar: appBar, - appBarHeight: appBarHeight, - sections: sections, - newFilters: newFilters, - sortFactor: sortFactor, - showHeaders: showHeaders, - selectable: selectable, - queryNotifier: queryNotifier, - applyQuery: applyQuery, - emptyBuilder: emptyBuilder, - heroType: heroType, + return NotificationListener( + onNotification: (notification) { + _draggableScrollBarEventStreamController.add(notification.event); + return false; + }, + child: Scaffold( + body: WillPopScope( + onWillPop: () { + final selection = context.read>>(); + if (selection.isSelecting) { + selection.browse(); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: DoubleBackPopScope( + child: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: FilterGrid( + // key is expected by test driver + key: const Key('filter-grid'), + settingsRouteKey: settingsRouteKey, + appBar: appBar, + appBarHeight: appBarHeight, + sections: sections, + newFilters: newFilters, + sortFactor: sortFactor, + 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, ); }, ), diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart index 6e82c61ba..77333f211 100644 --- a/lib/widgets/navigation/nav_bar/floating.dart +++ b/lib/widgets/navigation/nav_bar/floating.dart @@ -1,12 +1,17 @@ +import 'dart:async'; + +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; class FloatingNavBar extends StatefulWidget { final ScrollController? scrollController; + final Stream events; final Widget child; const FloatingNavBar({ Key? key, required this.scrollController, + required this.events, required this.child, }) : super(key: key); @@ -15,10 +20,12 @@ class FloatingNavBar extends StatefulWidget { } class _FloatingNavBarState extends State with SingleTickerProviderStateMixin { + final List _subscriptions = []; late AnimationController _controller; late Animation _offsetAnimation; double? _lastOffset; double _delta = 0; + bool _isDragging = false; static const double _deltaThreshold = 50; @@ -64,10 +71,14 @@ class _FloatingNavBarState extends State with SingleTickerProvid _lastOffset = null; _delta = 0; widget.scrollController?.addListener(_onScrollChange); + _subscriptions.add(widget.events.listen(_onDraggableScrollBarEvent)); } void _unregisterWidget(FloatingNavBar widget) { widget.scrollController?.removeListener(_onScrollChange); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); } @override @@ -79,6 +90,8 @@ class _FloatingNavBarState extends State with SingleTickerProvid } void _onScrollChange() { + if (_isDragging) return; + final scrollController = widget.scrollController; if (scrollController == null) return; @@ -88,13 +101,28 @@ class _FloatingNavBarState extends State with SingleTickerProvid if (_delta.abs() > _deltaThreshold) { if (_delta > 0) { - // hide - _controller.forward(); + _hide(); } else { - // show - _controller.reverse(); + _show(); } _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(); } diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart index 5bda60b48..66b936104 100644 --- a/lib/widgets/navigation/nav_bar/nav_bar.dart +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -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_source.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/fx/blurred.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -14,11 +15,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class AppBottomNavBar extends StatelessWidget { + final Stream events; // collection loaded in the `CollectionPage`, if any final CollectionLens? currentCollection; const AppBottomNavBar({ Key? key, + required this.events, this.currentCollection, }) : super(key: key); @@ -73,6 +76,7 @@ class AppBottomNavBar extends StatelessWidget { }, child: FloatingNavBar( scrollController: PrimaryScrollController.of(context), + events: events, child: SafeArea( child: child, ),