app bar review

This commit is contained in:
Thibault Deckers 2022-05-19 10:20:44 +09:00
parent d0f293abb2
commit 98e54f9dff
11 changed files with 303 additions and 172 deletions

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart';
@ -24,7 +23,7 @@ import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/favourite_toggler.dart';
import 'package:aves/widgets/common/sliver_app_bar_title.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/material.dart';
@ -46,14 +45,13 @@ class CollectionAppBar extends StatefulWidget {
State<CollectionAppBar> createState() => _CollectionAppBarState();
}
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = [];
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
late AnimationController _browseToSelectAnimation;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode();
late final Listenable _queryFocusRequestNotifier;
double _statusBarHeight = 0;
CollectionLens get collection => widget.collection;
@ -78,11 +76,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
_isSelectingNotifier.addListener(_onActivityChange);
_registerWidget(widget);
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateStatusBarHeight();
_onFilterChanged();
});
WidgetsBinding.instance.addPostFrameCallback((_) => _onFilterChanged());
}
@override
@ -101,7 +95,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@ -113,11 +106,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
widget.collection.filterChangeNotifier.removeListener(_onFilterChanged);
}
@override
void didChangeMetrics() {
_updateStatusBarHeight();
}
@override
Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
@ -133,15 +121,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
builder: (context, queryEnabled, child) {
return Selector<Settings, List<EntrySetAction>>(
selector: (context, s) => s.collectionBrowsingQuickActions,
builder: (context, _, child) => SliverAppBar(
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
title: SliverAppBarTitleWrapper(
child: _buildAppBarTitle(isSelecting),
builder: (context, _, child) {
return AvesAppBar(
contentHeight: appBarContentHeight,
leading: _buildAppBarLeading(
hasDrawer: appMode.hasDrawer,
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
actions: _buildActions(selection),
bottom: PreferredSize(
preferredSize: Size.fromHeight(appBarBottomHeight),
child: Column(
bottom: Column(
children: [
if (showFilterBar)
FilterBar(
@ -156,10 +145,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
)
],
),
),
titleSpacing: 0,
floating: true,
),
transitionKey: isSelecting,
);
},
);
},
);
@ -167,12 +155,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
}
double get appBarBottomHeight {
double get appBarContentHeight {
final hasQuery = context.read<Query>().enabled;
return (showFilterBar ? FilterBar.preferredHeight : .0) + (hasQuery ? EntryQueryBar.preferredHeight : .0);
return kToolbarHeight + (showFilterBar ? FilterBar.preferredHeight : .0) + (hasQuery ? EntryQueryBar.preferredHeight : .0);
}
Widget _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) {
if (!hasDrawer) {
return const CloseButton();
}
Widget _buildAppBarLeading(bool isSelecting) {
VoidCallback? onPressed;
String? tooltip;
if (isSelecting) {
@ -200,11 +192,21 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
if (isSelecting) {
return Selector<Selection<AvesEntry>, int>(
selector: (context, selection) => selection.selectedItems.length,
builder: (context, count, child) => Text(count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count)),
builder: (context, count, child) => Text(
count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
} else {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
Widget title = Text(appMode.isPickingMedia ? l10n.collectionPickPageTitle : (isTrash ? l10n.binPageTitle : l10n.collectionPageTitle));
Widget title = Text(
appMode.isPickingMedia ? l10n.collectionPickPageTitle : (isTrash ? l10n.binPageTitle : l10n.collectionPageTitle),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
);
if (appMode == AppMode.main) {
title = SourceStateAwareAppBarTitle(
title: title,
@ -430,13 +432,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
void _updateStatusBarHeight() {
_statusBarHeight = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio).top;
_updateAppBarHeight();
}
void _updateAppBarHeight() {
widget.appBarHeightNotifier.value = _statusBarHeight + kToolbarHeight + appBarBottomHeight;
widget.appBarHeightNotifier.value = AvesAppBar.appBarHeightForContentHeight(appBarContentHeight);
}
Future<void> _onActionSelected(EntrySetAction action) async {

View file

@ -76,7 +76,7 @@ class CollectionSectionHeader extends StatelessWidget {
}
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
headerExtent = max(headerExtent, SectionHeader.leadingDimension * textScaleFactor) + SectionHeader.padding.vertical;
headerExtent = max(headerExtent, SectionHeader.leadingSize.height * textScaleFactor) + SectionHeader.padding.vertical;
return headerExtent;
}
}

View file

@ -18,7 +18,6 @@ class InteractiveAppBarTitle extends StatelessWidget {
// so that we can also detect taps around the title `Text`
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing),
color: Colors.transparent,
height: kToolbarHeight,
child: child,

View file

@ -49,7 +49,9 @@ class _QueryBarState extends State<QueryBar> {
tooltip: context.l10n.clearTooltip,
);
return Row(
return DefaultTextStyle(
style: Theme.of(context).textTheme.bodyText2!,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
@ -86,8 +88,9 @@ class _QueryBarState extends State<QueryBar> {
child: value.text.isNotEmpty ? clearButton : const SizedBox(),
),
),
)
),
],
),
);
}
}

View file

@ -24,7 +24,7 @@ class SectionHeader<T> extends StatelessWidget {
this.selectable = true,
});
static const leadingDimension = 32.0;
static const leadingSize = Size(48, 32);
static const padding = EdgeInsets.all(16);
static const widgetSpanAlignment = PlaceholderAlignment.middle;
@ -33,7 +33,7 @@ class SectionHeader<T> extends StatelessWidget {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: padding,
constraints: const BoxConstraints(minHeight: leadingDimension),
constraints: BoxConstraints(minHeight: leadingSize.height),
child: GestureDetector(
onTap: selectable ? () => _toggleSectionSelection(context) : null,
child: Text.rich(
@ -47,8 +47,8 @@ class SectionHeader<T> extends StatelessWidget {
browsingBuilder: leading != null
? (context) => Container(
padding: const EdgeInsetsDirectional.only(end: 8, bottom: 4),
width: leadingDimension,
height: leadingDimension,
width: leadingSize.width,
height: leadingSize.height,
child: leading,
)
: null,
@ -136,8 +136,6 @@ class _SectionSelectableLeading<T> extends StatelessWidget {
required this.onPressed,
});
static const leadingDimension = SectionHeader.leadingDimension;
@override
Widget build(BuildContext context) {
if (!selectable) return _buildBrowsing(context);
@ -174,7 +172,7 @@ class _SectionSelectableLeading<T> extends StatelessWidget {
);
}
Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? const SizedBox(height: leadingDimension);
Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? SizedBox(height: SectionHeader.leadingSize.height);
}
class _SectionSelectingLeading<T> extends StatelessWidget {
@ -207,15 +205,14 @@ class _SectionSelectingLeading<T> extends StatelessWidget {
),
child: IconButton(
iconSize: 26,
padding: const EdgeInsets.only(top: 1),
alignment: AlignmentDirectional.topStart,
icon: Icon(isSelected ? AIcons.selected : AIcons.unselected),
padding: const EdgeInsetsDirectional.only(end: 6, bottom: 4),
onPressed: onPressed,
tooltip: isSelected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip,
constraints: const BoxConstraints(
minHeight: SectionHeader.leadingDimension,
minWidth: SectionHeader.leadingDimension,
constraints: BoxConstraints(
minHeight: SectionHeader.leadingSize.height,
minWidth: SectionHeader.leadingSize.width,
),
icon: Icon(isSelected ? AIcons.selected : AIcons.unselected),
),
),
);

View file

@ -0,0 +1,138 @@
import 'dart:ui';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AvesAppBar extends StatelessWidget {
final double contentHeight;
final Widget leading;
final Widget title;
final List<Widget> actions;
final Widget? bottom;
final Object? transitionKey;
const AvesAppBar({
super.key,
required this.contentHeight,
required this.leading,
required this.title,
required this.actions,
this.bottom,
this.transitionKey,
});
@override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
pinned: false,
delegate: _SliverAppBarDelegate(
height: appBarHeightForContentHeight(contentHeight),
child: SafeArea(
bottom: false,
child: AvesFloatingBar(
builder: (context, backgroundColor) => Material(
color: backgroundColor,
textStyle: Theme.of(context).appBarTheme.titleTextStyle,
child: Column(
children: [
SizedBox(
height: kToolbarHeight,
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: leading,
),
Expanded(
child: AnimatedSwitcher(
duration: context.read<DurationsData>().iconAnimation,
child: Row(
key: ValueKey(transitionKey),
children: [
Expanded(child: title),
...actions,
],
),
),
),
],
),
),
if (bottom != null) bottom!,
],
),
),
),
),
),
);
}
static double appBarHeightForContentHeight(double contentHeight) {
final topPadding = window.padding.top / window.devicePixelRatio;
return topPadding + AvesFloatingBar.margin.vertical + contentHeight;
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double height;
final Widget child;
const _SliverAppBarDelegate({
required this.height,
required this.child,
});
@override
double get minExtent => height;
@override
double get maxExtent => height;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;
@override
bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) => true;
}
class AvesFloatingBar extends StatelessWidget {
final Widget Function(BuildContext context, Color backgroundColor) builder;
const AvesFloatingBar({
super.key,
required this.builder,
});
static const margin = EdgeInsets.all(8);
static const borderRadius = BorderRadius.all(Radius.circular(8));
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final backgroundColor = theme.appBarTheme.backgroundColor!;
final blurred = context.select<Settings, bool>((s) => s.enableOverlayBlurEffect);
return Container(
foregroundDecoration: BoxDecoration(
border: Border.all(
color: theme.dividerColor,
),
borderRadius: borderRadius,
),
margin: margin,
child: BlurredRRect(
enabled: blurred,
borderRadius: borderRadius,
child: builder(
context,
blurred ? backgroundColor.withOpacity(.85) : backgroundColor,
),
),
);
}
}

View file

@ -47,6 +47,7 @@ class _ItemPickDialogState extends State<ItemPickDialog> {
initialQuery: liveFilter?.query,
child: GestureAreaProtectorStack(
child: SafeArea(
top: false,
bottom: false,
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,

View file

@ -13,10 +13,10 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/basic/query_bar.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/common/sliver_app_bar_title.dart';
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
@ -144,18 +144,22 @@ class _AlbumPickAppBar extends StatelessWidget {
}
}
return SliverAppBar(
return AvesAppBar(
contentHeight: preferredHeight,
leading: const BackButton(),
title: SliverAppBarTitleWrapper(
child: SourceStateAwareAppBarTitle(
title: SourceStateAwareAppBarTitle(
title: Text(title()),
source: source,
),
),
actions: _buildActions(context),
bottom: _AlbumQueryBar(
queryNotifier: queryNotifier,
),
actions: [
);
}
List<StatelessWidget> _buildActions(BuildContext context) {
return [
if (moveType != null)
IconButton(
icon: const Icon(AIcons.add),
@ -193,9 +197,7 @@ class _AlbumPickAppBar extends StatelessWidget {
},
),
),
],
floating: true,
);
];
}
}

View file

@ -8,7 +8,7 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/sliver_app_bar_title.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/material.dart';
@ -73,18 +73,23 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
final selection = context.watch<Selection<FilterGridItem<T>>>();
final isSelecting = selection.isSelecting;
_isSelectingNotifier.value = isSelecting;
return SliverAppBar(
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
title: SliverAppBarTitleWrapper(
child: _buildAppBarTitle(isSelecting),
return AvesAppBar(
contentHeight: kToolbarHeight,
leading: _buildAppBarLeading(
hasDrawer: appMode.hasDrawer,
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
actions: _buildActions(appMode, selection),
titleSpacing: 0,
floating: true,
transitionKey: isSelecting,
);
}
Widget _buildAppBarLeading(bool isSelecting) {
Widget _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) {
if (!hasDrawer) {
return const CloseButton();
}
VoidCallback? onPressed;
String? tooltip;
if (isSelecting) {

View file

@ -21,6 +21,6 @@ class FilterChipSectionHeader<T> extends StatelessWidget {
static double getPreferredHeight(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
return SectionHeader.leadingDimension * textScaleFactor + SectionHeader.padding.vertical;
return SectionHeader.leadingSize.height * textScaleFactor + SectionHeader.padding.vertical;
}
}

View file

@ -7,7 +7,7 @@ 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/media_query.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/navigation/nav_bar/floating.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_item.dart';
@ -27,17 +27,11 @@ class AppBottomNavBar extends StatelessWidget {
this.currentCollection,
});
static const padding = EdgeInsets.all(8);
static double get height => kBottomNavigationBarHeight + padding.vertical;
static double get height => kBottomNavigationBarHeight + AvesFloatingBar.margin.vertical;
@override
Widget build(BuildContext context) {
const borderRadius = BorderRadius.all(Radius.circular(8));
final blurred = context.select<Settings, bool>((s) => s.enableOverlayBlurEffect);
final showVideo = context.select<Settings, bool>((s) => !s.hiddenFilters.contains(MimeFilter.video));
final backgroundColor = Theme.of(context).canvasColor;
final items = [
const AvesBottomNavItem(route: CollectionPage.routeName),
@ -46,12 +40,8 @@ class AppBottomNavBar extends StatelessWidget {
const AvesBottomNavItem(route: AlbumListPage.routeName),
];
Widget child = Padding(
padding: padding,
child: BlurredRRect(
enabled: blurred,
borderRadius: borderRadius,
child: BottomNavigationBar(
Widget child = AvesFloatingBar(
builder: (context, backgroundColor) => BottomNavigationBar(
items: items
.map((item) => BottomNavigationBarItem(
icon: item.icon(context),
@ -61,11 +51,10 @@ class AppBottomNavBar extends StatelessWidget {
onTap: (index) => _goTo(context, items, index),
currentIndex: _getCurrentIndex(context, items),
type: BottomNavigationBarType.fixed,
backgroundColor: blurred ? backgroundColor.withOpacity(.85) : backgroundColor,
backgroundColor: backgroundColor,
showSelectedLabels: false,
showUnselectedLabels: false,
),
),
);
return Hero(