filter bar: animate chip removal
This commit is contained in:
parent
ff9420fce7
commit
e26f2b4fb6
6 changed files with 121 additions and 38 deletions
|
@ -136,7 +136,7 @@ class LicenseRow extends StatelessWidget {
|
|||
children: [
|
||||
Text(
|
||||
package.name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
|
|
|
@ -93,7 +93,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
leading: _buildAppBarLeading(),
|
||||
title: _buildAppBarTitle(),
|
||||
actions: _buildActions(),
|
||||
bottom: hasFilters ? FilterBar() : null,
|
||||
bottom: hasFilters
|
||||
? FilterBar(
|
||||
filters: collection.filters,
|
||||
onPressed: collection.removeFilter,
|
||||
)
|
||||
: null,
|
||||
floating: true,
|
||||
),
|
||||
);
|
||||
|
@ -141,8 +146,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
child: child,
|
||||
sizeFactor: animation,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: sourceState == SourceState.ready
|
||||
|
@ -337,12 +342,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _goToSearch() async {
|
||||
final filter = await showSearch(
|
||||
void _goToSearch() {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: ImageSearchDelegate(collection.source),
|
||||
delegate: ImageSearchDelegate(collection.source, collection.addFilter),
|
||||
);
|
||||
collection.addFilter(filter);
|
||||
}
|
||||
|
||||
Future<void> _goToStats() {
|
||||
|
|
|
@ -1,48 +1,113 @@
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilterBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
class FilterBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
static const double verticalPadding = 16;
|
||||
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
|
||||
|
||||
final List<CollectionFilter> filters;
|
||||
final FilterCallback onPressed;
|
||||
|
||||
FilterBar({
|
||||
Key key,
|
||||
@required Set<CollectionFilter> filters,
|
||||
@required this.onPressed,
|
||||
}) : filters = List.from(filters)..sort(),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
final Size preferredSize = const Size.fromHeight(preferredHeight);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final collection = Provider.of<CollectionLens>(context);
|
||||
final filters = collection.filters.toList()..sort();
|
||||
_FilterBarState createState() => _FilterBarState();
|
||||
}
|
||||
|
||||
class _FilterBarState extends State<FilterBar> {
|
||||
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey();
|
||||
CollectionFilter _userRemovedFilter;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FilterBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final current = widget.filters;
|
||||
final existing = oldWidget.filters;
|
||||
final removed = existing.where((filter) => !current.contains(filter)).toList();
|
||||
final added = current.where((filter) => !existing.contains(filter)).toList();
|
||||
final listState = _animatedListKey.currentState;
|
||||
removed.forEach((filter) {
|
||||
final index = existing.indexOf(filter);
|
||||
existing.removeAt(index);
|
||||
// only animate item removal when triggered by a user interaction with the chip,
|
||||
// not from automatic chip replacement following chip selection
|
||||
final animate = _userRemovedFilter == filter;
|
||||
listState.removeItem(
|
||||
index,
|
||||
animate
|
||||
? (context, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
sizeFactor: animation,
|
||||
child: ScaleTransition(
|
||||
scale: animation,
|
||||
child: _buildChip(filter),
|
||||
),
|
||||
),
|
||||
)
|
||||
: (context, animation) => _buildChip(filter),
|
||||
duration: animate ? const Duration(milliseconds: 200) : Duration.zero,
|
||||
);
|
||||
});
|
||||
added.forEach((filter) {
|
||||
final index = current.indexOf(filter);
|
||||
listState.insertItem(
|
||||
index,
|
||||
duration: Duration.zero,
|
||||
);
|
||||
});
|
||||
_userRemovedFilter = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
// specify transparent as a workaround to prevent
|
||||
// chip border clipping when the floating app bar is fading
|
||||
color: Colors.transparent,
|
||||
height: preferredSize.height,
|
||||
height: FilterBar.preferredHeight,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
// cancel notification bubbling so that the draggable scrollbar
|
||||
// does not misinterpret filter bar scrolling for collection scrolling
|
||||
onNotification: (notification) => true,
|
||||
child: ListView.separated(
|
||||
child: AnimatedList(
|
||||
key: _animatedListKey,
|
||||
initialItemCount: widget.filters.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= filters.length) return null;
|
||||
final filter = filters[index];
|
||||
return Center(
|
||||
child: AvesFilterChip(
|
||||
key: ValueKey(filter),
|
||||
filter: filter,
|
||||
removable: true,
|
||||
heroType: HeroType.always,
|
||||
onPressed: collection.removeFilter,
|
||||
),
|
||||
);
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
itemBuilder: (context, index, animation) {
|
||||
if (index >= widget.filters.length) return null;
|
||||
return _buildChip(widget.filters.toList()[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildChip(CollectionFilter filter) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Center(
|
||||
child: AvesFilterChip(
|
||||
key: ValueKey(filter),
|
||||
filter: filter,
|
||||
removable: true,
|
||||
heroType: HeroType.always,
|
||||
onPressed: (filter) {
|
||||
_userRemovedFilter = filter;
|
||||
widget.onPressed(filter);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemCount: filters.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -218,8 +218,8 @@ class SectionSelectableLeading extends StatelessWidget {
|
|||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeOutBack,
|
||||
transitionBuilder: (child, animation) => ScaleTransition(
|
||||
child: child,
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
@ -232,16 +232,16 @@ class SectionSelectableLeading extends StatelessWidget {
|
|||
switchOutCurve: Curves.easeInOut,
|
||||
transitionBuilder: (child, animation) {
|
||||
Widget transition = ScaleTransition(
|
||||
child: child,
|
||||
scale: animation,
|
||||
child: child,
|
||||
);
|
||||
if (browsingBuilder == null) {
|
||||
// when switching with a header that has no icon,
|
||||
// we also transition the size for a smooth push to the text
|
||||
transition = SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
child: transition,
|
||||
sizeFactor: animation,
|
||||
child: transition,
|
||||
);
|
||||
}
|
||||
return transition;
|
||||
|
|
|
@ -8,14 +8,16 @@ import 'package:aves/model/filters/query.dart';
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/mime_types.dart';
|
||||
import 'package:aves/widgets/album/search/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
||||
final CollectionSource source;
|
||||
final ValueNotifier<String> expandedSectionNotifier = ValueNotifier(null);
|
||||
final FilterCallback onSelection;
|
||||
|
||||
ImageSearchDelegate(this.source);
|
||||
ImageSearchDelegate(this.source, this.onSelection);
|
||||
|
||||
@override
|
||||
ThemeData appBarTheme(BuildContext context) {
|
||||
|
@ -29,7 +31,7 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () => close(context, null),
|
||||
onPressed: () => _select(context, null),
|
||||
tooltip: 'Back',
|
||||
);
|
||||
}
|
||||
|
@ -118,7 +120,7 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
title: title,
|
||||
filters: filters,
|
||||
expandedNotifier: expandedSectionNotifier,
|
||||
onPressed: (filter) => close(context, filter is QueryFilter ? QueryFilter(filter.query) : filter),
|
||||
onPressed: (filter) => _select(context, filter is QueryFilter ? QueryFilter(filter.query) : filter),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -128,7 +130,7 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
// `buildResults` is called in the build phase,
|
||||
// so we post the call that will filter the collection
|
||||
// and possibly trigger a rebuild here
|
||||
close(context, _buildQueryFilter(true));
|
||||
_select(context, _buildQueryFilter(true));
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
@ -137,4 +139,16 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
final cleanQuery = query.trim();
|
||||
return cleanQuery.isNotEmpty ? QueryFilter(cleanQuery, colorful: colorful) : null;
|
||||
}
|
||||
|
||||
void _select(BuildContext context, CollectionFilter filter) {
|
||||
if (filter != null) {
|
||||
onSelection(filter);
|
||||
}
|
||||
// we post closing the search page after applying the filter selection
|
||||
// so that hero animation target is ready in the `FilterBar`,
|
||||
// even when the target is a child of an `AnimatedList`
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
close(context, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,8 +81,8 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
|
|||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeOutBack,
|
||||
transitionBuilder: (child, animation) => ScaleTransition(
|
||||
child: child,
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue