collection: fixed scroll thumb top padding according to app bar height

This commit is contained in:
Thibault Deckers 2020-03-31 10:44:39 +09:00
parent 459fc24856
commit b3fde095e9
9 changed files with 128 additions and 125 deletions

View file

@ -19,6 +19,9 @@ class Constants {
], ],
); );
// ref _PopupMenuRoute._kMenuDuration
static const popupMenuTransitionDuration = Duration(milliseconds: 300);
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling // TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
static const double thumbnailCacheExtent = 50; static const double thumbnailCacheExtent = 50;

View file

@ -1,6 +1,7 @@
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/filter_bar.dart';
import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/album/search/search_delegate.dart';
@ -12,13 +13,17 @@ import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CollectionAppBar extends StatefulWidget implements PreferredSizeWidget { class CollectionAppBar extends StatefulWidget {
final ValueNotifier<PageState> stateNotifier; final ValueNotifier<PageState> stateNotifier;
final ValueNotifier<double> appBarHeightNotifier;
final CollectionLens collection;
@override const CollectionAppBar({
final Size preferredSize = Size.fromHeight(kToolbarHeight + FilterBar.preferredHeight); Key key,
@required this.stateNotifier,
CollectionAppBar({this.stateNotifier}); @required this.appBarHeightNotifier,
@required this.collection,
}) : super(key: key);
@override @override
_CollectionAppBarState createState() => _CollectionAppBarState(); _CollectionAppBarState createState() => _CollectionAppBarState();
@ -31,6 +36,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
ValueNotifier<PageState> get stateNotifier => widget.stateNotifier; ValueNotifier<PageState> get stateNotifier => widget.stateNotifier;
CollectionLens get collection => widget.collection;
bool get hasFilters => collection.filters.isNotEmpty;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -39,6 +48,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
vsync: this, vsync: this,
); );
_registerWidget(widget); _registerWidget(widget);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight());
} }
@override @override
@ -56,11 +66,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
void _registerWidget(CollectionAppBar widget) { void _registerWidget(CollectionAppBar widget) {
stateNotifier.addListener(_onStateChange); widget.stateNotifier.addListener(_onStateChange);
widget.collection.filterChangeNotifier.addListener(_updateHeight);
} }
void _unregisterWidget(CollectionAppBar widget) { void _unregisterWidget(CollectionAppBar widget) {
stateNotifier.removeListener(_onStateChange); widget.stateNotifier.removeListener(_onStateChange);
widget.collection.filterChangeNotifier.removeListener(_updateHeight);
} }
@override @override
@ -69,16 +81,14 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
valueListenable: stateNotifier, valueListenable: stateNotifier,
builder: (context, state, child) { builder: (context, state, child) {
debugPrint('$runtimeType builder state=$state'); debugPrint('$runtimeType builder state=$state');
return Consumer<CollectionLens>( return AnimatedBuilder(
builder: (context, collection, child) => AnimatedBuilder( animation: collection.filterChangeNotifier,
animation: collection.filterChangeNotifier, builder: (context, child) => SliverAppBar(
builder: (context, child) => SliverAppBar( leading: _buildAppBarLeading(),
leading: _buildAppBarLeading(), title: _buildAppBarTitle(),
title: _buildAppBarTitle(), actions: _buildActions(),
actions: _buildActions(), bottom: hasFilters ? FilterBar() : null,
bottom: collection.filters.isNotEmpty ? FilterBar() : null, floating: true,
floating: true,
),
), ),
); );
}, },
@ -127,17 +137,15 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
builder: (context) { builder: (context) {
switch (stateNotifier.value) { switch (stateNotifier.value) {
case PageState.browse: case PageState.browse:
return Consumer<CollectionLens>( return IconButton(
builder: (context, collection, child) => IconButton( icon: Icon(OMIcons.search),
icon: Icon(OMIcons.search), onPressed: () async {
onPressed: () async { final filter = await showSearch(
final filter = await showSearch( context: context,
context: context, delegate: ImageSearchDelegate(collection),
delegate: ImageSearchDelegate(collection), );
); collection.addFilter(filter);
collection.addFilter(filter); },
},
),
); );
case PageState.search: case PageState.search:
return IconButton( return IconButton(
@ -149,55 +157,53 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}, },
), ),
Builder( Builder(
builder: (context) => Consumer<CollectionLens>( builder: (context) => PopupMenuButton<CollectionAction>(
builder: (context, collection, child) => PopupMenuButton<CollectionAction>( itemBuilder: (context) => [
itemBuilder: (context) => [ PopupMenuItem(
value: CollectionAction.sortByDate,
child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date),
),
PopupMenuItem(
value: CollectionAction.sortBySize,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size),
),
PopupMenuItem(
value: CollectionAction.sortByName,
child: MenuRow(text: 'Sort by name', checked: collection.sortFactor == SortFactor.name),
),
const PopupMenuDivider(),
if (collection.sortFactor == SortFactor.date) ...[
PopupMenuItem( PopupMenuItem(
value: CollectionAction.sortByDate, value: CollectionAction.groupByAlbum,
child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date), child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.sortBySize, value: CollectionAction.groupByMonth,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size), child: MenuRow(text: 'Group by month', checked: collection.groupFactor == GroupFactor.month),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.sortByName, value: CollectionAction.groupByDay,
child: MenuRow(text: 'Sort by name', checked: collection.sortFactor == SortFactor.name), child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day),
), ),
const PopupMenuDivider(), const PopupMenuDivider(),
if (collection.sortFactor == SortFactor.date) ...[
PopupMenuItem(
value: CollectionAction.groupByAlbum,
child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
),
PopupMenuItem(
value: CollectionAction.groupByMonth,
child: MenuRow(text: 'Group by month', checked: collection.groupFactor == GroupFactor.month),
),
PopupMenuItem(
value: CollectionAction.groupByDay,
child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day),
),
const PopupMenuDivider(),
],
PopupMenuItem(
value: CollectionAction.stats,
child: MenuRow(text: 'Stats', icon: OMIcons.pieChart),
),
], ],
onSelected: (action) => _onActionSelected(collection, action), PopupMenuItem(
), value: CollectionAction.stats,
child: MenuRow(text: 'Stats', icon: OMIcons.pieChart),
),
],
onSelected: _onActionSelected,
), ),
), ),
]; ];
} }
void _onActionSelected(CollectionLens collection, CollectionAction action) async { void _onActionSelected(CollectionAction action) async {
// wait for the popup menu to hide before proceeding with the action // wait for the popup menu to hide before proceeding with the action
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(Constants.popupMenuTransitionDuration);
switch (action) { switch (action) {
case CollectionAction.stats: case CollectionAction.stats:
unawaited(_goToStats(collection)); unawaited(_goToStats());
break; break;
case CollectionAction.groupByAlbum: case CollectionAction.groupByAlbum:
settings.collectionGroupFactor = GroupFactor.album; settings.collectionGroupFactor = GroupFactor.album;
@ -226,7 +232,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
} }
Future<void> _goToStats(CollectionLens collection) { Future<void> _goToStats() {
return Navigator.push( return Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -245,6 +251,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
_searchFieldController.clear(); _searchFieldController.clear();
} }
} }
void _updateHeight() {
widget.appBarHeightNotifier.value = kToolbarHeight + (hasFilters ? FilterBar.preferredHeight : 0);
}
} }
class SearchField extends StatelessWidget { class SearchField extends StatelessWidget {

View file

@ -1,7 +1,5 @@
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/widgets/album/collection_app_bar.dart';
import 'package:aves/widgets/album/collection_drawer.dart'; import 'package:aves/widgets/album/collection_drawer.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -11,7 +9,9 @@ import 'package:provider/provider.dart';
class CollectionPage extends StatelessWidget { class CollectionPage extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
const CollectionPage(this.collection); final ValueNotifier<PageState> _stateNotifier = ValueNotifier(PageState.browse);
CollectionPage(this.collection);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -20,7 +20,18 @@ class CollectionPage extends StatelessWidget {
child: ChangeNotifierProvider<CollectionLens>.value( child: ChangeNotifierProvider<CollectionLens>.value(
value: collection, value: collection,
child: Scaffold( child: Scaffold(
body: CollectionPageBody(), body: WillPopScope(
onWillPop: () {
if (_stateNotifier.value == PageState.search) {
_stateNotifier.value = PageState.browse;
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: ThumbnailCollection(
stateNotifier: _stateNotifier,
),
),
drawer: CollectionDrawer( drawer: CollectionDrawer(
source: collection.source, source: collection.source,
), ),
@ -31,27 +42,4 @@ class CollectionPage extends StatelessWidget {
} }
} }
class CollectionPageBody extends StatelessWidget {
final ValueNotifier<PageState> _stateNotifier = ValueNotifier(PageState.browse);
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (_stateNotifier.value == PageState.search) {
_stateNotifier.value = PageState.browse;
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: ThumbnailCollection(
appBar: CollectionAppBar(
stateNotifier: _stateNotifier,
),
emptyBuilder: (context) => EmptyContent(),
),
);
}
}
enum PageState { browse, search } enum PageState { browse, search }

View file

@ -167,7 +167,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
), ),
), ),
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: ValueListenableBuilder( child: ValueListenableBuilder<double>(
valueListenable: widget.scaledCountNotifier, valueListenable: widget.scaledCountNotifier,
builder: (context, columnCount, child) { builder: (context, columnCount, child) {
final extent = gridWidth / columnCount; final extent = gridWidth / columnCount;

View file

@ -1,22 +1,24 @@
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
import 'package:aves/widgets/album/collection_app_bar.dart';
import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/album/collection_scaling.dart'; import 'package:aves/widgets/album/collection_scaling.dart';
import 'package:aves/widgets/album/collection_section.dart'; import 'package:aves/widgets/album/collection_section.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/scroll_thumb.dart'; import 'package:aves/widgets/common/scroll_thumb.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class ThumbnailCollection extends StatelessWidget { class ThumbnailCollection extends StatelessWidget {
final Widget appBar; final ValueNotifier<PageState> stateNotifier;
final WidgetBuilder emptyBuilder;
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<int> _columnCountNotifier = ValueNotifier(4); final ValueNotifier<int> _columnCountNotifier = ValueNotifier(4);
final GlobalKey _scrollableKey = GlobalKey(); final GlobalKey _scrollableKey = GlobalKey();
ThumbnailCollection({ ThumbnailCollection({
Key key, Key key,
this.appBar, @required this.stateNotifier,
this.emptyBuilder,
}) : super(key: key); }) : super(key: key);
@override @override
@ -27,16 +29,6 @@ class ThumbnailCollection extends StatelessWidget {
final sectionKeys = sections.keys.toList(); final sectionKeys = sections.keys.toList();
final showHeaders = collection.showHeaders; final showHeaders = collection.showHeaders;
double topPadding = 0;
if (appBar != null) {
final topWidget = appBar;
if (topWidget is PreferredSizeWidget) {
topPadding = topWidget.preferredSize.height;
} else if (topWidget is SliverAppBar) {
topPadding = kToolbarHeight + (topWidget.bottom?.preferredSize?.height ?? 0.0);
}
}
return SafeArea( return SafeArea(
child: Selector<MediaQueryData, double>( child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom, selector: (c, mq) => mq.viewInsets.bottom,
@ -44,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget {
return GridScaleGestureDetector( return GridScaleGestureDetector(
scrollableKey: _scrollableKey, scrollableKey: _scrollableKey,
columnCountNotifier: _columnCountNotifier, columnCountNotifier: _columnCountNotifier,
child: ValueListenableBuilder( child: ValueListenableBuilder<int>(
valueListenable: _columnCountNotifier, valueListenable: _columnCountNotifier,
builder: (context, columnCount, child) { builder: (context, columnCount, child) {
debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}'); debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}');
@ -55,10 +47,14 @@ class ThumbnailCollection extends StatelessWidget {
// when there is no content and we use `SliverFillRemaining` // when there is no content and we use `SliverFillRemaining`
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null, physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [ slivers: [
if (appBar != null) appBar, CollectionAppBar(
if (collection.isEmpty && emptyBuilder != null) stateNotifier: stateNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
),
if (collection.isEmpty)
SliverFillRemaining( SliverFillRemaining(
child: emptyBuilder(context), child: EmptyContent(),
hasScrollBody: false, hasScrollBody: false,
), ),
...sectionKeys.map((sectionKey) => SectionSliver( ...sectionKeys.map((sectionKey) => SectionSliver(
@ -78,16 +74,22 @@ class ThumbnailCollection extends StatelessWidget {
], ],
); );
return DraggableScrollbar( return ValueListenableBuilder<double>(
heightScrollThumb: avesScrollThumbHeight, valueListenable: _appBarHeightNotifier,
backgroundColor: Colors.white, builder: (context, appBarHeight, child) {
scrollThumbBuilder: avesScrollThumbBuilder(), return DraggableScrollbar(
controller: PrimaryScrollController.of(context), heightScrollThumb: avesScrollThumbHeight,
padding: EdgeInsets.only( backgroundColor: Colors.white,
// padding to get scroll thumb below app bar, above nav bar scrollThumbBuilder: avesScrollThumbBuilder(),
top: topPadding, controller: PrimaryScrollController.of(context),
bottom: mqViewInsetsBottom, padding: EdgeInsets.only(
), // padding to keep scroll thumb between app bar above and nav bar below
top: appBarHeight,
bottom: mqViewInsetsBottom,
),
child: child,
);
},
child: scrollView, child: scrollView,
); );
}, },

View file

@ -395,7 +395,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
), ),
), ),
]; ];
return ValueListenableBuilder( return ValueListenableBuilder<Color>(
valueListenable: _backgroundColorNotifier, valueListenable: _backgroundColorNotifier,
builder: (context, backgroundColor, child) => Container( builder: (context, backgroundColor, child) => Container(
color: backgroundColor, color: backgroundColor,

View file

@ -45,7 +45,7 @@ class BasicSection extends StatelessWidget {
'URI': entry.uri ?? '?', 'URI': entry.uri ?? '?',
if (entry.path != null) 'Path': entry.path, if (entry.path != null) 'Path': entry.path,
}), }),
ValueListenableBuilder( ValueListenableBuilder<bool>(
valueListenable: entry.isFavouriteNotifier, valueListenable: entry.isFavouriteNotifier,
builder: (context, isFavourite, child) { builder: (context, isFavourite, child) {
final album = entry.directory; final album = entry.directory;

View file

@ -57,14 +57,14 @@ class _LocationSectionState extends State<LocationSection> {
} }
void _registerWidget(LocationSection widget) { void _registerWidget(LocationSection widget) {
entry.metadataChangeNotifier.addListener(_handleChange); widget.entry.metadataChangeNotifier.addListener(_handleChange);
entry.addressChangeNotifier.addListener(_handleChange); widget.entry.addressChangeNotifier.addListener(_handleChange);
widget.visibleNotifier.addListener(_handleChange); widget.visibleNotifier.addListener(_handleChange);
} }
void _unregisterWidget(LocationSection widget) { void _unregisterWidget(LocationSection widget) {
entry.metadataChangeNotifier.removeListener(_handleChange); widget.entry.metadataChangeNotifier.removeListener(_handleChange);
entry.addressChangeNotifier.removeListener(_handleChange); widget.entry.addressChangeNotifier.removeListener(_handleChange);
widget.visibleNotifier.removeListener(_handleChange); widget.visibleNotifier.removeListener(_handleChange);
} }

View file

@ -41,7 +41,7 @@ class FullscreenTopOverlay extends StatelessWidget {
const Spacer(), const Spacer(),
OverlayButton( OverlayButton(
scale: scale, scale: scale,
child: ValueListenableBuilder( child: ValueListenableBuilder<bool>(
valueListenable: entry.isFavouriteNotifier, valueListenable: entry.isFavouriteNotifier,
builder: (context, isFavourite, child) => Stack( builder: (context, isFavourite, child) => Stack(
alignment: Alignment.center, alignment: Alignment.center,