#9 fast scroll feedback + staggered animation improvements

This commit is contained in:
Thibault Deckers 2021-03-22 19:47:41 +09:00
parent 81c9c8a757
commit 2a4722736b
19 changed files with 337 additions and 70 deletions

View file

@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
### Added
- Collection / Albums / Countries / Tags: added label when dragging scrollbar thumb
### Changed
- Upgraded Flutter to beta v2.1.0-12.2.pre

View file

@ -7,6 +7,12 @@
"@welcomeAnalyticsToggle": {},
"welcomeTermsToggle": "I agree to the terms and conditions",
"@welcomeTermsToggle": {},
"itemCount": "{count, plural, =1{1 item} other{{count} items}}",
"@itemCount": {
"placeholders": {
"count": {}
}
},
"applyButtonLabel": "APPLY",
"@applyButtonLabel": {},

View file

@ -3,6 +3,7 @@
"welcomeMessage": "아베스 사용을 환영합니다",
"welcomeAnalyticsToggle": "진단 데이터를 보내는 것에 동의합니다 (선택)",
"welcomeTermsToggle": "이용약관에 동의합니다",
"itemCount": "{count, plural, other{{count}개}}",
"applyButtonLabel": "확인",
"deleteButtonLabel": "삭제",

View file

@ -9,6 +9,7 @@ class Durations {
static const dialogTransitionAnimation = Duration(milliseconds: 150); // ref `transitionDuration` in `showDialog()`
static const staggeredAnimation = Duration(milliseconds: 375);
static const staggeredAnimationPageTarget = Duration(milliseconds: 900);
static const dialogFieldReachAnimation = Duration(milliseconds: 300);
static const appBarTitleAnimation = Duration(milliseconds: 300);

View file

@ -11,6 +11,7 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/app_bar.dart';
import 'package:aves/widgets/collection/draggable_thumb_label.dart';
import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/selector.dart';
import 'package:aves/widgets/collection/grid/thumbnail.dart';
@ -29,9 +30,9 @@ import 'package:aves/widgets/common/providers/tile_extent_controller_provider.da
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class CollectionGrid extends StatefulWidget {
final String settingsRouteKey;
@ -74,23 +75,34 @@ class _CollectionGridContent extends StatelessWidget {
builder: (context, tileExtent, child) {
return ThumbnailTheme(
extent: tileExtent,
child: SectionedEntryListLayoutProvider(
collection: collection,
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
tileExtent: tileExtent,
columnCount: context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)),
tileBuilder: (entry) => InteractiveThumbnail(
key: ValueKey(entry.contentId),
collection: collection,
entry: entry,
tileExtent: tileExtent,
isScrollingNotifier: _isScrollingNotifier,
),
child: _CollectionSectionedContent(
collection: collection,
isScrollingNotifier: _isScrollingNotifier,
scrollController: PrimaryScrollController.of(context),
),
child: Selector<TileExtentController, Tuple2<double, int>>(
selector: (context, c) => Tuple2(c.viewportSize.width, c.columnCount),
builder: (context, c, child) {
final scrollableWidth = c.item1;
final columnCount = c.item2;
// do not listen for animation delay change
final controller = Provider.of<TileExtentController>(context, listen: false);
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
return SectionedEntryListLayoutProvider(
collection: collection,
scrollableWidth: scrollableWidth,
tileExtent: tileExtent,
columnCount: columnCount,
tileBuilder: (entry) => InteractiveThumbnail(
key: ValueKey(entry.contentId),
collection: collection,
entry: entry,
tileExtent: tileExtent,
isScrollingNotifier: _isScrollingNotifier,
),
tileAnimationDelay: tileAnimationDelay,
child: _CollectionSectionedContent(
collection: collection,
isScrollingNotifier: _isScrollingNotifier,
scrollController: PrimaryScrollController.of(context),
),
);
},
),
);
},
@ -266,10 +278,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
@override
Widget build(BuildContext context) {
final scrollView = _buildScrollView(widget.appBar, widget.collection);
return _buildDraggableScrollView(scrollView);
return _buildDraggableScrollView(scrollView, widget.collection);
}
Widget _buildDraggableScrollView(ScrollView scrollView) {
Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) {
return ValueListenableBuilder<double>(
valueListenable: widget.appBarHeightNotifier,
builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>(
@ -287,6 +299,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
top: appBarHeight,
bottom: mqPaddingBottom,
),
labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel(
collection: collection,
offsetY: offsetY,
),
child: scrollView,
),
child: child,

View file

@ -0,0 +1,60 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CollectionDraggableThumbLabel extends StatelessWidget {
final CollectionLens collection;
final double offsetY;
const CollectionDraggableThumbLabel({
@required this.collection,
@required this.offsetY,
});
@override
Widget build(BuildContext context) {
return DraggableThumbLabel<AvesEntry>(
offsetY: offsetY,
lineBuilder: (context, entry) {
switch (collection.sortFactor) {
case EntrySortFactor.date:
switch (collection.groupFactor) {
case EntryGroupFactor.album:
return [
DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate),
if (_hasMultipleSections(context)) context.read<CollectionSource>().getUniqueAlbumName(context, entry.directory),
];
case EntryGroupFactor.month:
case EntryGroupFactor.none:
return [
DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate),
];
case EntryGroupFactor.day:
return [
DraggableThumbLabel.formatDayThumbLabel(context, entry.bestDate),
];
}
break;
case EntrySortFactor.name:
return [
if (_hasMultipleSections(context)) context.read<CollectionSource>().getUniqueAlbumName(context, entry.directory),
entry.bestTitle,
];
case EntrySortFactor.size:
return [
formatFilesize(entry.sizeBytes, round: 0),
];
}
return [];
},
);
}
bool _hasMultipleSections(BuildContext context) => context.read<SectionedListLayout<AvesEntry>>().sections.length > 1;
}

View file

@ -15,12 +15,14 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
@required int columnCount,
@required double tileExtent,
@required Widget Function(AvesEntry entry) tileBuilder,
@required Duration tileAnimationDelay,
@required Widget child,
}) : super(
scrollableWidth: scrollableWidth,
columnCount: columnCount,
tileExtent: tileExtent,
tileBuilder: tileBuilder,
tileAnimationDelay: tileAnimationDelay,
child: child,
);

View file

@ -62,7 +62,7 @@ class DraggableScrollbar extends StatefulWidget {
@required this.controller,
this.padding,
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
this.scrollbarTimeToFade = const Duration(milliseconds: 1000),
this.labelTextBuilder,
@required this.child,
}) : assert(controller != null),
@ -91,6 +91,7 @@ class DraggableScrollbar extends StatefulWidget {
backgroundColor: backgroundColor,
child: labelText,
),
SizedBox(width: 24),
scrollThumb,
],
);
@ -133,6 +134,7 @@ class ScrollLabel extends StatelessWidget {
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
bool _isDragInProcess = false;
Offset _longPressLastGlobalPosition;
AnimationController _thumbAnimationController;
Animation<double> _thumbAnimation;
@ -193,9 +195,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
),
RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onVerticalDragStart,
onVerticalDragUpdate: _onVerticalDragUpdate,
onVerticalDragEnd: _onVerticalDragEnd,
onLongPressStart: (details) {
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragStart();
},
onLongPressMoveUpdate: (details) {
final dy = (details.globalPosition - _longPressLastGlobalPosition).dy;
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragUpdate(dy);
},
onLongPressEnd: (_) => _onVerticalDragEnd(),
onVerticalDragStart: (_) => _onVerticalDragStart(),
onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy),
onVerticalDragEnd: (_) => _onVerticalDragEnd(),
child: ValueListenableBuilder(
valueListenable: _thumbOffsetNotifier,
builder: (context, thumbOffset, child) => Container(
@ -244,17 +256,18 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
}
}
void _onVerticalDragStart(DragStartDetails details) {
void _onVerticalDragStart() {
_labelAnimationController.forward();
_fadeoutTimer?.cancel();
_showThumb();
setState(() => _isDragInProcess = true);
}
void _onVerticalDragUpdate(DragUpdateDetails details) {
void _onVerticalDragUpdate(double deltaY) {
_showThumb();
if (_isDragInProcess) {
// thumb offset
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + details.delta.dy).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
// scroll offset
final min = controller.position.minScrollExtent;
@ -263,7 +276,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
}
}
void _onVerticalDragEnd(DragEndDetails details) {
void _onVerticalDragEnd() {
_scheduleFadeout();
setState(() => _isDragInProcess = false);
}

View file

@ -0,0 +1,67 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class DraggableThumbLabel<T> extends StatelessWidget {
final double offsetY;
final List<String> Function(BuildContext context, T item) lineBuilder;
const DraggableThumbLabel({
@required this.offsetY,
@required this.lineBuilder,
});
@override
Widget build(BuildContext context) {
final sll = context.read<SectionedListLayout<T>>();
final sectionLayout = sll.getSectionAt(offsetY);
if (sectionLayout == null) return null;
final section = sll.sections[sectionLayout.sectionKey];
final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent);
final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileExtent + sll.spacing)) * sll.columnCount;
final item = section[itemIndex];
if (item == null) return SizedBox();
final lines = lineBuilder(context, item);
if (lines.isEmpty) return SizedBox();
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 140),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: lines.length > 1
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: lines.map(_buildText).toList(),
)
: _buildText(lines.first),
),
);
}
Widget _buildText(String text) => Text(
text,
style: TextStyle(
color: Colors.black,
),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
);
static String formatMonthThumbLabel(BuildContext context, DateTime date) {
final l10n = context.l10n;
if (date == null) return l10n.sectionUnknown;
return DateFormat.yMMM(l10n.localeName).format(date);
}
static String formatDayThumbLabel(BuildContext context, DateTime date) {
final l10n = context.l10n;
if (date == null) return l10n.sectionUnknown;
return DateFormat.yMMMd(l10n.localeName).format(date);
}
}

View file

@ -12,6 +12,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final int columnCount;
final double spacing, tileExtent;
final Widget Function(T item) tileBuilder;
final Duration tileAnimationDelay;
final Widget child;
const SectionedListLayoutProvider({
@ -20,6 +21,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
this.spacing = 0,
@required this.tileExtent,
@required this.tileBuilder,
this.tileAnimationDelay,
@required this.child,
}) : assert(scrollableWidth != 0);
@ -40,6 +42,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final _showHeaders = showHeaders;
final _sections = sections;
final sectionKeys = _sections.keys.toList();
final animate = tileAnimationDelay > Duration.zero;
final sectionLayouts = <SectionLayout>[];
var currentIndex = 0, currentOffset = 0.0;
@ -76,6 +79,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
listIndex - sectionFirstIndex,
sectionKey,
headerExtent,
animate,
),
),
);
@ -97,10 +101,11 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
int sectionChildIndex,
SectionKey sectionKey,
double headerExtent,
bool animate,
) {
if (sectionChildIndex == 0) {
final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink();
return _buildAnimation(sectionGridIndex, header);
return animate ? _buildAnimation(sectionGridIndex, header) : header;
}
sectionChildIndex--;
@ -113,7 +118,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final itemGridIndex = sectionGridIndex + i - minItemIndex;
final item = tileBuilder(section[i]);
if (i != minItemIndex) children.add(SizedBox(width: spacing));
children.add(_buildAnimation(itemGridIndex, item));
children.add(animate ? _buildAnimation(itemGridIndex, item) : item);
}
return Row(
mainAxisSize: MainAxisSize.min,
@ -126,7 +131,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
position: index,
columnCount: columnCount,
duration: Durations.staggeredAnimation,
delay: Durations.staggeredAnimationDelay,
delay: tileAnimationDelay ?? Durations.staggeredAnimationDelay,
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
@ -189,9 +194,11 @@ class SectionedListLayout<T> {
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
}
SectionLayout getSectionAt(double offsetY) => sectionLayouts.firstWhere((sl) => offsetY < sl.maxOffset, orElse: () => null);
T getItemAt(Offset position) {
var dy = position.dy;
final sectionLayout = sectionLayouts.firstWhere((sl) => dy < sl.maxOffset, orElse: () => null);
final sectionLayout = getSectionAt(dy);
if (sectionLayout == null) return null;
final section = sections[sectionLayout.sectionKey];

View file

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:aves/model/settings/settings.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
class TileExtentController {
@ -45,7 +46,7 @@ class TileExtentController {
? oldUserPreferredExtent
: currentExtent;
final columnCount = getEffectiveColumnCountForExtent(targetExtent);
final columnCount = _effectiveColumnCountForExtent(targetExtent);
final newExtent = _extentForColumnCount(columnCount);
if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) {
@ -67,15 +68,24 @@ class TileExtentController {
int _effectiveColumnCountMax() => _columnCountForExtent(extentMin).floor();
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
int getEffectiveColumnCountForExtent(double extent) {
int _effectiveColumnCountForExtent(double extent) {
if (extent > 0) {
final columnCount = _columnCountForExtent(extent);
return columnCount.clamp(_effectiveColumnCountMin(), _effectiveColumnCountMax()).round();
}
return columnCountDefault;
}
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
int get columnCount => _effectiveColumnCountForExtent(extentNotifier.value);
Duration getTileAnimationDelay(Duration pageTarget) {
final extent = extentNotifier.value;
final columnCount = ((viewportSize.width + spacing) / (extent + spacing)).round();
final rowCount = (viewportSize.height + spacing) ~/ (extent + spacing);
return pageTarget ~/ (columnCount + rowCount) * timeDilation;
}
}

View file

@ -62,6 +62,7 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
appBar: appBar,
appBarHeight: AlbumPickAppBar.preferredHeight,
filterSections: AlbumListPage.getAlbumEntries(context, source),
sortFactor: settings.albumSortFactor,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
queryNotifier: _queryNotifier,
applyQuery: (filters, query) {

View file

@ -34,6 +34,7 @@ class AlbumListPage extends StatelessWidget {
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
source: source,
title: context.l10n.albumPageTitle,
sortFactor: settings.albumSortFactor,
groupable: true,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
chipSetActionDelegate: AlbumChipSetActionDelegate(),

View file

@ -0,0 +1,42 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FilterDraggableThumbLabel<T extends CollectionFilter> extends StatelessWidget {
final ChipSortFactor sortFactor;
final double offsetY;
const FilterDraggableThumbLabel({
@required this.sortFactor,
@required this.offsetY,
});
@override
Widget build(BuildContext context) {
return DraggableThumbLabel<FilterGridItem<T>>(
offsetY: offsetY,
lineBuilder: (context, filterGridItem) {
switch (sortFactor) {
case ChipSortFactor.count:
return [
context.l10n.itemCount(context.read<CollectionSource>().count(filterGridItem.filter)),
];
break;
case ChipSortFactor.date:
return [
DraggableThumbLabel.formatMonthThumbLabel(context, filterGridItem.entry.bestDate),
];
case ChipSortFactor.name:
return [
filterGridItem.filter.getLabel(context),
];
}
return [];
},
);
}
}

View file

@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart';
@ -20,12 +21,14 @@ import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/drawer/app_drawer.dart';
import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart';
import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart';
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
import 'package:aves/widgets/filter_grids/common/section_layout.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
typedef QueryTest<T extends CollectionFilter> = Iterable<FilterGridItem<T>> Function(Iterable<FilterGridItem<T>> filters, String query);
@ -34,6 +37,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final ChipSortFactor sortFactor;
final bool showHeaders;
final ValueNotifier<String> queryNotifier;
final QueryTest<T> applyQuery;
@ -47,6 +51,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
@required this.appBar,
this.appBarHeight = kToolbarHeight,
@required this.filterSections,
@required this.sortFactor,
@required this.showHeaders,
@required this.queryNotifier,
this.applyQuery,
@ -72,6 +77,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
appBar: appBar,
appBarHeight: appBarHeight,
filterSections: filterSections,
sortFactor: sortFactor,
showHeaders: showHeaders,
queryNotifier: queryNotifier,
applyQuery: applyQuery,
@ -95,6 +101,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final ChipSortFactor sortFactor;
final bool showHeaders;
final ValueNotifier<String> queryNotifier;
final QueryTest<T> applyQuery;
@ -108,6 +115,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
@required this.appBar,
@required this.appBarHeight,
@required this.filterSections,
@required this.sortFactor,
@required this.showHeaders,
@required this.queryNotifier,
@required this.applyQuery,
@ -137,6 +145,7 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
appBar: widget.appBar,
appBarHeight: widget.appBarHeight,
filterSections: widget.filterSections,
sortFactor: widget.sortFactor,
showHeaders: widget.showHeaders,
queryNotifier: widget.queryNotifier,
applyQuery: widget.applyQuery,
@ -151,6 +160,7 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
final Widget appBar;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final ChipSortFactor sortFactor;
final bool showHeaders;
final ValueNotifier<String> queryNotifier;
final Widget Function() emptyBuilder;
@ -165,6 +175,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
@required this.appBar,
@required double appBarHeight,
@required this.filterSections,
@required this.sortFactor,
@required this.showHeaders,
@required this.queryNotifier,
@required this.applyQuery,
@ -197,38 +208,48 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, tileExtent, child) {
final columnCount = context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent));
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
return SectionedFilterListLayoutProvider<T>(
sections: visibleFilterSections,
showHeaders: showHeaders,
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
tileExtent: tileExtent,
columnCount: columnCount,
spacing: tileSpacing,
tileBuilder: (gridItem) {
final filter = gridItem.filter;
final entry = gridItem.entry;
return MetaData(
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
child: DecoratedFilterChip(
key: Key(filter.key),
filter: filter,
extent: tileExtent,
pinned: pinnedFilters.contains(filter),
onTap: onTap,
onLongPress: onLongPress,
),
);
},
child: _FilterSectionedContent<T>(
appBar: appBar,
appBarHeightNotifier: _appBarHeightNotifier,
visibleFilterSections: visibleFilterSections,
emptyBuilder: emptyBuilder,
scrollController: PrimaryScrollController.of(context),
),
);
return Selector<TileExtentController, Tuple3<double, int, double>>(
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
builder: (context, c, child) {
final scrollableWidth = c.item1;
final columnCount = c.item2;
final tileSpacing = c.item3;
// do not listen for animation delay change
final controller = Provider.of<TileExtentController>(context, listen: false);
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
return SectionedFilterListLayoutProvider<T>(
sections: visibleFilterSections,
showHeaders: showHeaders,
scrollableWidth: scrollableWidth,
tileExtent: tileExtent,
columnCount: columnCount,
spacing: tileSpacing,
tileBuilder: (gridItem) {
final filter = gridItem.filter;
final entry = gridItem.entry;
return MetaData(
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
child: DecoratedFilterChip(
key: Key(filter.key),
filter: filter,
extent: tileExtent,
pinned: pinnedFilters.contains(filter),
onTap: onTap,
onLongPress: onLongPress,
),
);
},
tileAnimationDelay: tileAnimationDelay,
child: _FilterSectionedContent<T>(
appBar: appBar,
appBarHeightNotifier: _appBarHeightNotifier,
visibleFilterSections: visibleFilterSections,
sortFactor: sortFactor,
emptyBuilder: emptyBuilder,
scrollController: PrimaryScrollController.of(context),
),
);
});
},
);
return sectionedListLayoutProvider;
@ -241,6 +262,7 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
final Widget appBar;
final ValueNotifier<double> appBarHeightNotifier;
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
final ChipSortFactor sortFactor;
final Widget Function() emptyBuilder;
final ScrollController scrollController;
@ -248,6 +270,7 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
@required this.appBar,
@required this.appBarHeightNotifier,
@required this.visibleFilterSections,
@required this.sortFactor,
@required this.emptyBuilder,
@required this.scrollController,
});
@ -282,6 +305,7 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
scrollableKey: _scrollableKey,
appBar: appBar,
appBarHeightNotifier: appBarHeightNotifier,
sortFactor: widget.sortFactor,
emptyBuilder: emptyBuilder,
scrollController: scrollController,
),
@ -380,6 +404,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
final GlobalKey scrollableKey;
final Widget appBar;
final ValueNotifier<double> appBarHeightNotifier;
final ChipSortFactor sortFactor;
final Widget Function() emptyBuilder;
final ScrollController scrollController;
@ -387,6 +412,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
@required this.scrollableKey,
@required this.appBar,
@required this.appBarHeightNotifier,
@required this.sortFactor,
@required this.emptyBuilder,
@required this.scrollController,
});
@ -413,6 +439,10 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
top: appBarHeightNotifier.value,
bottom: mqPaddingBottom,
),
labelTextBuilder: (offsetY) => FilterDraggableThumbLabel<T>(
sortFactor: sortFactor,
offsetY: offsetY,
),
child: scrollView,
),
);

View file

@ -28,6 +28,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source;
final String title;
final ChipSetActionDelegate chipSetActionDelegate;
final ChipSortFactor sortFactor;
final bool groupable, showHeaders;
final ChipActionDelegate chipActionDelegate;
final List<ChipAction> Function(T filter) chipActionsBuilder;
@ -37,6 +38,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
const FilterNavigationPage({
@required this.source,
@required this.title,
@required this.sortFactor,
this.groupable = false,
this.showHeaders = false,
@required this.chipSetActionDelegate,
@ -64,6 +66,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
floating: true,
),
filterSections: filterSections,
sortFactor: sortFactor,
showHeaders: showHeaders,
queryNotifier: ValueNotifier(''),
emptyBuilder: () => ValueListenableBuilder<SourceState>(

View file

@ -14,6 +14,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
double spacing = 0,
@required double tileExtent,
@required Widget Function(FilterGridItem<T> gridItem) tileBuilder,
@required Duration tileAnimationDelay,
@required Widget child,
}) : super(
scrollableWidth: scrollableWidth,
@ -21,6 +22,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
spacing: spacing,
tileExtent: tileExtent,
tileBuilder: tileBuilder,
tileAnimationDelay: tileAnimationDelay,
child: child,
);

View file

@ -31,6 +31,7 @@ class CountryListPage extends StatelessWidget {
builder: (context, snapshot) => FilterNavigationPage<LocationFilter>(
source: source,
title: context.l10n.countryPageTitle,
sortFactor: settings.countrySortFactor,
chipSetActionDelegate: CountryChipSetActionDelegate(),
chipActionDelegate: ChipActionDelegate(),
chipActionsBuilder: (filter) => [

View file

@ -31,6 +31,7 @@ class TagListPage extends StatelessWidget {
builder: (context, snapshot) => FilterNavigationPage<TagFilter>(
source: source,
title: context.l10n.tagPageTitle,
sortFactor: settings.tagSortFactor,
chipSetActionDelegate: TagChipSetActionDelegate(),
chipActionDelegate: ChipActionDelegate(),
chipActionsBuilder: (filter) => [