#1080 scrolling quick action selector for tag/move/copy

This commit is contained in:
Thibault Deckers 2024-07-24 00:32:42 +02:00
parent dc061e89e6
commit afc09e2ab4
14 changed files with 316 additions and 85 deletions

View file

@ -6,7 +6,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
mixin AppSettings on SettingsAccess {
static const int _recentFilterHistoryMax = 10;
static const int recentFilterHistoryMax = 20;
bool get hasAcceptedTerms => getBool(SettingKeys.hasAcceptedTermsKey) ?? SettingsDefaults.hasAcceptedTerms;
@ -105,9 +105,9 @@ mixin AppSettings on SettingsAccess {
List<String> get recentDestinationAlbums => getStringList(SettingKeys.recentDestinationAlbumsKey) ?? [];
set recentDestinationAlbums(List<String> newValue) => set(SettingKeys.recentDestinationAlbumsKey, newValue.take(_recentFilterHistoryMax).toList());
set recentDestinationAlbums(List<String> newValue) => set(SettingKeys.recentDestinationAlbumsKey, newValue.take(recentFilterHistoryMax).toList());
List<CollectionFilter> get recentTags => (getStringList(SettingKeys.recentTagsKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList();
set recentTags(List<CollectionFilter> newValue) => set(SettingKeys.recentTagsKey, newValue.take(_recentFilterHistoryMax).map((filter) => filter.toJson()).toList());
set recentTags(List<CollectionFilter> newValue) => set(SettingKeys.recentTagsKey, newValue.take(recentFilterHistoryMax).map((filter) => filter.toJson()).toList());
}

View file

@ -162,6 +162,8 @@ class AIcons {
static const zoomOut = Icons.remove_outlined;
static const collapse = Icons.expand_less_outlined;
static const expand = Icons.expand_more_outlined;
static const up = Icons.keyboard_arrow_up_outlined;
static const down = Icons.keyboard_arrow_down_outlined;
static const previous = Icons.chevron_left_outlined;
static const next = Icons.chevron_right_outlined;

View file

@ -142,7 +142,7 @@ class _Chip extends StatelessWidget {
key: ValueKey(filter),
filter: filter,
maxWidth: single
? AvesFilterChip.computeMaxWidth(
? AvesFilterChip.computeMaxWidthForRow(
context,
minChipPerRow: 1,
chipPadding: FilterBar.chipPadding.horizontal,

View file

@ -1,14 +1,17 @@
import 'dart:async';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AlbumQuickChooser extends StatelessWidget {
class AlbumQuickChooser extends StatelessWidget with FilterQuickChooserMixin<String> {
final ValueNotifier<String?> valueNotifier;
@override
final List<String> options;
final bool blurred;
final PopupMenuPosition chooserPosition;
@ -25,7 +28,6 @@ class AlbumQuickChooser extends StatelessWidget {
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
return MenuQuickChooser<String>(
valueNotifier: valueNotifier,
options: options,
@ -33,10 +35,17 @@ class AlbumQuickChooser extends StatelessWidget {
blurred: blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
itemBuilder: (context, album) => AvesFilterChip(
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
allowGenericIcon: false,
),
maxTotalOptionCount: FilterQuickChooserMixin.maxTotalOptionCount,
itemHeight: computeItemHeight(context),
contentWidth: computeLargestItemWidth,
itemBuilder: itemBuilder,
emptyBuilder: (context) => Text(context.l10n.albumEmpty),
);
}
@override
CollectionFilter buildFilter(BuildContext context, String option) {
final source = context.read<CollectionSource>();
return AlbumFilter(option, source.getAlbumDisplayName(context, option));
}
}

View file

@ -1,9 +1,10 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/quick_chooser.dart';
import 'package:aves_ui/aves_ui.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -16,9 +17,13 @@ class MenuQuickChooser<T> extends StatefulWidget {
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> pointerGlobalPosition;
final int maxTotalOptionCount;
final double itemHeight;
final double? Function(BuildContext context)? contentWidth;
final Widget Function(BuildContext context, T menuItem) itemBuilder;
final WidgetBuilder? emptyBuilder;
static const int maxOptionCount = 5;
static const int maxVisibleOptionCount = 5;
MenuQuickChooser({
super.key,
@ -28,8 +33,12 @@ class MenuQuickChooser<T> extends StatefulWidget {
required this.blurred,
required this.chooserPosition,
required this.pointerGlobalPosition,
this.maxTotalOptionCount = maxVisibleOptionCount,
this.itemHeight = kMinInteractiveDimension,
this.contentWidth,
required this.itemBuilder,
}) : options = options.take(maxOptionCount).toList();
this.emptyBuilder,
}) : options = options.take(maxTotalOptionCount).toList();
@override
State<MenuQuickChooser<T>> createState() => _MenuQuickChooserState<T>();
@ -38,6 +47,10 @@ class MenuQuickChooser<T> extends StatefulWidget {
class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
final ScrollController _scrollController = ScrollController();
int _scrollDirection = 0;
Timer? _scrollUpdateTimer;
Offset _globalPosition = Offset.zero;
ValueNotifier<T?> get valueNotifier => widget.valueNotifier;
@ -45,12 +58,26 @@ class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
bool get reversed => widget.autoReverse && widget.chooserPosition == PopupMenuPosition.over;
static const double intraPadding = 8;
bool get scrollable => options.length > MenuQuickChooser.maxVisibleOptionCount;
int get visibleOptionCount => min(MenuQuickChooser.maxVisibleOptionCount, options.length);
double get itemHeight => widget.itemHeight;
double get contentHeight => max(0, itemHeight * visibleOptionCount + _intraPadding * (visibleOptionCount - 1));
static const double _selectorMargin = 24;
static const double _intraPadding = 8;
static const double _nonScrollablePaddingHeight = _intraPadding;
static const double _scrollerAreaHeight = kMinInteractiveDimension;
static const double scrollMaxPixelPerSecond = 600.0;
static const Duration scrollUpdateInterval = Duration(milliseconds: 100);
@override
void initState() {
super.initState();
_registerWidget(widget);
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {}));
}
@override
@ -69,6 +96,9 @@ class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
@override
void dispose() {
_unregisterWidget(widget);
_selectedRowRect.dispose();
_scrollController.dispose();
_scrollUpdateTimer?.cancel();
super.dispose();
}
@ -91,28 +121,11 @@ class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
builder: (context, selectedValue, child) {
final durations = context.watch<DurationsData>();
List<Widget> optionChildren = options.mapIndexed((index, value) {
final isFirst = index == (reversed ? options.length - 1 : 0);
if (options.isEmpty) {
return Padding(
padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding),
child: widget.itemBuilder(context, value),
padding: const EdgeInsets.all(16),
child: widget.emptyBuilder?.call(context) ?? const SizedBox(),
);
}).toList();
optionChildren = AnimationConfiguration.toStaggeredList(
duration: durations.staggeredAnimation * .5,
delay: durations.staggeredAnimationDelay * .5 * timeDilation,
childAnimationBuilder: (child) => SlideAnimation(
verticalOffset: 50.0 * (widget.chooserPosition == PopupMenuPosition.over ? 1 : -1),
child: FadeInAnimation(
child: child,
),
),
children: optionChildren,
);
if (reversed) {
optionChildren = optionChildren.reversed.toList();
}
return Stack(
@ -137,12 +150,67 @@ class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
return child;
},
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 24),
Container(
width: widget.contentWidth?.call(context),
margin: const EdgeInsetsDirectional.only(start: _selectorMargin),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: optionChildren,
children: [
scrollable
? ListenableBuilder(
listenable: _scrollController,
builder: (context, child) => Opacity(
opacity: canGoUp ? 1 : .5,
child: child,
),
child: _buildScrollerArea(AIcons.up),
)
: const SizedBox(height: _nonScrollablePaddingHeight),
ConstrainedBox(
constraints: BoxConstraints.tightFor(height: contentHeight),
child: ListView.separated(
reverse: reversed,
controller: _scrollController,
shrinkWrap: true,
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
final child = Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints.tightFor(height: itemHeight),
child: widget.itemBuilder(context, options[index]),
);
if (index < MenuQuickChooser.maxVisibleOptionCount) {
// only animate items visible on first render
return AnimationConfiguration.staggeredList(
position: index,
duration: durations.staggeredAnimation * .5,
delay: durations.staggeredAnimationDelay * .5 * timeDilation,
child: SlideAnimation(
verticalOffset: 50.0 * (widget.chooserPosition == PopupMenuPosition.over ? 1 : -1),
child: FadeInAnimation(
child: child,
),
),
);
} else {
return child;
}
},
separatorBuilder: (context, index) => const SizedBox(height: _intraPadding),
itemCount: options.length,
),
),
scrollable
? ListenableBuilder(
listenable: _scrollController,
builder: (context, child) => Opacity(
opacity: canGoDown ? 1 : .5,
child: child,
),
child: _buildScrollerArea(AIcons.down),
)
: const SizedBox(height: _nonScrollablePaddingHeight),
],
),
),
],
@ -152,30 +220,97 @@ class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
);
}
void _onPointerMove(Offset globalPosition) {
final padding = QuickChooser.margin.vertical + QuickChooser.padding.vertical;
bool get canGoUp {
if (!_scrollController.hasClients) return false;
final position = _scrollController.position;
return reversed ? position.pixels < position.maxScrollExtent : 0 < position.pixels;
}
bool get canGoDown {
if (!_scrollController.hasClients) return false;
final position = _scrollController.position;
return reversed ? 0 < position.pixels : position.pixels < position.maxScrollExtent;
}
Widget _buildScrollerArea(IconData icon) {
return Container(
alignment: Alignment.center,
height: _scrollerAreaHeight,
margin: const EdgeInsetsDirectional.only(end: _selectorMargin),
child: Icon(icon),
);
}
void _onPointerMove(Offset globalPosition) {
_globalPosition = globalPosition;
final chooserBox = context.findRenderObject() as RenderBox?;
if (chooserBox == null) return;
final chooserBox = context.findRenderObject() as RenderBox;
final chooserSize = chooserBox.size;
final contentWidth = chooserSize.width;
final contentHeight = chooserSize.height - padding;
final chooserBoxEdgeHeight = (QuickChooser.margin.vertical + QuickChooser.padding.vertical) / 2;
final optionCount = options.length;
final itemHeight = (contentHeight - (optionCount + 1) * intraPadding) / optionCount;
final localPosition = chooserBox.globalToLocal(globalPosition);
final dx = localPosition.dx;
if (!(0 < dx && dx < contentWidth)) {
valueNotifier.value = null;
return;
}
final local = chooserBox.globalToLocal(globalPosition);
final dx = local.dx;
final dy = local.dy - padding / 2;
double dy = localPosition.dy - chooserBoxEdgeHeight;
int scrollDirection = 0;
if (scrollable) {
dy -= _scrollerAreaHeight;
if (-_scrollerAreaHeight < dy && dy < 0) {
scrollDirection = reversed ? 1 : -1;
} else if (contentHeight < dy && dy < contentHeight + _scrollerAreaHeight) {
scrollDirection = reversed ? -1 : 1;
}
_scroll(scrollDirection);
} else {
dy -= _nonScrollablePaddingHeight;
}
T? selectedValue;
if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) {
final index = (optionCount * dy / contentHeight).floor();
if (0 <= index && index < optionCount) {
selectedValue = options[reversed ? optionCount - 1 - index : index];
final top = index * (itemHeight + intraPadding) + intraPadding;
if (scrollDirection == 0 && 0 < dy && dy < contentHeight) {
final visibleOffset = reversed ? contentHeight - dy : dy;
final fullItemHeight = itemHeight + _intraPadding;
final scrollOffset = _scrollController.offset;
final index = (visibleOffset + _intraPadding + scrollOffset) ~/ (fullItemHeight);
if (0 <= index && index < options.length) {
selectedValue = options[index];
double fromEdge = fullItemHeight * index;
fromEdge += (scrollable ? _scrollerAreaHeight - scrollOffset : _nonScrollablePaddingHeight);
final top = reversed ? chooserSize.height - chooserBoxEdgeHeight - fromEdge - fullItemHeight : fromEdge;
_selectedRowRect.value = Rect.fromLTWH(0, top, contentWidth, itemHeight);
}
}
valueNotifier.value = selectedValue;
}
void _scroll(int scrollDirection) {
if (scrollDirection == _scrollDirection) return;
_scrollDirection = scrollDirection;
_scrollUpdateTimer?.cancel();
final current = _scrollController.offset;
if (scrollDirection == 0) {
_scrollController.jumpTo(current);
return;
}
final target = scrollDirection > 0 ? _scrollController.position.maxScrollExtent : .0;
if (target != current) {
final distance = target - current;
final millis = distance * 1000 / scrollMaxPixelPerSecond / scrollDirection;
_scrollController.animateTo(
target,
duration: Duration(milliseconds: millis.round()),
curve: Curves.linear,
);
// use a timer to update the selection, because `_onPointerMove`
// is not called when the pointer stays still while the view is scrolling
_scrollUpdateTimer = Timer.periodic(scrollUpdateInterval, (_) => _onPointerMove(_globalPosition));
}
}
}

View file

@ -0,0 +1,56 @@
import 'dart:math';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/modules/app.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
mixin FilterQuickChooserMixin<T> {
List<T> get options;
static const int maxTotalOptionCount = AppSettings.recentFilterHistoryMax;
static const double _chipPadding = AvesFilterChip.defaultPadding;
static const bool _chipAllowGenericIcon = false;
CollectionFilter buildFilter(BuildContext context, T option);
Widget itemBuilder(BuildContext context, T option) {
return AvesFilterChip(
filter: buildFilter(context, option),
allowGenericIcon: _chipAllowGenericIcon,
padding: _chipPadding,
maxWidth: double.infinity,
);
}
double computeItemHeight(BuildContext context) => AvesFilterChip.minChipHeight;
double? computeLargestItemWidth(BuildContext context) {
if (options.isEmpty) return null;
final textStyle = DefaultTextStyle.of(context).style.copyWith(
fontSize: AvesFilterChip.fontSize,
);
final textDirection = Directionality.of(context);
final textScaler = MediaQuery.textScalerOf(context);
final iconSize = textScaler.scale(AvesFilterChip.iconSize);
return options.map((option) {
final filter = buildFilter(context, option);
final icon = filter.iconBuilder(context, iconSize, allowGenericIcon: _chipAllowGenericIcon);
final label = filter.getLabel(context);
final paragraph = RenderParagraph(
TextSpan(text: label, style: textStyle),
textDirection: textDirection,
textScaler: textScaler,
)..layout(const BoxConstraints(), parentUsesSize: true);
final labelWidth = paragraph.getMaxIntrinsicWidth(double.infinity);
double chipWidth = labelWidth + _chipPadding * 4;
if (icon != null) {
chipWidth += iconSize + _chipPadding;
}
return max(AvesFilterChip.minChipWidth, chipWidth);
}).reduce(max);
}
}

View file

@ -5,7 +5,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/album_chooser.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
import 'package:aves_model/aves_model.dart';
@ -42,7 +42,7 @@ class _MoveButtonState extends ChooserQuickButtonState<MoveButton, String> {
final source = context.read<CollectionSource>();
final rawAlbums = source.rawAlbums;
final options = settings.recentDestinationAlbums.where(rawAlbums.contains).toList();
final takeCount = MenuQuickChooser.maxOptionCount - options.length;
final takeCount = FilterQuickChooserMixin.maxTotalOptionCount - options.length;
if (takeCount > 0) {
final filters = rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet();
final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList();

View file

@ -51,7 +51,6 @@ class _ShareButtonState extends ChooserQuickButtonState<ShareButton, ShareAction
child: ShareQuickChooser(
valueNotifier: chooserValueNotifier,
options: options,
autoReverse: false,
blurred: widget.blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,

View file

@ -1,24 +1,26 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ShareQuickChooser extends StatelessWidget {
final ValueNotifier<ShareAction?> valueNotifier;
final List<ShareAction> options;
final bool autoReverse;
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> pointerGlobalPosition;
static const _itemPadding = EdgeInsetsDirectional.only(end: 8);
const ShareQuickChooser({
super.key,
required this.valueNotifier,
required this.options,
required this.autoReverse,
required this.blurred,
required this.chooserPosition,
required this.pointerGlobalPosition,
@ -29,20 +31,39 @@ class ShareQuickChooser extends StatelessWidget {
return MenuQuickChooser<ShareAction>(
valueNotifier: valueNotifier,
options: options,
autoReverse: autoReverse,
autoReverse: false,
blurred: blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
itemBuilder: (context, action) => ConstrainedBox(
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: MenuRow(
text: action.getText(context),
icon: action.getIcon(),
),
itemHeight: kMinInteractiveDimension,
contentWidth: _computeLargestItemWidth,
itemBuilder: (context, action) => Padding(
padding: _itemPadding,
child: MenuRow(
text: action.getText(context),
icon: action.getIcon(),
),
),
);
}
double? _computeLargestItemWidth(BuildContext context) {
if (options.isEmpty) return null;
final textStyle = DefaultTextStyle.of(context).style;
final textDirection = Directionality.of(context);
final textScaler = MediaQuery.textScalerOf(context);
final iconSize = IconTheme.of(context).size ?? 24;
return options.map((action) {
final text = action.getText(context);
final paragraph = RenderParagraph(
TextSpan(text: text, style: textStyle),
textDirection: textDirection,
textScaler: textScaler,
)..layout(const BoxConstraints(), parentUsesSize: true);
final labelWidth = paragraph.getMaxIntrinsicWidth(double.infinity);
return iconSize + MenuRow.leadingPadding.horizontal + labelWidth + _itemPadding.horizontal;
}).reduce(max);
}
}

View file

@ -4,7 +4,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/tag_chooser.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
@ -38,7 +38,7 @@ class _TagButtonState extends ChooserQuickButtonState<TagButton, CollectionFilte
@override
Widget buildChooser(Animation<double> animation, PopupMenuPosition chooserPosition) {
final options = settings.recentTags;
final takeCount = MenuQuickChooser.maxOptionCount - options.length;
final takeCount = FilterQuickChooserMixin.maxTotalOptionCount - options.length;
if (takeCount > 0) {
final source = context.read<CollectionSource>();
final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet();

View file

@ -2,11 +2,13 @@ import 'dart:async';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
class TagQuickChooser extends StatelessWidget {
class TagQuickChooser extends StatelessWidget with FilterQuickChooserMixin<CollectionFilter> {
final ValueNotifier<CollectionFilter?> valueNotifier;
@override
final List<CollectionFilter> options;
final bool blurred;
final PopupMenuPosition chooserPosition;
@ -30,10 +32,14 @@ class TagQuickChooser extends StatelessWidget {
blurred: blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
itemBuilder: (context, filter) => AvesFilterChip(
filter: filter,
allowGenericIcon: false,
),
maxTotalOptionCount: FilterQuickChooserMixin.maxTotalOptionCount,
itemHeight: computeItemHeight(context),
contentWidth: computeLargestItemWidth,
itemBuilder: itemBuilder,
emptyBuilder: (context) => Text(context.l10n.tagEmpty),
);
}
@override
CollectionFilter buildFilter(BuildContext context, CollectionFilter option) => option;
}

View file

@ -10,6 +10,8 @@ class MenuRow extends StatelessWidget {
this.icon,
});
static const leadingPadding = EdgeInsetsDirectional.only(end: 12);
@override
Widget build(BuildContext context) {
return Row(
@ -17,7 +19,7 @@ class MenuRow extends StatelessWidget {
children: [
if (icon != null)
Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
padding: leadingPadding,
child: IconTheme.merge(
data: IconThemeData(
color: ListTileTheme.of(context).iconColor,

View file

@ -55,9 +55,9 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
return scrollableBox.size.width;
}
static const double scrollEdgeRatio = .15;
static const double scrollMaxPixelPerSecond = 600.0;
static const Duration scrollUpdateInterval = Duration(milliseconds: 100);
static const double _scrollEdgeRatio = .15;
static const double _scrollMaxPixelPerSecond = 600.0;
static const Duration _scrollUpdateInterval = Duration(milliseconds: 100);
@override
void initState() {
@ -158,7 +158,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
final top = dy < height / 2;
final distanceToEdge = max(0, top ? dy - _scrollableInsets.top : height - dy - _scrollableInsets.bottom);
final threshold = height * scrollEdgeRatio;
final threshold = height * _scrollEdgeRatio;
if (distanceToEdge < threshold) {
_setScrollSpeed((top ? -1 : 1) * roundToPrecision((threshold - distanceToEdge) / threshold, decimals: 1));
} else {
@ -185,7 +185,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
final target = speedFactor > 0 ? scrollController.position.maxScrollExtent : .0;
if (target != current) {
final distance = target - current;
final millis = distance * 1000 / scrollMaxPixelPerSecond / speedFactor;
final millis = distance * 1000 / _scrollMaxPixelPerSecond / speedFactor;
scrollController.animateTo(
target,
duration: Duration(milliseconds: millis.round()),
@ -193,7 +193,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
);
// use a timer to update the selection, because `onLongPressMoveUpdate`
// is not called when the pointer stays still while the view is scrolling
_selectionUpdateTimer = Timer.periodic(scrollUpdateInterval, (_) => _onLongPressUpdate());
_selectionUpdateTimer = Timer.periodic(_scrollUpdateInterval, (_) => _onLongPressUpdate());
}
}

View file

@ -59,6 +59,7 @@ class AvesFilterChip extends StatefulWidget {
final FilterCallback? onTap, onRemove;
final OffsetFilterCallback? onLongPress;
static const double defaultPadding = 6.0;
static const double defaultRadius = 32;
static const double outlineWidth = 2;
static const double minChipHeight = kMinInteractiveDimension;
@ -79,7 +80,7 @@ class AvesFilterChip extends StatefulWidget {
this.banner,
this.leadingOverride,
this.details,
this.padding = 6.0,
this.padding = defaultPadding,
this.maxWidth,
this.heroType = HeroType.onTap,
this.onTap,
@ -87,7 +88,7 @@ class AvesFilterChip extends StatefulWidget {
this.onLongPress = showDefaultLongPressMenu,
});
static double computeMaxWidth(
static double computeMaxWidthForRow(
BuildContext context, {
required int minChipPerRow,
required double chipPadding,
@ -347,7 +348,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
maxWidth: max(
AvesFilterChip.minChipWidth,
widget.maxWidth ??
AvesFilterChip.computeMaxWidth(
AvesFilterChip.computeMaxWidthForRow(
context,
minChipPerRow: 2,
chipPadding: FilterBar.chipPadding.horizontal,