#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. All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
### Added
- Collection / Albums / Countries / Tags: added label when dragging scrollbar thumb
### Changed ### Changed
- Upgraded Flutter to beta v2.1.0-12.2.pre - Upgraded Flutter to beta v2.1.0-12.2.pre

View file

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

View file

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

View file

@ -9,6 +9,7 @@ class Durations {
static const dialogTransitionAnimation = Duration(milliseconds: 150); // ref `transitionDuration` in `showDialog()` static const dialogTransitionAnimation = Duration(milliseconds: 150); // ref `transitionDuration` in `showDialog()`
static const staggeredAnimation = Duration(milliseconds: 375); static const staggeredAnimation = Duration(milliseconds: 375);
static const staggeredAnimationPageTarget = Duration(milliseconds: 900);
static const dialogFieldReachAnimation = Duration(milliseconds: 300); static const dialogFieldReachAnimation = Duration(milliseconds: 300);
static const appBarTitleAnimation = 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/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/app_bar.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/section_layout.dart';
import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/selector.dart';
import 'package:aves/widgets/collection/grid/thumbnail.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/scaling.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class CollectionGrid extends StatefulWidget { class CollectionGrid extends StatefulWidget {
final String settingsRouteKey; final String settingsRouteKey;
@ -74,23 +75,34 @@ class _CollectionGridContent extends StatelessWidget {
builder: (context, tileExtent, child) { builder: (context, tileExtent, child) {
return ThumbnailTheme( return ThumbnailTheme(
extent: tileExtent, extent: tileExtent,
child: SectionedEntryListLayoutProvider( child: Selector<TileExtentController, Tuple2<double, int>>(
collection: collection, selector: (context, c) => Tuple2(c.viewportSize.width, c.columnCount),
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width), builder: (context, c, child) {
tileExtent: tileExtent, final scrollableWidth = c.item1;
columnCount: context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)), final columnCount = c.item2;
tileBuilder: (entry) => InteractiveThumbnail( // do not listen for animation delay change
key: ValueKey(entry.contentId), final controller = Provider.of<TileExtentController>(context, listen: false);
collection: collection, final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
entry: entry, return SectionedEntryListLayoutProvider(
tileExtent: tileExtent, collection: collection,
isScrollingNotifier: _isScrollingNotifier, scrollableWidth: scrollableWidth,
), tileExtent: tileExtent,
child: _CollectionSectionedContent( columnCount: columnCount,
collection: collection, tileBuilder: (entry) => InteractiveThumbnail(
isScrollingNotifier: _isScrollingNotifier, key: ValueKey(entry.contentId),
scrollController: PrimaryScrollController.of(context), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scrollView = _buildScrollView(widget.appBar, widget.collection); 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>( return ValueListenableBuilder<double>(
valueListenable: widget.appBarHeightNotifier, valueListenable: widget.appBarHeightNotifier,
builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>( builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>(
@ -287,6 +299,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
top: appBarHeight, top: appBarHeight,
bottom: mqPaddingBottom, bottom: mqPaddingBottom,
), ),
labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel(
collection: collection,
offsetY: offsetY,
),
child: scrollView, child: scrollView,
), ),
child: child, 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 int columnCount,
@required double tileExtent, @required double tileExtent,
@required Widget Function(AvesEntry entry) tileBuilder, @required Widget Function(AvesEntry entry) tileBuilder,
@required Duration tileAnimationDelay,
@required Widget child, @required Widget child,
}) : super( }) : super(
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
columnCount: columnCount, columnCount: columnCount,
tileExtent: tileExtent, tileExtent: tileExtent,
tileBuilder: tileBuilder, tileBuilder: tileBuilder,
tileAnimationDelay: tileAnimationDelay,
child: child, child: child,
); );

View file

@ -62,7 +62,7 @@ class DraggableScrollbar extends StatefulWidget {
@required this.controller, @required this.controller,
this.padding, this.padding,
this.scrollbarAnimationDuration = const Duration(milliseconds: 300), this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
this.scrollbarTimeToFade = const Duration(milliseconds: 600), this.scrollbarTimeToFade = const Duration(milliseconds: 1000),
this.labelTextBuilder, this.labelTextBuilder,
@required this.child, @required this.child,
}) : assert(controller != null), }) : assert(controller != null),
@ -91,6 +91,7 @@ class DraggableScrollbar extends StatefulWidget {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
child: labelText, child: labelText,
), ),
SizedBox(width: 24),
scrollThumb, scrollThumb,
], ],
); );
@ -133,6 +134,7 @@ class ScrollLabel extends StatelessWidget {
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin { class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
bool _isDragInProcess = false; bool _isDragInProcess = false;
Offset _longPressLastGlobalPosition;
AnimationController _thumbAnimationController; AnimationController _thumbAnimationController;
Animation<double> _thumbAnimation; Animation<double> _thumbAnimation;
@ -193,9 +195,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
), ),
RepaintBoundary( RepaintBoundary(
child: GestureDetector( child: GestureDetector(
onVerticalDragStart: _onVerticalDragStart, onLongPressStart: (details) {
onVerticalDragUpdate: _onVerticalDragUpdate, _longPressLastGlobalPosition = details.globalPosition;
onVerticalDragEnd: _onVerticalDragEnd, _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( child: ValueListenableBuilder(
valueListenable: _thumbOffsetNotifier, valueListenable: _thumbOffsetNotifier,
builder: (context, thumbOffset, child) => Container( builder: (context, thumbOffset, child) => Container(
@ -244,17 +256,18 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
} }
} }
void _onVerticalDragStart(DragStartDetails details) { void _onVerticalDragStart() {
_labelAnimationController.forward(); _labelAnimationController.forward();
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
_showThumb();
setState(() => _isDragInProcess = true); setState(() => _isDragInProcess = true);
} }
void _onVerticalDragUpdate(DragUpdateDetails details) { void _onVerticalDragUpdate(double deltaY) {
_showThumb(); _showThumb();
if (_isDragInProcess) { if (_isDragInProcess) {
// thumb offset // thumb offset
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + details.delta.dy).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); _thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
// scroll offset // scroll offset
final min = controller.position.minScrollExtent; final min = controller.position.minScrollExtent;
@ -263,7 +276,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
} }
} }
void _onVerticalDragEnd(DragEndDetails details) { void _onVerticalDragEnd() {
_scheduleFadeout(); _scheduleFadeout();
setState(() => _isDragInProcess = false); 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 int columnCount;
final double spacing, tileExtent; final double spacing, tileExtent;
final Widget Function(T item) tileBuilder; final Widget Function(T item) tileBuilder;
final Duration tileAnimationDelay;
final Widget child; final Widget child;
const SectionedListLayoutProvider({ const SectionedListLayoutProvider({
@ -20,6 +21,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
this.spacing = 0, this.spacing = 0,
@required this.tileExtent, @required this.tileExtent,
@required this.tileBuilder, @required this.tileBuilder,
this.tileAnimationDelay,
@required this.child, @required this.child,
}) : assert(scrollableWidth != 0); }) : assert(scrollableWidth != 0);
@ -40,6 +42,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final _showHeaders = showHeaders; final _showHeaders = showHeaders;
final _sections = sections; final _sections = sections;
final sectionKeys = _sections.keys.toList(); final sectionKeys = _sections.keys.toList();
final animate = tileAnimationDelay > Duration.zero;
final sectionLayouts = <SectionLayout>[]; final sectionLayouts = <SectionLayout>[];
var currentIndex = 0, currentOffset = 0.0; var currentIndex = 0, currentOffset = 0.0;
@ -76,6 +79,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
listIndex - sectionFirstIndex, listIndex - sectionFirstIndex,
sectionKey, sectionKey,
headerExtent, headerExtent,
animate,
), ),
), ),
); );
@ -97,10 +101,11 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
int sectionChildIndex, int sectionChildIndex,
SectionKey sectionKey, SectionKey sectionKey,
double headerExtent, double headerExtent,
bool animate,
) { ) {
if (sectionChildIndex == 0) { if (sectionChildIndex == 0) {
final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink(); final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink();
return _buildAnimation(sectionGridIndex, header); return animate ? _buildAnimation(sectionGridIndex, header) : header;
} }
sectionChildIndex--; sectionChildIndex--;
@ -113,7 +118,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final itemGridIndex = sectionGridIndex + i - minItemIndex; final itemGridIndex = sectionGridIndex + i - minItemIndex;
final item = tileBuilder(section[i]); final item = tileBuilder(section[i]);
if (i != minItemIndex) children.add(SizedBox(width: spacing)); if (i != minItemIndex) children.add(SizedBox(width: spacing));
children.add(_buildAnimation(itemGridIndex, item)); children.add(animate ? _buildAnimation(itemGridIndex, item) : item);
} }
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -126,7 +131,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
position: index, position: index,
columnCount: columnCount, columnCount: columnCount,
duration: Durations.staggeredAnimation, duration: Durations.staggeredAnimation,
delay: Durations.staggeredAnimationDelay, delay: tileAnimationDelay ?? Durations.staggeredAnimationDelay,
child: SlideAnimation( child: SlideAnimation(
verticalOffset: 50.0, verticalOffset: 50.0,
child: FadeInAnimation( child: FadeInAnimation(
@ -189,9 +194,11 @@ class SectionedListLayout<T> {
return Rect.fromLTWH(left, top, tileExtent, tileExtent); return Rect.fromLTWH(left, top, tileExtent, tileExtent);
} }
SectionLayout getSectionAt(double offsetY) => sectionLayouts.firstWhere((sl) => offsetY < sl.maxOffset, orElse: () => null);
T getItemAt(Offset position) { T getItemAt(Offset position) {
var dy = position.dy; var dy = position.dy;
final sectionLayout = sectionLayouts.firstWhere((sl) => dy < sl.maxOffset, orElse: () => null); final sectionLayout = getSectionAt(dy);
if (sectionLayout == null) return null; if (sectionLayout == null) return null;
final section = sections[sectionLayout.sectionKey]; final section = sections[sectionLayout.sectionKey];

View file

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class TileExtentController { class TileExtentController {
@ -45,7 +46,7 @@ class TileExtentController {
? oldUserPreferredExtent ? oldUserPreferredExtent
: currentExtent; : currentExtent;
final columnCount = getEffectiveColumnCountForExtent(targetExtent); final columnCount = _effectiveColumnCountForExtent(targetExtent);
final newExtent = _extentForColumnCount(columnCount); final newExtent = _extentForColumnCount(columnCount);
if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) {
@ -67,15 +68,24 @@ class TileExtentController {
int _effectiveColumnCountMax() => _columnCountForExtent(extentMin).floor(); int _effectiveColumnCountMax() => _columnCountForExtent(extentMin).floor();
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax()); int _effectiveColumnCountForExtent(double extent) {
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
int getEffectiveColumnCountForExtent(double extent) {
if (extent > 0) { if (extent > 0) {
final columnCount = _columnCountForExtent(extent); final columnCount = _columnCountForExtent(extent);
return columnCount.clamp(_effectiveColumnCountMin(), _effectiveColumnCountMax()).round(); return columnCount.clamp(_effectiveColumnCountMin(), _effectiveColumnCountMax()).round();
} }
return columnCountDefault; 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, appBar: appBar,
appBarHeight: AlbumPickAppBar.preferredHeight, appBarHeight: AlbumPickAppBar.preferredHeight,
filterSections: AlbumListPage.getAlbumEntries(context, source), filterSections: AlbumListPage.getAlbumEntries(context, source),
sortFactor: settings.albumSortFactor,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
queryNotifier: _queryNotifier, queryNotifier: _queryNotifier,
applyQuery: (filters, query) { applyQuery: (filters, query) {

View file

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

View file

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

View file

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

View file

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

View file

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