beautiful_staggered_grid_view 1.0.0+3
beautiful_staggered_grid_view: ^1.0.0+3 copied to clipboard
A beautiful and customizable staggered grid view package for Flutter.
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
/// Entry point of the application.
void main() => runApp(const MyApp());
/// The root widget of the example application.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Staggered Grid View Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ExampleHomePage(),
);
}
}
/// The home page that demonstrates the staggered grid layout.
class ExampleHomePage extends StatelessWidget {
const ExampleHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Staggered Grid Example')),
body: const StaggeredPage(),
);
}
}
/// A scaffold widget that provides a common layout with an app bar.
class AppScaffold extends StatelessWidget {
/// The title to display in the app bar.
final String title;
/// The content of the page.
final Widget child;
/// Creates an [AppScaffold] with the given [title] and [child].
const AppScaffold({
Key? key,
required this.title,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: child,
),
);
}
}
/// A model that represents the configuration for a grid tile.
class GridTile {
/// Number of cells the tile spans in the cross axis.
final int crossAxisCount;
/// Number of cells the tile spans in the main axis.
final int mainAxisCount;
/// Creates a [GridTile] with the given cell counts.
const GridTile(this.crossAxisCount, this.mainAxisCount);
}
/// A widget that displays a colored tile with its index.
class ImageTile extends StatelessWidget {
/// The index used to determine the color.
final int index;
/// The width of the tile.
final double width;
/// The height of the tile.
final double height;
/// Creates an [ImageTile] with the specified dimensions.
const ImageTile({
Key? key,
required this.index,
required this.width,
required this.height,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Use Color.fromRGBO to generate a color with 50% opacity.
final baseColor = Colors.primaries[index % Colors.primaries.length];
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: Color.fromRGBO(baseColor.red, baseColor.green, baseColor.blue, 0.5),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'Tile $index',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white),
),
),
);
}
}
/// A custom staggered grid view widget that arranges its children.
class StaggeredGrid extends StatelessWidget {
/// Number of columns in the grid.
final int crossAxisCount;
/// Vertical spacing between grid items.
final double mainAxisSpacing;
/// Horizontal spacing between grid items.
final double crossAxisSpacing;
/// The list of grid tiles to display.
final List<StaggeredGridTile> children;
/// Creates a [StaggeredGrid] with a fixed column count.
const StaggeredGrid.count({
Key? key,
required this.crossAxisCount,
this.mainAxisSpacing = 4.0,
this.crossAxisSpacing = 4.0,
required this.children,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final totalWidth = constraints.maxWidth;
final cellWidth = (totalWidth - (crossAxisCount - 1) * crossAxisSpacing) / crossAxisCount;
final cellHeight = cellWidth; // Using square cells
final occupancy = <List<bool>>[];
final positions = <_TilePosition>[];
for (var tile in children) {
final pos = _findPositionForTile(tile, occupancy, crossAxisCount);
positions.add(pos);
_markOccupancy(occupancy, pos, tile.crossAxisCellCount, tile.mainAxisCellCount);
}
final totalRows = occupancy.length;
final gridHeight = totalRows * cellHeight + (totalRows - 1) * mainAxisSpacing;
final positionedChildren = <Widget>[];
for (var i = 0; i < children.length; i++) {
final tile = children[i];
final pos = positions[i];
final left = pos.col * (cellWidth + crossAxisSpacing);
final top = pos.row * (cellHeight + mainAxisSpacing);
final tileWidth = tile.crossAxisCellCount * cellWidth + (tile.crossAxisCellCount - 1) * crossAxisSpacing;
final tileHeight = tile.mainAxisCellCount * cellHeight + (tile.mainAxisCellCount - 1) * mainAxisSpacing;
positionedChildren.add(Positioned(
left: left,
top: top,
width: tileWidth,
height: tileHeight,
child: tile.child,
));
}
return SizedBox(
width: totalWidth,
height: gridHeight,
child: Stack(children: positionedChildren),
);
},
);
}
}
/// A widget that defines a tile for the [StaggeredGrid].
class StaggeredGridTile extends StatelessWidget {
/// Number of cells to span in the cross axis.
final int crossAxisCellCount;
/// Number of cells to span in the main axis.
final int mainAxisCellCount;
/// The widget to display within this tile.
final Widget child;
/// Creates a [StaggeredGridTile] with the given spans and child.
const StaggeredGridTile.count({
Key? key,
required this.crossAxisCellCount,
required this.mainAxisCellCount,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) => child;
}
/// Internal helper class to track a tile's position.
class _TilePosition {
int row;
int col;
_TilePosition({required this.row, required this.col});
}
_TilePosition _findPositionForTile(StaggeredGridTile tile, List<List<bool>> occupancy, int crossAxisCount) {
final requiredRows = tile.mainAxisCellCount;
final requiredCols = tile.crossAxisCellCount;
var row = 0;
while (true) {
while (occupancy.length < row + requiredRows) {
occupancy.add(List.filled(crossAxisCount, false));
}
for (var col = 0; col <= crossAxisCount - requiredCols; col++) {
if (_canPlaceAt(occupancy, row, col, requiredRows, requiredCols)) {
return _TilePosition(row: row, col: col);
}
}
row++;
}
}
bool _canPlaceAt(List<List<bool>> occupancy, int startRow, int startCol, int requiredRows, int requiredCols) {
for (var r = startRow; r < startRow + requiredRows; r++) {
for (var c = startCol; c < startCol + requiredCols; c++) {
if (occupancy[r][c]) return false;
}
}
return true;
}
void _markOccupancy(List<List<bool>> occupancy, _TilePosition pos, int tileCols, int tileRows) {
for (var r = pos.row; r < pos.row + tileRows; r++) {
for (var c = pos.col; c < pos.col + tileCols; c++) {
occupancy[r][c] = true;
}
}
}
/// A page that demonstrates the custom staggered grid layout.
class StaggeredPage extends StatelessWidget {
const StaggeredPage({Key? key}) : super(key: key);
/// A list of grid tile configurations.
static const tiles = [
GridTile(2, 2),
GridTile(2, 1),
GridTile(1, 2),
GridTile(1, 1),
GridTile(2, 2),
GridTile(1, 2),
GridTile(1, 1),
GridTile(3, 1),
GridTile(1, 1),
GridTile(4, 1),
];
@override
Widget build(BuildContext context) {
return AppScaffold(
title: 'Staggered Grid',
child: SingleChildScrollView(
child: StaggeredGrid.count(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: tiles.mapIndexed((index, tile) {
return StaggeredGridTile.count(
crossAxisCellCount: tile.crossAxisCount,
mainAxisCellCount: tile.mainAxisCount,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(2, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ImageTile(
index: index,
width: tile.crossAxisCount * 100,
height: tile.mainAxisCount * 100,
),
),
),
);
}).toList(),
),
),
);
}
}