408 lines
12 KiB
Dart
408 lines
12 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
typedef WrapBuilder = Widget Function(Widget child);
|
|
|
|
class Grid extends MultiChildRenderObjectWidget {
|
|
final double mainAxisSpacing;
|
|
|
|
final double crossAxisSpacing;
|
|
|
|
final double? mainAxisExtent;
|
|
|
|
final int crossAxisCount;
|
|
|
|
final AxisDirection axisDirection;
|
|
|
|
final TextDirection textDirection;
|
|
|
|
const Grid({
|
|
super.key,
|
|
this.mainAxisSpacing = 0,
|
|
this.crossAxisSpacing = 0,
|
|
int? crossAxisCount,
|
|
AxisDirection? axisDirection,
|
|
TextDirection? textDirection,
|
|
this.mainAxisExtent,
|
|
List<Widget>? children,
|
|
}) : crossAxisCount = crossAxisCount ?? 1,
|
|
axisDirection = axisDirection ?? AxisDirection.down,
|
|
textDirection = textDirection ?? TextDirection.ltr,
|
|
super(children: children ?? const []);
|
|
|
|
const Grid.baseGap({
|
|
Key? key,
|
|
double mainAxisSpacing = 8.0,
|
|
double crossAxisSpacing = 8.0,
|
|
int? crossAxisCount,
|
|
AxisDirection? axisDirection,
|
|
TextDirection? textDirection,
|
|
double? mainAxisExtent,
|
|
List<Widget>? children,
|
|
}) : this(
|
|
key: key,
|
|
mainAxisSpacing: mainAxisSpacing,
|
|
crossAxisSpacing: crossAxisSpacing,
|
|
crossAxisCount: crossAxisCount,
|
|
axisDirection: axisDirection,
|
|
textDirection: textDirection,
|
|
mainAxisExtent: mainAxisExtent,
|
|
children: children,
|
|
);
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return RenderGrid(
|
|
textDirection: textDirection,
|
|
crossAxisCount: crossAxisCount,
|
|
mainAxisSpacing: mainAxisSpacing,
|
|
crossAxisSpacing: crossAxisSpacing,
|
|
axisDirection: axisDirection,
|
|
mainAxisExtent: mainAxisExtent,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, RenderGrid renderObject) {
|
|
renderObject
|
|
..mainAxisSpacing = mainAxisSpacing
|
|
..mainAxisExtent = mainAxisExtent
|
|
..crossAxisSpacing = crossAxisSpacing
|
|
..textDirection = textDirection
|
|
..axisDirection = axisDirection
|
|
..crossAxisCount = crossAxisCount;
|
|
}
|
|
}
|
|
|
|
class RenderGrid extends RenderBox
|
|
with
|
|
ContainerRenderObjectMixin<RenderBox, GridParentData>,
|
|
RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
|
|
RenderGrid({
|
|
required double mainAxisSpacing,
|
|
required double crossAxisSpacing,
|
|
required int crossAxisCount,
|
|
required AxisDirection axisDirection,
|
|
required TextDirection textDirection,
|
|
double? mainAxisExtent,
|
|
}) : _crossAxisCount = crossAxisCount,
|
|
_crossAxisSpacing = crossAxisSpacing,
|
|
_mainAxisSpacing = mainAxisSpacing,
|
|
_axisDirection = axisDirection,
|
|
_textDirection = textDirection,
|
|
_mainAxisExtent = mainAxisExtent;
|
|
|
|
int _crossAxisCount;
|
|
|
|
int get crossAxisCount => _crossAxisCount;
|
|
|
|
set crossAxisCount(int value) {
|
|
if (_crossAxisCount != value) {
|
|
_crossAxisCount = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
double? _mainAxisExtent;
|
|
|
|
double? get mainAxisExtent => _mainAxisExtent;
|
|
|
|
set mainAxisExtent(double? value) {
|
|
if (_mainAxisExtent != value) {
|
|
_mainAxisExtent = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
double _mainAxisSpacing;
|
|
|
|
double get mainAxisSpacing => _mainAxisSpacing;
|
|
|
|
set mainAxisSpacing(double value) {
|
|
if (_mainAxisSpacing != value) {
|
|
_mainAxisSpacing = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
double _crossAxisSpacing;
|
|
|
|
double get crossAxisSpacing => _crossAxisSpacing;
|
|
|
|
set crossAxisSpacing(double value) {
|
|
if (_crossAxisSpacing != value) {
|
|
_crossAxisSpacing = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
AxisDirection _axisDirection;
|
|
|
|
AxisDirection get axisDirection => _axisDirection;
|
|
|
|
set axisDirection(AxisDirection value) {
|
|
if (_axisDirection != value) {
|
|
_axisDirection = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
TextDirection _textDirection;
|
|
|
|
TextDirection get textDirection => _textDirection;
|
|
|
|
set textDirection(TextDirection value) {
|
|
if (_textDirection != value) {
|
|
_textDirection = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
Axis get mainAxis => axisDirectionToAxis(_axisDirection);
|
|
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! GridParentData) {
|
|
child.parentData = GridParentData();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
final requestedSize = _computeSize(constraints: constraints);
|
|
size = constraints.constrain(requestedSize);
|
|
_hasOverflow = size != requestedSize;
|
|
}
|
|
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
|
return defaultHitTestChildren(result, position: position);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (_hasOverflow) {
|
|
context.pushClipRect(
|
|
needsCompositing,
|
|
offset,
|
|
Offset.zero & size,
|
|
defaultPaint,
|
|
);
|
|
} else {
|
|
defaultPaint(context, offset);
|
|
}
|
|
}
|
|
|
|
GridParentData _getParentData(RenderBox child) {
|
|
return child.parentData as GridParentData;
|
|
}
|
|
|
|
void _layoutChild(
|
|
RenderBox child,
|
|
BoxConstraints constraints, {
|
|
bool parentUsesSize = false,
|
|
}) {
|
|
child.layout(constraints, parentUsesSize: parentUsesSize);
|
|
}
|
|
|
|
int _computeCrossAxisCellCount(
|
|
GridParentData childParentData,
|
|
int crossAxisCount,
|
|
) {
|
|
return math.min(childParentData.crossAxisCellCount ?? 1, crossAxisCount);
|
|
}
|
|
|
|
Size _computeSize({required BoxConstraints constraints}) {
|
|
final crossAxisExtent = mainAxis == Axis.vertical
|
|
? constraints.maxWidth
|
|
: constraints.maxHeight;
|
|
final stride = (crossAxisExtent + crossAxisSpacing) / crossAxisCount;
|
|
final offsets = List.filled(crossAxisCount, 0.0);
|
|
RenderBox? child = firstChild;
|
|
while (child != null) {
|
|
final childParentData = _getParentData(child);
|
|
final crossAxisCellCount = _computeCrossAxisCellCount(
|
|
childParentData,
|
|
crossAxisCount,
|
|
);
|
|
final crossAxisExtent = stride * crossAxisCellCount - crossAxisSpacing;
|
|
final shouldFitContent = childParentData.mainAxisCellCount == null;
|
|
|
|
double mainAxisExtent = 0;
|
|
|
|
if (shouldFitContent) {
|
|
final childConstraints = mainAxis == Axis.vertical
|
|
? BoxConstraints.tightFor(width: crossAxisExtent)
|
|
: BoxConstraints.tightFor(height: crossAxisExtent);
|
|
_layoutChild(child, childConstraints, parentUsesSize: true);
|
|
mainAxisExtent = mainAxis == Axis.vertical
|
|
? child.size.height
|
|
: child.size.width;
|
|
} else {
|
|
final mainAxisCellCount = childParentData.mainAxisCellCount ?? 1;
|
|
mainAxisExtent =
|
|
(this.mainAxisExtent ?? stride) * mainAxisCellCount -
|
|
mainAxisSpacing;
|
|
childParentData.realMainAxisExtent = mainAxisExtent;
|
|
final childSize = mainAxis == Axis.vertical
|
|
? Size(crossAxisExtent, mainAxisExtent)
|
|
: Size(mainAxisExtent, crossAxisExtent);
|
|
final childConstraints = BoxConstraints.tight(childSize);
|
|
_layoutChild(child, childConstraints);
|
|
}
|
|
final origin = _getOrigin(offsets, crossAxisCellCount);
|
|
final mainAxisOffset = origin.mainAxisOffset;
|
|
final crossAxisOffset = origin.crossAxisIndex * stride;
|
|
final offset = mainAxis == Axis.vertical
|
|
? Offset(crossAxisOffset, mainAxisOffset)
|
|
: Offset(mainAxisOffset, crossAxisOffset);
|
|
childParentData.offset = offset;
|
|
final nextOffset = mainAxisOffset + mainAxisExtent + mainAxisSpacing;
|
|
for (int i = 0; i < crossAxisCellCount; i++) {
|
|
offsets[origin.crossAxisIndex + i] = nextOffset;
|
|
}
|
|
child = childAfter(child);
|
|
}
|
|
final mainAxisExtent = offsets.reduce(math.max) - mainAxisSpacing;
|
|
|
|
if (axisDirectionIsReversed(axisDirection)) {
|
|
child = firstChild;
|
|
while (child != null) {
|
|
final childParentData = _getParentData(child);
|
|
final offset = childParentData.offset;
|
|
final crossAxisOffset = offset.getCrossAxisOffset(mainAxis);
|
|
final mainAxisOffset =
|
|
mainAxisExtent -
|
|
offset.getMainAxisOffset(mainAxis) -
|
|
childParentData.realMainAxisExtent!;
|
|
final newOffset = mainAxis == Axis.vertical
|
|
? Offset(crossAxisOffset, mainAxisOffset)
|
|
: Offset(mainAxisOffset, crossAxisOffset);
|
|
childParentData.offset = newOffset;
|
|
child = childAfter(child);
|
|
}
|
|
}
|
|
|
|
if (mainAxis == Axis.vertical && textDirection == TextDirection.rtl) {
|
|
child = firstChild;
|
|
while (child != null) {
|
|
final childParentData = _getParentData(child);
|
|
final crossAxisCellCount = crossAxisCount;
|
|
final crossAxisCellExtent =
|
|
stride * crossAxisCellCount - crossAxisSpacing;
|
|
final offset = childParentData.offset;
|
|
final crossAxisOffset =
|
|
crossAxisExtent - offset.dx - crossAxisCellExtent;
|
|
final mainAxisOffset = offset.dy;
|
|
final newOffset = Offset(crossAxisOffset, mainAxisOffset);
|
|
childParentData.offset = newOffset;
|
|
child = childAfter(child);
|
|
}
|
|
}
|
|
|
|
return mainAxis == Axis.vertical
|
|
? Size(crossAxisExtent, mainAxisExtent)
|
|
: Size(mainAxisExtent, crossAxisExtent);
|
|
}
|
|
|
|
bool _hasOverflow = false;
|
|
}
|
|
|
|
class GridParentData extends ContainerBoxParentData<RenderBox> {
|
|
int? crossAxisCellCount;
|
|
num? mainAxisCellCount;
|
|
double? realMainAxisExtent;
|
|
|
|
@override
|
|
String toString() =>
|
|
'crossAxisCellCount=$crossAxisCellCount; mainAxisCellCount=$mainAxisCellCount;';
|
|
}
|
|
|
|
class GridItem extends ParentDataWidget<GridParentData> {
|
|
final int crossAxisCellCount;
|
|
final num? mainAxisCellCount;
|
|
|
|
const GridItem({
|
|
super.key,
|
|
required super.child,
|
|
this.mainAxisCellCount,
|
|
this.crossAxisCellCount = 1,
|
|
});
|
|
|
|
@override
|
|
void applyParentData(RenderObject renderObject) {
|
|
final parentData = renderObject.parentData;
|
|
if (parentData is GridParentData) {
|
|
bool needsLayout = false;
|
|
if (parentData.crossAxisCellCount != crossAxisCellCount) {
|
|
parentData.crossAxisCellCount = crossAxisCellCount;
|
|
needsLayout = true;
|
|
}
|
|
|
|
if (parentData.mainAxisCellCount != mainAxisCellCount) {
|
|
parentData.mainAxisCellCount = mainAxisCellCount;
|
|
needsLayout = true;
|
|
}
|
|
|
|
if (needsLayout) {
|
|
final targetParent = renderObject.parent;
|
|
if (targetParent is RenderGrid) {
|
|
targetParent.markNeedsLayout();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Type get debugTypicalAncestorWidgetClass => GridItem;
|
|
|
|
GridItem wrap({required WrapBuilder builder}) {
|
|
return GridItem(
|
|
mainAxisCellCount: mainAxisCellCount,
|
|
crossAxisCellCount: crossAxisCellCount,
|
|
child: builder(child),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Origin {
|
|
final int crossAxisIndex;
|
|
final double mainAxisOffset;
|
|
|
|
const _Origin(this.crossAxisIndex, this.mainAxisOffset);
|
|
}
|
|
|
|
_Origin _getOrigin(List<double> offsets, int crossAxisCount) {
|
|
final length = offsets.length;
|
|
_Origin origin = const _Origin(0, double.infinity);
|
|
for (int i = 0; i < length; i++) {
|
|
final offset = offsets[i];
|
|
if (offset.moreOrEqual(origin.mainAxisOffset)) {
|
|
continue;
|
|
}
|
|
int start = 0;
|
|
int span = 0;
|
|
for (
|
|
int j = 0;
|
|
span < crossAxisCount &&
|
|
j < length &&
|
|
length - j >= crossAxisCount - span;
|
|
j++
|
|
) {
|
|
if (offset.moreOrEqual(offsets[j])) {
|
|
span++;
|
|
if (span == crossAxisCount) {
|
|
origin = _Origin(start, offset);
|
|
}
|
|
} else {
|
|
start = j + 1;
|
|
span = 0;
|
|
}
|
|
}
|
|
}
|
|
return origin;
|
|
}
|