#38 changed decorated filter layout, with label below cover

This commit is contained in:
Thibault Deckers 2021-09-26 11:41:03 +09:00
parent 8f2a0a8247
commit 5939668fd5
9 changed files with 318 additions and 216 deletions

View file

@ -79,18 +79,17 @@ class _CollectionGridContent 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) {
return GridTheme( return Selector<TileExtentController, Tuple3<double, int, double>>(
extent: tileExtent, selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
child: Selector<TileExtentController, Tuple3<double, int, double>>( builder: (context, c, child) {
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), final scrollableWidth = c.item1;
builder: (context, c, child) { final columnCount = c.item2;
final scrollableWidth = c.item1; final tileSpacing = c.item3;
final columnCount = c.item2; // do not listen for animation delay change
final tileSpacing = c.item3; final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
// do not listen for animation delay change return GridTheme(
final controller = Provider.of<TileExtentController>(context, listen: false); extent: tileExtent,
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget); child: SectionedEntryListLayoutProvider(
return SectionedEntryListLayoutProvider(
collection: collection, collection: collection,
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
columnCount: columnCount, columnCount: columnCount,
@ -104,16 +103,18 @@ class _CollectionGridContent extends StatelessWidget {
isScrollingNotifier: _isScrollingNotifier, isScrollingNotifier: _isScrollingNotifier,
), ),
tileAnimationDelay: tileAnimationDelay, tileAnimationDelay: tileAnimationDelay,
child: _CollectionSectionedContent( child: child!,
collection: collection, ),
isScrollingNotifier: _isScrollingNotifier, );
scrollController: PrimaryScrollController.of(context)!, },
), child: child,
);
},
),
); );
}, },
child: _CollectionSectionedContent(
collection: collection,
isScrollingNotifier: _isScrollingNotifier,
scrollController: PrimaryScrollController.of(context)!,
),
); );
return sectionedListLayoutProvider; return sectionedListLayoutProvider;
}, },
@ -199,10 +200,11 @@ class _CollectionScaler extends StatelessWidget {
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing); final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
return GridScaleGestureDetector<AvesEntry>( return GridScaleGestureDetector<AvesEntry>(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
gridBuilder: (center, extent, child) => CustomPaint( heightForWidth: (width) => width,
gridBuilder: (center, tileSize, child) => CustomPaint(
painter: GridPainter( painter: GridPainter(
center: center, center: center,
extent: extent, tileSize: tileSize,
spacing: tileSpacing, spacing: tileSpacing,
borderWidth: DecoratedThumbnail.borderWidth, borderWidth: DecoratedThumbnail.borderWidth,
borderRadius: Radius.zero, borderRadius: Radius.zero,
@ -210,7 +212,7 @@ class _CollectionScaler extends StatelessWidget {
), ),
child: child, child: child,
), ),
scaledBuilder: (entry, extent) => DecoratedThumbnail( scaledBuilder: (entry, tileSize) => DecoratedThumbnail(
entry: entry, entry: entry,
tileExtent: context.read<TileExtentController>().effectiveExtentMax, tileExtent: context.read<TileExtentController>().effectiveExtentMax,
selectable: false, selectable: false,

View file

@ -23,7 +23,8 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
columnCount: columnCount, columnCount: columnCount,
spacing: spacing, spacing: spacing,
tileExtent: tileExtent, tileWidth: tileExtent,
tileHeight: tileExtent,
tileBuilder: tileBuilder, tileBuilder: tileBuilder,
tileAnimationDelay: tileAnimationDelay, tileAnimationDelay: tileAnimationDelay,
child: child, child: child,

View file

@ -23,7 +23,7 @@ class DraggableThumbLabel<T> extends StatelessWidget {
final section = sll.sections[sectionLayout.sectionKey]!; final section = sll.sections[sectionLayout.sectionKey]!;
final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent); final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent);
final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileExtent + sll.spacing)) * sll.columnCount; final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileHeight + sll.spacing)) * sll.columnCount;
final item = section[itemIndex]; final item = section[itemIndex];
final lines = lineBuilder(context, item); final lines = lineBuilder(context, item);

View file

@ -13,7 +13,7 @@ import 'package:provider/provider.dart';
abstract class SectionedListLayoutProvider<T> extends StatelessWidget { abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final double scrollableWidth; final double scrollableWidth;
final int columnCount; final int columnCount;
final double spacing, tileExtent; final double spacing, tileWidth, tileHeight;
final Widget Function(T item) tileBuilder; final Widget Function(T item) tileBuilder;
final Duration tileAnimationDelay; final Duration tileAnimationDelay;
final Widget child; final Widget child;
@ -23,7 +23,8 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
required this.scrollableWidth, required this.scrollableWidth,
required this.columnCount, required this.columnCount,
required this.spacing, required this.spacing,
required this.tileExtent, required this.tileWidth,
required this.tileHeight,
required this.tileBuilder, required this.tileBuilder,
required this.tileAnimationDelay, required this.tileAnimationDelay,
required this.child, required this.child,
@ -60,7 +61,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final sectionLastIndex = currentIndex - 1; final sectionLastIndex = currentIndex - 1;
final sectionMinOffset = currentOffset; final sectionMinOffset = currentOffset;
currentOffset += headerExtent + tileExtent * rowCount + spacing * (rowCount - 1); currentOffset += headerExtent + tileHeight * rowCount + spacing * (rowCount - 1);
final sectionMaxOffset = currentOffset; final sectionMaxOffset = currentOffset;
sectionLayouts.add( sectionLayouts.add(
@ -71,7 +72,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
minOffset: sectionMinOffset, minOffset: sectionMinOffset,
maxOffset: sectionMaxOffset, maxOffset: sectionMaxOffset,
headerExtent: headerExtent, headerExtent: headerExtent,
tileExtent: tileExtent, tileHeight: tileHeight,
spacing: spacing, spacing: spacing,
builder: (context, listIndex) => _buildInSection( builder: (context, listIndex) => _buildInSection(
context, context,
@ -89,7 +90,8 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
sections: _sections, sections: _sections,
showHeaders: _showHeaders, showHeaders: _showHeaders,
columnCount: columnCount, columnCount: columnCount,
tileExtent: tileExtent, tileWidth: tileWidth,
tileHeight: tileHeight,
spacing: spacing, spacing: spacing,
sectionLayouts: sectionLayouts, sectionLayouts: sectionLayouts,
); );
@ -123,7 +125,8 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
children.add(animate ? _buildAnimation(itemGridIndex, item) : item); children.add(animate ? _buildAnimation(itemGridIndex, item) : item);
} }
return _GridRow( return _GridRow(
extent: tileExtent, width: tileWidth,
height: tileHeight,
spacing: spacing, spacing: spacing,
children: children, children: children,
); );
@ -158,7 +161,8 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
properties.add(DoubleProperty('scrollableWidth', scrollableWidth)); properties.add(DoubleProperty('scrollableWidth', scrollableWidth));
properties.add(IntProperty('columnCount', columnCount)); properties.add(IntProperty('columnCount', columnCount));
properties.add(DoubleProperty('spacing', spacing)); properties.add(DoubleProperty('spacing', spacing));
properties.add(DoubleProperty('tileExtent', tileExtent)); properties.add(DoubleProperty('tileWidth', tileWidth));
properties.add(DoubleProperty('tileHeight', tileHeight));
properties.add(DiagnosticsProperty<bool>('showHeaders', showHeaders)); properties.add(DiagnosticsProperty<bool>('showHeaders', showHeaders));
} }
} }
@ -167,14 +171,15 @@ class SectionedListLayout<T> {
final Map<SectionKey, List<T>> sections; final Map<SectionKey, List<T>> sections;
final bool showHeaders; final bool showHeaders;
final int columnCount; final int columnCount;
final double tileExtent, spacing; final double tileWidth, tileHeight, spacing;
final List<SectionLayout> sectionLayouts; final List<SectionLayout> sectionLayouts;
const SectionedListLayout({ const SectionedListLayout({
required this.sections, required this.sections,
required this.showHeaders, required this.showHeaders,
required this.columnCount, required this.columnCount,
required this.tileExtent, required this.tileWidth,
required this.tileHeight,
required this.spacing, required this.spacing,
required this.sectionLayouts, required this.sectionLayouts,
}); });
@ -192,9 +197,9 @@ class SectionedListLayout<T> {
final row = (sectionItemIndex / columnCount).floor(); final row = (sectionItemIndex / columnCount).floor();
final listIndex = sectionLayout.firstIndex + 1 + row; final listIndex = sectionLayout.firstIndex + 1 + row;
final left = tileExtent * column + spacing * (column - 1); final left = tileWidth * column + spacing * (column - 1);
final top = sectionLayout.indexToLayoutOffset(listIndex); final top = sectionLayout.indexToLayoutOffset(listIndex);
return Rect.fromLTWH(left, top, tileExtent, tileExtent); return Rect.fromLTWH(left, top, tileWidth, tileHeight);
} }
SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset);
@ -210,8 +215,8 @@ class SectionedListLayout<T> {
dy -= sectionLayout.minOffset + sectionLayout.headerExtent; dy -= sectionLayout.minOffset + sectionLayout.headerExtent;
if (dy < 0) return null; if (dy < 0) return null;
final row = dy ~/ (tileExtent + spacing); final row = dy ~/ (tileHeight + spacing);
final column = position.dx ~/ (tileExtent + spacing); final column = position.dx ~/ (tileWidth + spacing);
final index = row * columnCount + column; final index = row * columnCount + column;
if (index >= section.length) return null; if (index >= section.length) return null;
@ -219,7 +224,7 @@ class SectionedListLayout<T> {
} }
@override @override
String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileExtent=$tileExtent}'; String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileWidth=$tileWidth, tileHeight=$tileHeight}';
} }
@immutable @immutable
@ -227,11 +232,11 @@ class SectionLayout extends Equatable {
final SectionKey sectionKey; final SectionKey sectionKey;
final int firstIndex, lastIndex, bodyFirstIndex; final int firstIndex, lastIndex, bodyFirstIndex;
final double minOffset, maxOffset, bodyMinOffset; final double minOffset, maxOffset, bodyMinOffset;
final double headerExtent, tileExtent, spacing, mainAxisStride; final double headerExtent, tileHeight, spacing, mainAxisStride;
final IndexedWidgetBuilder builder; final IndexedWidgetBuilder builder;
@override @override
List<Object?> get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileExtent, spacing]; List<Object?> get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileHeight, spacing];
const SectionLayout({ const SectionLayout({
required this.sectionKey, required this.sectionKey,
@ -240,12 +245,12 @@ class SectionLayout extends Equatable {
required this.minOffset, required this.minOffset,
required this.maxOffset, required this.maxOffset,
required this.headerExtent, required this.headerExtent,
required this.tileExtent, required this.tileHeight,
required this.spacing, required this.spacing,
required this.builder, required this.builder,
}) : bodyFirstIndex = firstIndex + 1, }) : bodyFirstIndex = firstIndex + 1,
bodyMinOffset = minOffset + headerExtent, bodyMinOffset = minOffset + headerExtent,
mainAxisStride = tileExtent + spacing; mainAxisStride = tileHeight + spacing;
bool hasChild(int index) => firstIndex <= index && index <= lastIndex; bool hasChild(int index) => firstIndex <= index && index <= lastIndex;
@ -271,11 +276,12 @@ class SectionLayout extends Equatable {
} }
class _GridRow extends MultiChildRenderObjectWidget { class _GridRow extends MultiChildRenderObjectWidget {
final double extent, spacing; final double width, height, spacing;
_GridRow({ _GridRow({
Key? key, Key? key,
required this.extent, required this.width,
required this.height,
required this.spacing, required this.spacing,
required List<Widget> children, required List<Widget> children,
}) : super(key: key, children: children); }) : super(key: key, children: children);
@ -283,21 +289,24 @@ class _GridRow extends MultiChildRenderObjectWidget {
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return _RenderGridRow( return _RenderGridRow(
extent: extent, width: width,
height: height,
spacing: spacing, spacing: spacing,
); );
} }
@override @override
void updateRenderObject(BuildContext context, _RenderGridRow renderObject) { void updateRenderObject(BuildContext context, _RenderGridRow renderObject) {
renderObject.extent = extent; renderObject.width = width;
renderObject.height = height;
renderObject.spacing = spacing; renderObject.spacing = spacing;
} }
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DoubleProperty('extent', extent)); properties.add(DoubleProperty('width', width));
properties.add(DoubleProperty('height', height));
properties.add(DoubleProperty('spacing', spacing)); properties.add(DoubleProperty('spacing', spacing));
} }
} }
@ -307,19 +316,30 @@ class _GridRowParentData extends ContainerBoxParentData<RenderBox> {}
class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, _GridRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _GridRowParentData> { class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, _GridRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _GridRowParentData> {
_RenderGridRow({ _RenderGridRow({
List<RenderBox>? children, List<RenderBox>? children,
required double extent, required double width,
required double height,
required double spacing, required double spacing,
}) : _extent = extent, }) : _width = width,
_height = height,
_spacing = spacing { _spacing = spacing {
addAll(children); addAll(children);
} }
double get extent => _extent; double get width => _width;
double _extent; double _width;
set extent(double value) { set width(double value) {
if (_extent == value) return; if (_width == value) return;
_extent = value; _width = value;
markNeedsLayout();
}
double get height => _height;
double _height;
set height(double value) {
if (_height == value) return;
_height = value;
markNeedsLayout(); markNeedsLayout();
} }
@ -339,7 +359,7 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox
} }
} }
double get intrinsicWidth => extent * childCount + spacing * (childCount - 1); double get intrinsicWidth => width * childCount + spacing * (childCount - 1);
@override @override
double computeMinIntrinsicWidth(double height) => intrinsicWidth; double computeMinIntrinsicWidth(double height) => intrinsicWidth;
@ -348,10 +368,10 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox
double computeMaxIntrinsicWidth(double height) => intrinsicWidth; double computeMaxIntrinsicWidth(double height) => intrinsicWidth;
@override @override
double computeMinIntrinsicHeight(double width) => extent; double computeMinIntrinsicHeight(double width) => height;
@override @override
double computeMaxIntrinsicHeight(double width) => extent; double computeMaxIntrinsicHeight(double width) => height;
@override @override
void performLayout() { void performLayout() {
@ -360,14 +380,14 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox
size = constraints.smallest; size = constraints.smallest;
return; return;
} }
size = Size(constraints.maxWidth, extent); size = Size(constraints.maxWidth, height);
final childConstraints = BoxConstraints.tight(Size(extent, extent)); final childConstraints = BoxConstraints.tight(Size(width, height));
var offset = Offset.zero; var offset = Offset.zero;
while (child != null) { while (child != null) {
child.layout(childConstraints, parentUsesSize: false); child.layout(childConstraints, parentUsesSize: false);
final childParentData = child.parentData! as _GridRowParentData; final childParentData = child.parentData! as _GridRowParentData;
childParentData.offset = offset; childParentData.offset = offset;
offset += Offset(extent + spacing, 0); offset += Offset(width + spacing, 0);
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
} }
@ -390,7 +410,8 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DoubleProperty('extent', extent)); properties.add(DoubleProperty('width', width));
properties.add(DoubleProperty('height', height));
properties.add(DoubleProperty('spacing', spacing)); properties.add(DoubleProperty('spacing', spacing));
} }
} }

View file

@ -6,8 +6,8 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.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/utils/constants.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -18,14 +18,28 @@ typedef OffsetFilterCallback = void Function(BuildContext context, CollectionFil
enum HeroType { always, onTap, never } enum HeroType { always, onTap, never }
@immutable
class AvesFilterDecoration {
final Widget widget;
final Radius radius;
const AvesFilterDecoration({
required this.widget,
required this.radius,
});
BorderRadius get textBorderRadius => BorderRadius.vertical(bottom: radius);
BorderRadius get chipBorderRadius => BorderRadius.all(radius);
}
class AvesFilterChip extends StatefulWidget { class AvesFilterChip extends StatefulWidget {
final CollectionFilter filter; final CollectionFilter filter;
final bool removable; final bool removable;
final bool showGenericIcon; final bool showGenericIcon;
final Widget? background; final AvesFilterDecoration? decoration;
final String? banner; final String? banner;
final Widget? details; final Widget? details;
final BorderRadius? borderRadius;
final double padding; final double padding;
final HeroType heroType; final HeroType heroType;
final FilterCallback? onTap; final FilterCallback? onTap;
@ -37,16 +51,18 @@ class AvesFilterChip extends StatefulWidget {
static const double minChipHeight = kMinInteractiveDimension; static const double minChipHeight = kMinInteractiveDimension;
static const double minChipWidth = 80; static const double minChipWidth = 80;
static const double maxChipWidth = 160; static const double maxChipWidth = 160;
static const double iconSize = 18;
static const double fontSize = 14;
static const double decoratedContentVerticalPadding = 5;
const AvesFilterChip({ const AvesFilterChip({
Key? key, Key? key,
required this.filter, required this.filter,
this.removable = false, this.removable = false,
this.showGenericIcon = true, this.showGenericIcon = true,
this.background, this.decoration,
this.banner, this.banner,
this.details, this.details,
this.borderRadius,
this.padding = 6.0, this.padding = 6.0,
this.heroType = HeroType.onTap, this.heroType = HeroType.onTap,
this.onTap, this.onTap,
@ -140,15 +156,15 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final chipBackground = Theme.of(context).scaffoldBackgroundColor;
final textScaleFactor = MediaQuery.textScaleFactorOf(context); final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final iconSize = 20 * textScaleFactor; final iconSize = AvesFilterChip.iconSize * textScaleFactor;
final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon);
final hasBackground = widget.background != null;
final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground);
final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null; final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null;
final decoration = widget.decoration;
Widget content = Row( Widget content = Row(
mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min, mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (leading != null) ...[ if (leading != null) ...[
@ -158,6 +174,9 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
Flexible( Flexible(
child: Text( child: Text(
filter.getLabel(context), filter.getLabel(context),
style: const TextStyle(
fontSize: AvesFilterChip.fontSize,
),
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
maxLines: 1, maxLines: 1,
@ -170,36 +189,37 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
], ],
); );
if (widget.details != null) { final details = widget.details;
if (details != null) {
content = Column( content = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
content, content,
Flexible(child: widget.details!), Flexible(child: details),
], ],
); );
} }
content = Padding( if (decoration != null) {
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2), content = Align(
child: content, alignment: Alignment.bottomCenter,
); child: ClipRRect(
borderRadius: decoration.textBorderRadius,
if (hasBackground) { child: Container(
content = Center( padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: AvesFilterChip.decoratedContentVerticalPadding),
child: ColoredBox( color: chipBackground,
color: Colors.black54,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyText2!.copyWith(
shadows: Constants.embossShadows,
),
child: content, child: content,
), ),
), ),
); );
} else {
content = Padding(
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2),
child: content,
);
} }
final borderRadius = widget.borderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)); final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
final banner = widget.banner; final banner = widget.banner;
Widget chip = Container( Widget chip = Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -210,13 +230,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
child: Stack( child: Stack(
fit: StackFit.passthrough, fit: StackFit.passthrough,
children: [ children: [
if (hasBackground) if (decoration != null)
ClipRRect( ClipRRect(
borderRadius: borderRadius, borderRadius: decoration.chipBorderRadius,
child: widget.background, child: decoration.widget,
), ),
Material( Material(
color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor, color: decoration != null ? Colors.transparent : chipBackground,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
@ -248,7 +268,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
), ),
position: DecorationPosition.foreground, position: DecorationPosition.foreground,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: decoration != null ? 0 : 8),
child: content, child: content,
), ),
); );
@ -279,9 +299,11 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
chip = Hero( chip = Hero(
tag: filter, tag: filter,
transitionOnUserGestures: true, transitionOnUserGestures: true,
child: DefaultTextStyle( child: MediaQueryDataProvider(
style: const TextStyle(), child: DefaultTextStyle(
child: chip, style: const TextStyle(),
child: chip,
),
), ),
); );
} }

View file

@ -19,14 +19,16 @@ class ScalerMetadata<T> {
class GridScaleGestureDetector<T> extends StatefulWidget { class GridScaleGestureDetector<T> extends StatefulWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final Widget Function(Offset center, double extent, Widget child) gridBuilder; final double Function(double width) heightForWidth;
final Widget Function(T item, double extent) scaledBuilder; final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
final Widget Function(T item, Size tileSize) scaledBuilder;
final Object Function(T item)? highlightItem; final Object Function(T item)? highlightItem;
final Widget child; final Widget child;
const GridScaleGestureDetector({ const GridScaleGestureDetector({
Key? key, Key? key,
required this.scrollableKey, required this.scrollableKey,
required this.heightForWidth,
required this.gridBuilder, required this.gridBuilder,
required this.scaledBuilder, required this.scaledBuilder,
this.highlightItem, this.highlightItem,
@ -38,9 +40,10 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
} }
class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T>> { class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T>> {
double? _startExtent, _extentMin, _extentMax; Size? _startSize;
double? _extentMin, _extentMax;
bool _applyingScale = false; bool _applyingScale = false;
ValueNotifier<double>? _scaledExtentNotifier; ValueNotifier<Size>? _scaledSizeNotifier;
OverlayEntry? _overlayEntry; OverlayEntry? _overlayEntry;
ScalerMetadata<T>? _metadata; ScalerMetadata<T>? _metadata;
@ -63,8 +66,8 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
// abort if we cannot find an image to show on overlay // abort if we cannot find an image to show on overlay
if (renderMetaData == null) return; if (renderMetaData == null) return;
_metadata = renderMetaData.metaData; _metadata = renderMetaData.metaData;
_startExtent = renderMetaData.size.width; _startSize = renderMetaData.size;
_scaledExtentNotifier = ValueNotifier(_startExtent!); _scaledSizeNotifier = ValueNotifier(_startSize!);
// not the same as `MediaQuery.size.width`, because of screen insets/padding // not the same as `MediaQuery.size.width`, because of screen insets/padding
final gridWidth = scrollableBox.size.width; final gridWidth = scrollableBox.size.width;
@ -73,33 +76,33 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
_extentMin = tileExtentController.effectiveExtentMin; _extentMin = tileExtentController.effectiveExtentMin;
_extentMax = tileExtentController.effectiveExtentMax; _extentMax = tileExtentController.effectiveExtentMax;
final halfExtent = _startExtent! / 2; final halfSize = _startSize! / 2;
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (context) => ScaleOverlay( builder: (context) => ScaleOverlay(
builder: (extent) => SizedBox( builder: (scaledTileSize) => SizedBox.fromSize(
width: extent, size: scaledTileSize,
height: extent,
child: GridTheme( child: GridTheme(
extent: extent, extent: scaledTileSize.width,
child: widget.scaledBuilder(_metadata!.item, extent), child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
), ),
), ),
center: thumbnailCenter, center: thumbnailCenter,
viewportWidth: gridWidth, viewportWidth: gridWidth,
gridBuilder: widget.gridBuilder, gridBuilder: widget.gridBuilder,
scaledExtentNotifier: _scaledExtentNotifier!, scaledSizeNotifier: _scaledSizeNotifier!,
), ),
); );
Overlay.of(scrollableContext)!.insert(_overlayEntry!); Overlay.of(scrollableContext)!.insert(_overlayEntry!);
}, },
onScaleUpdate: (details) { onScaleUpdate: (details) {
if (_scaledExtentNotifier == null) return; if (_scaledSizeNotifier == null) return;
final s = details.scale; final s = details.scale;
_scaledExtentNotifier!.value = (_startExtent! * s).clamp(_extentMin!, _extentMax!); final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
}, },
onScaleEnd: (details) { onScaleEnd: (details) {
if (_scaledExtentNotifier == null) return; if (_scaledSizeNotifier == null) return;
if (_overlayEntry != null) { if (_overlayEntry != null) {
_overlayEntry!.remove(); _overlayEntry!.remove();
_overlayEntry = null; _overlayEntry = null;
@ -109,8 +112,8 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
final tileExtentController = context.read<TileExtentController>(); final tileExtentController = context.read<TileExtentController>();
final oldExtent = tileExtentController.extentNotifier.value; final oldExtent = tileExtentController.extentNotifier.value;
// sanitize and update grid layout if necessary // sanitize and update grid layout if necessary
final newExtent = tileExtentController.setUserPreferredExtent(_scaledExtentNotifier!.value); final newExtent = tileExtentController.setUserPreferredExtent(_scaledSizeNotifier!.value.width);
_scaledExtentNotifier = null; _scaledSizeNotifier = null;
if (newExtent == oldExtent) { if (newExtent == oldExtent) {
_applyingScale = false; _applyingScale = false;
} else { } else {
@ -138,18 +141,18 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
} }
class ScaleOverlay extends StatefulWidget { class ScaleOverlay extends StatefulWidget {
final Widget Function(double extent) builder; final Widget Function(Size scaledTileSize) builder;
final Offset center; final Offset center;
final double viewportWidth; final double viewportWidth;
final ValueNotifier<double> scaledExtentNotifier; final ValueNotifier<Size> scaledSizeNotifier;
final Widget Function(Offset center, double extent, Widget child) gridBuilder; final Widget Function(Offset center, Size extent, Widget child) gridBuilder;
const ScaleOverlay({ const ScaleOverlay({
Key? key, Key? key,
required this.builder, required this.builder,
required this.center, required this.center,
required this.viewportWidth, required this.viewportWidth,
required this.scaledExtentNotifier, required this.scaledSizeNotifier,
required this.gridBuilder, required this.gridBuilder,
}) : super(key: key); }) : super(key: key);
@ -197,26 +200,28 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
), ),
), ),
duration: Durations.collectionScalingBackgroundAnimation, duration: Durations.collectionScalingBackgroundAnimation,
child: ValueListenableBuilder<double>( child: ValueListenableBuilder<Size>(
valueListenable: widget.scaledExtentNotifier, valueListenable: widget.scaledSizeNotifier,
builder: (context, extent, child) { builder: (context, scaledSize, child) {
final width = scaledSize.width;
final height = scaledSize.height;
// keep scaled thumbnail within the screen // keep scaled thumbnail within the screen
final xMin = context.select<MediaQueryData, double>((mq) => mq.padding.left); final xMin = context.select<MediaQueryData, double>((mq) => mq.padding.left);
final xMax = xMin + gridWidth; final xMax = xMin + gridWidth;
var dx = .0; var dx = .0;
if (center.dx - extent / 2 < xMin) { if (center.dx - width / 2 < xMin) {
dx = xMin - (center.dx - extent / 2); dx = xMin - (center.dx - width / 2);
} else if (center.dx + extent / 2 > xMax) { } else if (center.dx + width / 2 > xMax) {
dx = xMax - (center.dx + extent / 2); dx = xMax - (center.dx + width / 2);
} }
final clampedCenter = center.translate(dx, 0); final clampedCenter = center.translate(dx, 0);
var child = widget.builder(extent); var child = widget.builder(scaledSize);
child = Stack( child = Stack(
children: [ children: [
Positioned( Positioned(
left: clampedCenter.dx - extent / 2, left: clampedCenter.dx - width / 2,
top: clampedCenter.dy - extent / 2, top: clampedCenter.dy - height / 2,
child: DefaultTextStyle( child: DefaultTextStyle(
style: const TextStyle(), style: const TextStyle(),
child: child, child: child,
@ -224,7 +229,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
), ),
], ],
); );
child = widget.gridBuilder(clampedCenter, extent, child); child = widget.gridBuilder(clampedCenter, scaledSize, child);
return child; return child;
}, },
), ),
@ -237,13 +242,14 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
class GridPainter extends CustomPainter { class GridPainter extends CustomPainter {
final Offset center; final Offset center;
final double extent, spacing, borderWidth; final Size tileSize;
final double spacing, borderWidth;
final Radius borderRadius; final Radius borderRadius;
final Color color; final Color color;
const GridPainter({ const GridPainter({
required this.center, required this.center,
required this.extent, required this.tileSize,
required this.spacing, required this.spacing,
required this.borderWidth, required this.borderWidth,
required this.borderRadius, required this.borderRadius,
@ -252,12 +258,15 @@ class GridPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final tileWidth = tileSize.width;
final tileHeight = tileSize.height;
final strokePaint = Paint() final strokePaint = Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = borderWidth ..strokeWidth = borderWidth
..shader = ui.Gradient.radial( ..shader = ui.Gradient.radial(
center, center,
extent * 2, tileWidth * 2,
[ [
color, color,
Colors.transparent, Colors.transparent,
@ -271,17 +280,18 @@ class GridPainter extends CustomPainter {
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..color = color.withOpacity(.25); ..color = color.withOpacity(.25);
final delta = extent + spacing; final deltaX = tileWidth + spacing;
final deltaY = tileHeight + spacing;
for (var i = -2; i <= 2; i++) { for (var i = -2; i <= 2; i++) {
final dx = delta * i; final dx = deltaX * i;
for (var j = -2; j <= 2; j++) { for (var j = -2; j <= 2; j++) {
if (i == 0 && j == 0) continue; if (i == 0 && j == 0) continue;
final dy = delta * j; final dy = deltaY * j;
final rect = RRect.fromRectAndRadius( final rect = RRect.fromRectAndRadius(
Rect.fromCenter( Rect.fromCenter(
center: center + Offset(dx, dy), center: center + Offset(dx, dy),
width: extent, width: tileWidth,
height: extent, height: tileHeight,
), ),
borderRadius, borderRadius,
); );

View file

@ -12,6 +12,7 @@ import 'package:aves/model/source/tag.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/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
@ -40,6 +41,22 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
}) : thumbnailExtent = thumbnailExtent ?? extent, }) : thumbnailExtent = thumbnailExtent ?? extent,
super(key: key); super(key: key);
static double tileHeight({required double extent, required double textScaleFactor}) {
return extent + infoHeight(extent: extent, textScaleFactor: textScaleFactor);
}
static double infoHeight({required double extent, required double textScaleFactor}) {
// height can actually be a little larger or smaller, when info includes icons or non-latin scripts
// but it's not worth measuring text metrics, as the widget is flexible enough to absorb the difference
return (AvesFilterChip.fontSize + detailFontSize(extent) + 4) * textScaleFactor + AvesFilterChip.decoratedContentVerticalPadding * 2;
}
static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4));
static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 8);
static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 6);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<CollectionSource>( return Consumer<CollectionSource>(
@ -50,7 +67,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
final album = (filter as AlbumFilter).album; final album = (filter as AlbumFilter).album;
return StreamBuilder<AlbumSummaryInvalidatedEvent>( return StreamBuilder<AlbumSummaryInvalidatedEvent>(
stream: source.eventBus.on<AlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories!.contains(album)), stream: source.eventBus.on<AlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories!.contains(album)),
builder: (context, snapshot) => _buildChip(source), builder: (context, snapshot) => _buildChip(context, source),
); );
} }
case LocationFilter: case LocationFilter:
@ -58,7 +75,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
final countryCode = (filter as LocationFilter).countryCode; final countryCode = (filter as LocationFilter).countryCode;
return StreamBuilder<CountrySummaryInvalidatedEvent>( return StreamBuilder<CountrySummaryInvalidatedEvent>(
stream: source.eventBus.on<CountrySummaryInvalidatedEvent>().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)), stream: source.eventBus.on<CountrySummaryInvalidatedEvent>().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)),
builder: (context, snapshot) => _buildChip(source), builder: (context, snapshot) => _buildChip(context, source),
); );
} }
case TagFilter: case TagFilter:
@ -66,7 +83,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
final tag = (filter as TagFilter).tag; final tag = (filter as TagFilter).tag;
return StreamBuilder<TagSummaryInvalidatedEvent>( return StreamBuilder<TagSummaryInvalidatedEvent>(
stream: source.eventBus.on<TagSummaryInvalidatedEvent>().where((event) => event.tags == null || event.tags!.contains(tag)), stream: source.eventBus.on<TagSummaryInvalidatedEvent>().where((event) => event.tags == null || event.tags!.contains(tag)),
builder: (context, snapshot) => _buildChip(source), builder: (context, snapshot) => _buildChip(context, source),
); );
} }
default: default:
@ -76,38 +93,53 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
); );
} }
static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4)); Widget _buildChip(BuildContext context, CollectionSource source) {
Widget _buildChip(CollectionSource source) {
final entry = coverEntry ?? source.coverEntry(filter); final entry = coverEntry ?? source.coverEntry(filter);
final backgroundImage = entry == null
? Container(color: Colors.white)
: ThumbnailImage(
entry: entry,
extent: thumbnailExtent,
);
final titlePadding = min<double>(4.0, extent / 32); final titlePadding = min<double>(4.0, extent / 32);
return SizedBox( return AvesFilterChip(
width: extent, filter: filter,
height: extent, showGenericIcon: false,
child: AvesFilterChip( decoration: AvesFilterDecoration(
filter: filter, widget: Selector<MediaQueryData, double>(
showGenericIcon: false, selector: (context, mq) => mq.textScaleFactor,
background: backgroundImage, builder: (context, textScaleFactor, child) {
banner: banner, return Padding(
details: _buildDetails(source, filter), padding: EdgeInsets.only(bottom: infoHeight(extent: extent, textScaleFactor: textScaleFactor)),
borderRadius: BorderRadius.all(radius(extent)), child: child,
padding: titlePadding, );
onTap: onTap, },
onLongPress: null, child: entry == null
? Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
stringToColor(filter.getLabel(context)),
],
),
),
)
: ThumbnailImage(
entry: entry,
extent: thumbnailExtent,
),
),
radius: radius(extent),
), ),
banner: banner,
details: _buildDetails(source, filter),
padding: titlePadding,
onTap: onTap,
onLongPress: null,
); );
} }
Widget _buildDetails(CollectionSource source, T filter) { Widget _buildDetails(CollectionSource source, T filter) {
final padding = min<double>(8.0, extent / 16); final padding = min<double>(8.0, extent / 16);
final iconSize = min<double>(14.0, extent / 8); final iconSize = detailIconSize(extent);
final fontSize = min<double>(14.0, extent / 6); final fontSize = detailFontSize(extent);
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View file

@ -238,56 +238,66 @@ 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) {
return GridTheme( return Selector<TileExtentController, Tuple3<double, int, double>>(
extent: tileExtent, selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
child: Selector<TileExtentController, Tuple3<double, int, double>>( builder: (context, c, child) {
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), final scrollableWidth = c.item1;
builder: (context, c, child) { final columnCount = c.item2;
final scrollableWidth = c.item1; final tileSpacing = c.item3;
final columnCount = c.item2; // do not listen for animation delay change
final tileSpacing = c.item3; final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
// do not listen for animation delay change return Selector<MediaQueryData, double>(
final controller = Provider.of<TileExtentController>(context, listen: false); selector: (context, mq) => mq.textScaleFactor,
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget); builder: (context, textScaleFactor, child) {
return SectionedFilterListLayoutProvider<T>( final tileHeight = CoveredFilterChip.tileHeight(extent: tileExtent, textScaleFactor: textScaleFactor);
sections: visibleSections, return GridTheme(
showHeaders: showHeaders, extent: tileExtent,
scrollableWidth: scrollableWidth, child: SectionedFilterListLayoutProvider<T>(
columnCount: columnCount, sections: visibleSections,
spacing: tileSpacing, showHeaders: showHeaders,
tileExtent: tileExtent, scrollableWidth: scrollableWidth,
tileBuilder: (gridItem) { columnCount: columnCount,
final filter = gridItem.filter; spacing: tileSpacing,
return MetaData( tileWidth: tileExtent,
metaData: ScalerMetadata(gridItem), tileHeight: tileHeight,
child: FilterChipGridDecorator<T, FilterGridItem<T>>( tileBuilder: (gridItem) {
gridItem: gridItem, final filter = gridItem.filter;
extent: tileExtent, return MetaData(
child: CoveredFilterChip( metaData: ScalerMetadata(gridItem),
key: Key(filter.key), child: FilterChipGridDecorator<T, FilterGridItem<T>>(
filter: filter, gridItem: gridItem,
extent: tileExtent, extent: tileExtent,
pinned: pinnedFilters.contains(filter), child: CoveredFilterChip(
banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null, key: Key(filter.key),
onTap: onTap, filter: filter,
extent: tileExtent,
pinned: pinnedFilters.contains(filter),
banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null,
onTap: onTap,
),
), ),
), );
); },
}, tileAnimationDelay: tileAnimationDelay,
tileAnimationDelay: tileAnimationDelay, child: child!,
child: _FilterSectionedContent<T>(
appBar: appBar,
appBarHeightNotifier: _appBarHeightNotifier,
visibleSections: visibleSections,
sortFactor: sortFactor,
selectable: selectable,
emptyBuilder: emptyBuilder,
scrollController: PrimaryScrollController.of(context)!,
), ),
); );
}), },
child: child,
);
},
child: child,
); );
}, },
child: _FilterSectionedContent<T>(
appBar: appBar,
appBarHeightNotifier: _appBarHeightNotifier,
visibleSections: visibleSections,
sortFactor: sortFactor,
selectable: selectable,
emptyBuilder: emptyBuilder,
scrollController: PrimaryScrollController.of(context)!,
),
); );
return sectionedListLayoutProvider; return sectionedListLayoutProvider;
}, },
@ -399,24 +409,26 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pinnedFilters = settings.pinnedFilters; final pinnedFilters = settings.pinnedFilters;
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing); final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
return GridScaleGestureDetector<FilterGridItem<T>>( return GridScaleGestureDetector<FilterGridItem<T>>(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
gridBuilder: (center, extent, child) => CustomPaint( heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor),
gridBuilder: (center, tileSize, child) => CustomPaint(
painter: GridPainter( painter: GridPainter(
center: center, center: center,
extent: extent, tileSize: tileSize,
spacing: tileSpacing, spacing: tileSpacing,
borderWidth: AvesFilterChip.outlineWidth, borderWidth: AvesFilterChip.outlineWidth,
borderRadius: CoveredFilterChip.radius(extent), borderRadius: CoveredFilterChip.radius(tileSize.width),
color: Colors.grey.shade700, color: Colors.grey.shade700,
), ),
child: child, child: child,
), ),
scaledBuilder: (item, extent) { scaledBuilder: (item, tileSize) {
final filter = item.filter; final filter = item.filter;
return CoveredFilterChip( return CoveredFilterChip(
filter: filter, filter: filter,
extent: extent, extent: tileSize.width,
thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax, thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
pinned: pinnedFilters.contains(filter), pinned: pinnedFilters.contains(filter),
); );

View file

@ -13,7 +13,8 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
required double scrollableWidth, required double scrollableWidth,
required int columnCount, required int columnCount,
required double spacing, required double spacing,
required double tileExtent, required double tileWidth,
required double tileHeight,
required Widget Function(FilterGridItem<T> gridItem) tileBuilder, required Widget Function(FilterGridItem<T> gridItem) tileBuilder,
required Duration tileAnimationDelay, required Duration tileAnimationDelay,
required Widget child, required Widget child,
@ -22,7 +23,8 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
columnCount: columnCount, columnCount: columnCount,
spacing: spacing, spacing: spacing,
tileExtent: tileExtent, tileWidth: tileWidth,
tileHeight: tileHeight,
tileBuilder: tileBuilder, tileBuilder: tileBuilder,
tileAnimationDelay: tileAnimationDelay, tileAnimationDelay: tileAnimationDelay,
child: child, child: child,