#38 changed decorated filter layout, with label below cover
This commit is contained in:
parent
8f2a0a8247
commit
5939668fd5
9 changed files with 318 additions and 216 deletions
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue