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
static const double thumbnailCacheExtent = 50;

View file

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

View file

@ -1,7 +1,5 @@
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/empty.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/foundation.dart';
@ -11,7 +9,9 @@ import 'package:provider/provider.dart';
class CollectionPage extends StatelessWidget {
final CollectionLens collection;
const CollectionPage(this.collection);
final ValueNotifier<PageState> _stateNotifier = ValueNotifier(PageState.browse);
CollectionPage(this.collection);
@override
Widget build(BuildContext context) {
@ -20,7 +20,18 @@ class CollectionPage extends StatelessWidget {
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,
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(
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 }

View file

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

View file

@ -1,22 +1,24 @@
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_section.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/scroll_thumb.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ThumbnailCollection extends StatelessWidget {
final Widget appBar;
final WidgetBuilder emptyBuilder;
final ValueNotifier<PageState> stateNotifier;
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<int> _columnCountNotifier = ValueNotifier(4);
final GlobalKey _scrollableKey = GlobalKey();
ThumbnailCollection({
Key key,
this.appBar,
this.emptyBuilder,
@required this.stateNotifier,
}) : super(key: key);
@override
@ -27,16 +29,6 @@ class ThumbnailCollection extends StatelessWidget {
final sectionKeys = sections.keys.toList();
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(
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom,
@ -44,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget {
return GridScaleGestureDetector(
scrollableKey: _scrollableKey,
columnCountNotifier: _columnCountNotifier,
child: ValueListenableBuilder(
child: ValueListenableBuilder<int>(
valueListenable: _columnCountNotifier,
builder: (context, columnCount, child) {
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`
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [
if (appBar != null) appBar,
if (collection.isEmpty && emptyBuilder != null)
CollectionAppBar(
stateNotifier: stateNotifier,
appBarHeightNotifier: _appBarHeightNotifier,
collection: collection,
),
if (collection.isEmpty)
SliverFillRemaining(
child: emptyBuilder(context),
child: EmptyContent(),
hasScrollBody: false,
),
...sectionKeys.map((sectionKey) => SectionSliver(
@ -78,16 +74,22 @@ class ThumbnailCollection extends StatelessWidget {
],
);
return DraggableScrollbar(
heightScrollThumb: avesScrollThumbHeight,
backgroundColor: Colors.white,
scrollThumbBuilder: avesScrollThumbBuilder(),
controller: PrimaryScrollController.of(context),
padding: EdgeInsets.only(
// padding to get scroll thumb below app bar, above nav bar
top: topPadding,
bottom: mqViewInsetsBottom,
),
return ValueListenableBuilder<double>(
valueListenable: _appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return DraggableScrollbar(
heightScrollThumb: avesScrollThumbHeight,
backgroundColor: Colors.white,
scrollThumbBuilder: avesScrollThumbBuilder(),
controller: PrimaryScrollController.of(context),
padding: EdgeInsets.only(
// padding to keep scroll thumb between app bar above and nav bar below
top: appBarHeight,
bottom: mqViewInsetsBottom,
),
child: child,
);
},
child: scrollView,
);
},

View file

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

View file

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

View file

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

View file

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