Files
stream-chat-flutter/packages/stream_chat_flutter/lib/src/misc/flex_grid.dart
T
2023-07-28 21:07:02 +05:30

260 lines
8.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
/// {@template matrix}
/// A matrix represents a 2D array of integers.
/// {@endtemplate}
typedef Matrix = List<List<int>>;
extension on Matrix {
/// Returns the total number of items in the matrix.
int get count => fold(0, (sum, row) => sum + row.length);
/// Creates a lazy iterable of the [count] first elements of this iterable.
///
/// The returned `Iterable` may contain fewer than `count` elements, if `this`
/// contains fewer than `count` elements.
Matrix takeItems(int count) {
final matrix = [...this];
// Remove items from the end of the matrix until the count is equal to n.
while (matrix.count > count) {
matrix.last.removeLast();
if (matrix.last.isEmpty) {
matrix.removeLast();
}
}
return matrix;
}
}
/// Signature for a function that builds a widget for the overlay of the last
/// child in the grid in case the number of children exceeds the maximum.
///
/// The [remaining] parameter represents the number of children that are not
/// displayed in the grid.
typedef OverlayBuilder = Widget Function(BuildContext context, int remaining);
/// {@template flex_grid}
/// A flexible grid widget that arranges its children based on a provided
/// [pattern].
///
/// The [FlexGrid] widget displays a grid of [children] widgets based on a
/// provided [pattern]. Each numeric value in the matrix represents the
/// flex value of the corresponding widget in the grid. The number of widgets
/// must match the number of cells in the matrix.
///
/// The grid can be configured to have a maximum number of children to display.
/// If the number of children exceeds the maximum, the last child will show the
/// remaining number of children as a count in an overlay. An overlay builder
/// can be provided to customize the overlay for the last child.
///
/// The direction of the grid can be reversed, with either the column or row as
/// the primary direction. Spacing can be applied between children in the main
/// axis and between the runs (rows or columns) themselves in the cross axis.
///
/// Example usage:
/// ```dart
/// FlexGrid(
/// pattern: const [
/// [1, 1],
/// [1, 1],
/// ],
/// children: [
/// Container(color: Colors.red),
/// Container(color: Colors.blue),
/// Container(color: Colors.green),
/// Container(color: Colors.yellow),
/// ],
/// )
/// ```
/// {@endtemplate}
class FlexGrid extends StatelessWidget {
/// {@macro flex_grid}
FlexGrid({
super.key,
required this.pattern,
required this.children,
this.maxChildren,
this.overlayBuilder,
this.reverse = false,
this.spacing = 2.0,
this.runSpacing = 2.0,
}) : assert(
pattern.count == children.length,
'The number of children must match the number of cells in the matrix',
),
assert(
maxChildren == null || maxChildren <= pattern.count,
'The number of maxChildren must be less than or equal to the number '
'of cells in the matrix',
),
assert(
maxChildren == null || overlayBuilder != null,
'overlayBuilder must be provided when maxChildren is not null',
);
/// The pattern of the grid.
///
/// Each numeric value in the array represents the flex value of
/// corresponding widget in grid.
///
/// For example, a grid with 2 rows and 2 columns can be represented as:
///
/// ```dart
/// [
/// [1, 1],
/// [1, 1],
/// ]
/// ```
///
/// This will create a grid with 4 cells with each cell having a flex value
/// of 1.
final Matrix pattern;
/// The widgets to display in the grid.
///
/// The number of widgets must match the number of cells in the matrix.
final List<Widget> children;
/// Whether to reverse the direction of the grid.
///
/// By default, the grid is rendered with column as primary direction and row
/// as secondary direction.
///
/// If this is set to `true`, the grid will be rendered with row as primary
/// direction and column as secondary direction.
final bool reverse;
/// The maximum number of children to display in the grid.
///
/// If this is set, the grid will be rendered with a maximum of [maxChildren]
/// children. If the number of children is greater than [maxChildren], The
/// last child will show the remaining number of children as a count in a
/// overlay.
final int? maxChildren;
/// The builder to use to build the overlay for the last child in case the
/// number of children is greater than [maxChildren].
///
/// The builder will be passed the number of remaining children to display.
final OverlayBuilder? overlayBuilder;
/// How much space to place between children in a run in the main axis.
///
/// For example, if [spacing] is 10.0, the children will be spaced at least
/// 10.0 logical pixels apart in the main axis.
///
/// Defaults to 2.0.
final double spacing;
/// How much space to place between the runs themselves in the cross axis.
///
/// For example, if [runSpacing] is 10.0, the runs will be spaced at least
/// 10.0 logical pixels apart in the cross axis.
///
/// Defaults to 2.0.
final double runSpacing;
@override
Widget build(BuildContext context) {
// Determine the primary and secondary directions.
final primaryDirection = reverse ? Axis.horizontal : Axis.vertical;
final secondaryDirection = reverse ? Axis.vertical : Axis.horizontal;
var pattern = [...this.pattern];
var children = [...this.children];
// If the number of children is greater than the maximum number of children,
// remove the extra children.
final maxChildren = this.maxChildren;
if (maxChildren != null && maxChildren < pattern.count) {
children = [...children.take(maxChildren)];
pattern = [...pattern.takeItems(maxChildren)];
}
// Track the current child index.
//
// This is used to determine which child to render next.
var childIndex = 0;
return Flex(
direction: primaryDirection,
children: <Widget>[
for (final row in pattern)
Expanded(
child: Flex(
direction: secondaryDirection,
children: <Widget>[
...row.map((flex) {
final isLastChild = childIndex == children.length - 1;
// Determine the number of remaining children.
final remaining = (this.children.length - 1) - childIndex;
// If we are at the last child and there are remaining
// children, show the remaining number of children as a
// count in a overlay.
if (isLastChild && remaining > 0) {
return Expanded(
flex: flex,
child: Stack(
fit: StackFit.passthrough,
children: [
children[childIndex++],
overlayBuilder!.call(context, remaining),
],
),
);
}
// Otherwise, return the child.
return Expanded(
flex: flex,
child: children[childIndex++],
);
}),
].insertBetween(
Gap(direction: secondaryDirection, spacing: runSpacing),
),
),
),
].insertBetween(
Gap(direction: primaryDirection, spacing: spacing),
),
);
}
}
/// {@template gap}
/// A gap widget used to add spacing between children in either the horizontal
/// or vertical direction.
/// {@endtemplate}
class Gap extends StatelessWidget {
/// {@macro gap}
const Gap({
super.key,
required this.direction,
this.spacing = 0.0,
});
/// The direction of the gap.
final Axis direction;
/// The spacing between children in the gap.
///
/// Defaults to 0.0.
final double spacing;
@override
Widget build(BuildContext context) {
switch (direction) {
case Axis.horizontal:
return SizedBox(width: spacing);
case Axis.vertical:
return SizedBox(height: spacing);
}
}
}