aves/lib/widgets/common/grid/sections/mosaic/section_layout_builder.dart
2023-10-01 00:48:51 +02:00

198 lines
6.9 KiB
Dart

import 'dart:math';
import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:aves/widgets/common/grid/sections/mosaic/list_layout.dart';
import 'package:aves/widgets/common/grid/sections/mosaic/row.dart';
import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart';
import 'package:aves/widgets/common/grid/sections/provider.dart';
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MosaicSectionLayoutBuilder<T> extends SectionLayoutBuilder<T> {
int _currentIndex = 0;
double _currentOffset = 0;
late double Function(int itemCount) rowAvailableWidth;
late double rowHeightMax;
final CoverRatioResolver<T> coverRatioResolver;
static const double heightMaxFactor = 2.4;
static const double minThumbnailAspectRatio = 9 / 32;
static const double maxThumbnailAspectRatio = 32 / 9;
MosaicSectionLayoutBuilder({
required super.sections,
required super.showHeaders,
required super.getHeaderExtent,
required super.buildHeader,
required super.scrollableWidth,
required super.tileLayout,
required super.columnCount,
required super.spacing,
required super.horizontalPadding,
required super.tileWidth,
required super.tileHeight,
required super.tileBuilder,
required Duration tileAnimationDelay,
required this.coverRatioResolver,
}) : super(tileAnimationDelay: Duration(milliseconds: (tileAnimationDelay.inMilliseconds / columnCount).ceil())) {
final rowWidth = scrollableWidth - horizontalPadding * 2;
rowAvailableWidth = (itemCount) => rowWidth - (itemCount - 1) * spacing;
rowHeightMax = tileWidth * heightMaxFactor;
}
@override
SectionedListLayout<T> updateLayouts(BuildContext context) {
final sectionLayouts = sections.keys
.map((sectionKey) => buildSectionLayout(
headerExtent: showHeaders ? getHeaderExtent(context, sectionKey) : 0.0,
sectionKey: sectionKey,
section: sections[sectionKey]!,
animate: animate,
))
.toList();
return MosaicSectionedListLayout<T>(
sections: sections,
showHeaders: showHeaders,
spacing: spacing,
horizontalPadding: horizontalPadding,
sectionLayouts: sectionLayouts,
);
}
@override
SectionLayout buildSectionLayout({
required double headerExtent,
required SectionKey sectionKey,
required List<T> section,
required bool animate,
}) {
final rows = computeMosaicRows(
section: section,
availableWidthFor: rowAvailableWidth,
heightMax: rowHeightMax,
targetExtent: tileWidth,
spacing: spacing,
bottom: bottom,
coverRatioResolver: (item) => coverRatioResolver(item).clamp(minThumbnailAspectRatio, maxThumbnailAspectRatio),
);
final rowCount = rows.length;
final sectionChildCount = 1 + rowCount;
final sectionFirstIndex = _currentIndex;
_currentIndex += sectionChildCount;
final sectionLastIndex = _currentIndex - 1;
final sectionMinOffset = _currentOffset;
_currentOffset += headerExtent + rows.map((v) => v.height).sum - spacing;
final sectionMaxOffset = _currentOffset;
return MosaicSectionLayout(
sectionKey: sectionKey,
firstIndex: sectionFirstIndex,
lastIndex: sectionLastIndex,
minOffset: sectionMinOffset,
maxOffset: sectionMaxOffset,
headerExtent: headerExtent,
rows: rows,
spacing: spacing,
builder: (context, listIndex) {
final textDirection = Directionality.of(context);
final sectionChildIndex = listIndex - sectionFirstIndex;
final isHeader = sectionChildIndex == 0;
final row = isHeader ? rows.first : rows[sectionChildIndex - 1];
final sectionGridIndex = isHeader ? sectionFirstIndex * columnCount : (sectionChildIndex + 1) * columnCount + row.firstIndex;
return buildSectionWidget(
context: context,
section: section,
sectionGridIndex: sectionGridIndex,
sectionChildIndex: sectionChildIndex,
itemIndexRange: () => isHeader ? const (0, 0) : (row.firstIndex, row.lastIndex + 1),
sectionKey: sectionKey,
headerExtent: headerExtent,
itemSizes: row.itemWidths.map((v) => Size(v, row.height)).toList(),
animate: animate,
buildGridRow: (children) {
return isHeader
? const SizedBox()
: MosaicGridRow(
rowLayout: row,
spacing: spacing,
textDirection: textDirection,
children: children,
);
},
);
},
);
}
static List<MosaicRowLayout> computeMosaicRows<T>({
required List<T> section,
required double Function(int itemCount) availableWidthFor,
required double heightMax,
required double targetExtent,
required double spacing,
required double bottom,
required CoverRatioResolver<T> coverRatioResolver,
}) {
final rows = <MosaicRowLayout>[];
final items = <T>[];
double ratioSum = 0, ratioMin = double.infinity;
int firstIndex = 0;
double minOffset = 0;
void addRow(int i, {required bool complete}) {
if (items.isEmpty) return;
final availableWidth = availableWidthFor(items.length);
var height = availableWidth / ratioSum + spacing;
if (height > heightMax + precisionErrorTolerance) {
if (!complete) {
ratioSum = availableWidth / (heightMax - spacing);
addRow(i, complete: complete);
}
return;
}
height += bottom;
rows.add(MosaicRowLayout(
firstIndex: firstIndex,
lastIndex: i - 1,
minOffset: minOffset,
height: height,
itemWidths: items.map((item) => availableWidth * coverRatioResolver(item) / ratioSum).toList(),
));
firstIndex = i;
minOffset += height;
ratioMin = double.infinity;
ratioSum = 0;
items.clear();
}
section.forEachIndexed((i, item) {
final ratio = coverRatioResolver(item);
final nextAvailableWidth = availableWidthFor(items.length + 1);
final nextRatioSum = ratio + ratioSum;
final nextItemMinWidth = nextAvailableWidth * min(ratio, ratioMin) / nextRatioSum;
final nextHeight = nextAvailableWidth / nextRatioSum + spacing;
if (nextItemMinWidth < targetExtent || nextHeight < targetExtent) {
// add row when appending the next item would make other items too small
addRow(i, complete: true);
}
items.add(item);
ratioMin = min(ratio, ratioMin);
ratioSum += ratio;
});
if (items.isNotEmpty) {
// add last row, possibly incomplete
addRow(section.length, complete: false);
}
return rows;
}
}