Skip to content

Instantly share code, notes, and snippets.

@pingbird
Created January 28, 2026 01:01
Show Gist options
  • Select an option

  • Save pingbird/5bc9ae354d91b2c831cf6509e05b7470 to your computer and use it in GitHub Desktop.

Select an option

Save pingbird/5bc9ae354d91b2c831cf6509e05b7470 to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'package:boxy/boxy.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class SliverMosaic extends StatelessWidget {
const SliverMosaic({
Key? key,
required this.buildChild,
required this.childCount,
required this.maxCrossAxisExtent,
required this.aspectRatio,
required this.mainSpacing,
required this.crossSpacing,
}) : super(key: key);
final IndexedWidgetBuilder buildChild;
final int childCount;
final double maxCrossAxisExtent;
final double Function(int) aspectRatio;
final double mainSpacing;
final double crossSpacing;
@override
Widget build(BuildContext context) {
return CustomBoxy.sliver(
delegate: MosaicBoxy(
buildChild: buildChild,
childCount: childCount,
maxCrossAxisExtent: maxCrossAxisExtent,
aspectRatio: aspectRatio,
mainSpacing: mainSpacing,
crossSpacing: crossSpacing,
),
);
}
}
class MosaicController {
var lastCrossSize = 0.0;
var lastCols = 0;
final cachedChildren = <int, Widget>{};
final cachedChildCols = <int>[];
final cachedAspect = <double>[];
final cachedCols = <List<int>>[];
final cachedOffsets = <List<double>>[];
IndexedWidgetBuilder? lastBuildChild;
void updateCrossSize(double crossSize, int numCols) {
if (crossSize == lastCrossSize) return;
lastCols = numCols;
lastCrossSize = crossSize;
cachedChildCols.clear();
cachedCols.clear();
cachedCols.addAll(Iterable.generate(numCols, (i) => []));
cachedOffsets.clear();
cachedOffsets.addAll(Iterable.generate(numCols, (i) => []));
}
Widget getChildAt(IndexedWidgetBuilder builder, int index) {
if (builder != lastBuildChild) {
cachedChildren.clear();
}
return cachedChildren[index] ??=
Builder(builder: (context) => builder(context, index));
}
double calculateSizes(
double end,
int childCount,
double Function(int) aspectRatio,
) {
var minExtent = cachedOffsets.fold<double>(
0.0, (n, e) => min(n, (e.isEmpty ? 0.0 : e.last)));
while (minExtent < end && cachedChildCols.length != childCount) {
final i = cachedChildCols.length;
while (cachedAspect.length < i + 1) {
final aspect = aspectRatio(cachedAspect.length);
cachedAspect.add(aspect);
}
final height = 1 / cachedAspect[i];
var minCol = 0;
for (var col = 1; col < lastCols; col++) {
if ((cachedOffsets[col].lastOrNull ?? 0.0) <
(cachedOffsets[minCol].lastOrNull ?? 0.0)) {
minCol = col;
}
}
cachedChildCols.add(minCol);
cachedCols[minCol].add(i);
final offset = (cachedOffsets[minCol].lastOrNull ?? 0.0) + height;
cachedOffsets[minCol].add(offset);
minExtent = min(minExtent, offset);
}
return minExtent;
}
int findColEnd(int col, double end, double spacing) {
final offsets = cachedOffsets[col];
int n = 0;
int m = offsets.length - 1;
while (n < m) {
final int mid = n + ((m - n) >> 1);
final double offset = offsets[mid] + (spacing * mid);
final int comp = offset.compareTo(end);
if (comp < 0) {
n = mid + 1;
} else {
m = mid;
}
}
return min(n + 1, offsets.length - 1);
}
}
class MosaicBoxy extends SliverBoxyDelegate<MosaicController> {
MosaicBoxy({
required this.buildChild,
required this.childCount,
required this.maxCrossAxisExtent,
required this.aspectRatio,
required this.mainSpacing,
required this.crossSpacing,
});
final IndexedWidgetBuilder buildChild;
final int childCount;
final double maxCrossAxisExtent;
final double Function(int) aspectRatio;
final double mainSpacing;
final double crossSpacing;
MosaicController get controller => layoutData ??= MosaicController();
@override
SliverGeometry layout() {
final crossSize = constraints.crossAxisExtent;
final numCols = (crossSize / maxCrossAxisExtent).ceil();
final colWidth = crossSize / numCols;
final childWidth = (crossSize - crossSpacing * (numCols - 1)) / numCols;
controller.updateCrossSize(crossSize, numCols);
final startOffset = constraints.scrollOffset + constraints.cacheOrigin;
final endOffset = constraints.scrollOffset +
constraints.cacheOrigin +
constraints.remainingCacheExtent;
controller.calculateSizes(
endOffset / childWidth,
childCount,
aspectRatio,
);
for (var col = 0; col < numCols; col++) {
for (var i = controller.findColEnd(
col,
endOffset / childWidth,
mainSpacing / childWidth,
);
i >= 0;
i--) {
final cachedOffset = controller.cachedOffsets[col][i];
final childIndex = controller.cachedCols[col][i];
final childHeight = childWidth / controller.cachedAspect[childIndex];
final mainOffset = i * mainSpacing +
controller.cachedOffsets[col][i] * childWidth -
(childHeight + constraints.scrollOffset);
if (mainOffset + childHeight < 0) break;
final child = inflate<BoxyChild>(
controller.getChildAt(buildChild, childIndex),
id: childIndex,
);
child.layout(
BoxConstraints.tight(Size(
childWidth,
childHeight,
)),
);
child.positionOnAxis(
col * childWidth + col * crossSpacing,
mainOffset,
);
}
}
final maxExtent = controller.cachedOffsets.fold<double>(
0.0,
(n, e) => max(n,
(e.lastOrNull ?? 0.0) * childWidth + mainSpacing * (e.length - 1)));
final estimatedExtent = maxExtent == 0
? 0.0
: (maxExtent / controller.cachedChildCols.length) * childCount;
return SliverGeometry(
scrollExtent: estimatedExtent,
paintExtent: render.calculatePaintOffset(
constraints,
from: 0.0,
to: estimatedExtent,
),
maxPaintExtent: estimatedExtent,
hasVisualOverflow: true,
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment