nav bar: auto hide/show when fast scrolling
This commit is contained in:
parent
22d82135f0
commit
6d410e7540
5 changed files with 135 additions and 66 deletions
|
@ -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,7 +80,12 @@ 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>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
_draggableScrollBarEventStreamController.add(notification.event);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
body: SelectionProvider<AvesEntry>(
|
body: SelectionProvider<AvesEntry>(
|
||||||
child: QueryProvider(
|
child: QueryProvider(
|
||||||
initialQuery: liveFilter?.query,
|
initialQuery: liveFilter?.query,
|
||||||
|
@ -112,9 +119,15 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
drawer: AppDrawer(currentCollection: _collection),
|
drawer: AppDrawer(currentCollection: _collection),
|
||||||
bottomNavigationBar: showBottomNavigationBar ? AppBottomNavBar(currentCollection: _collection) : null,
|
bottomNavigationBar: showBottomNavigationBar
|
||||||
|
? AppBottomNavBar(
|
||||||
|
events: _draggableScrollBarEventStreamController.stream,
|
||||||
|
currentCollection: _collection,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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,7 +76,12 @@ 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>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
_draggableScrollBarEventStreamController.add(notification.event);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
body: WillPopScope(
|
body: WillPopScope(
|
||||||
onWillPop: () {
|
onWillPop: () {
|
||||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
final selection = context.read<Selection<FilterGridItem<T>>>();
|
||||||
|
@ -108,9 +116,14 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
bottomNavigationBar: showBottomNavigationBar ? const AppBottomNavBar() : null,
|
bottomNavigationBar: showBottomNavigationBar
|
||||||
|
? AppBottomNavBar(
|
||||||
|
events: _draggableScrollBarEventStreamController.stream,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue