Skip to content

Instantly share code, notes, and snippets.

@aloisdeniel
Last active August 10, 2023 11:27
Show Gist options
  • Save aloisdeniel/81d6a5241ca9e2f82f17cf3c001444c5 to your computer and use it in GitHub Desktop.
Save aloisdeniel/81d6a5241ca9e2f82f17cf3c001444c5 to your computer and use it in GitHub Desktop.
Custom GridView with various cell sizes in Flutter
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/src/rendering/sliver.dart';
import 'package:flutter/src/rendering/sliver_grid.dart';
class _CoordinateOffset {
final double main, cross;
_CoordinateOffset(this.main, this.cross);
}
typedef int GetCrossAxisSpan(int index);
typedef double GetMainAxisExtent(int index);
class SpanableSliverGridLayout extends SliverGridLayout {
/// Creates a layout that uses equally sized and spaced tiles.
///
/// All of the arguments must not be null and must not be negative. The
/// `crossAxisCount` argument must be greater than zero.
const SpanableSliverGridLayout(
this.crossAxisCount,
this.childCrossAxisExtent,
this.crossAxisStride,
this.mainAxisSpacing,
this.getCrossAxisSpan,
this.getMainAxisExtend) :
assert(crossAxisCount != null && crossAxisCount > 0),
assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
assert(crossAxisStride != null && crossAxisStride >= 0),
assert(getCrossAxisSpan != null),
assert(getMainAxisExtend != null);
/// The number of children in the cross axis.
final int crossAxisCount;
/// The number of pixels from the leading edge of one tile to the trailing
/// edge of the same tile in the main axis.
final double mainAxisSpacing;
/// The number of pixels from the leading edge of one tile to the leading edge
/// of the next tile in the cross axis.
final double crossAxisStride;
/// The number of pixels from the leading edge of one tile to the trailing
/// edge of the same tile in the cross axis.
final double childCrossAxisExtent;
final GetCrossAxisSpan getCrossAxisSpan;
final GetMainAxisExtent getMainAxisExtend;
_CoordinateOffset _findOffset(int index) {
int cross= 0;
double mainOffset = 0.0;
double crossOffset = 0.0;
double extend = 0.0;
int span;
for (int i = 0; i <= index; i++) {
span = getCrossAxisSpan(i);
span = math.min(this.crossAxisCount, math.max(0, span));
if((cross + span) > this.crossAxisCount) {
cross = 0;
mainOffset += extend + this.mainAxisSpacing;
crossOffset = 0.0;
extend = 0.0;
}
crossOffset = cross * crossAxisStride;
extend = math.max(extend, getMainAxisExtend(i));
cross += span;
}
return new _CoordinateOffset(mainOffset, crossOffset);
}
int getMinOrMaxChildIndexForScrollOffset(double scrollOffset, bool min) {
int cross = 0;
double mainOffset = 0.0;
double extend = 0.0;
int i = 0;
int span = 0;
while (true) {
span = getCrossAxisSpan(i);
span = math.min(this.crossAxisCount, math.max(0, span));
if ((cross + span) > this.crossAxisCount) {
cross = 0;
mainOffset += extend + this.mainAxisSpacing;
extend = 0.0;
}
extend = math.max(extend, getMainAxisExtend(i));
cross += span;
if (min && scrollOffset <= mainOffset + extend) {
return (i ~/ this.crossAxisCount) * this.crossAxisCount;
}
else if(!min && scrollOffset < mainOffset) {
return i;
}
i++;
}
}
@override
int getMinChildIndexForScrollOffset(double scrollOffset) => getMinOrMaxChildIndexForScrollOffset(scrollOffset, true);
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) => getMinOrMaxChildIndexForScrollOffset(scrollOffset, false);
@override
SliverGridGeometry getGeometryForChildIndex(int index) {
var span = getCrossAxisSpan(index);
var mainAxisExtent = getMainAxisExtend(index);
var offset = _findOffset(index);
return new SliverGridGeometry(
scrollOffset: offset.main,
crossAxisOffset: offset.cross,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: this.childCrossAxisExtent + (span - 1) * this.crossAxisStride,
);
}
@override
double estimateMaxScrollOffset(int childCount)
{
if(childCount <= 0)
return 0.0;
var lastOffset = _findOffset(childCount-1);
var extent = getMainAxisExtend(childCount-1);
return lastOffset.main + extent;
}
}
abstract class SpanableSliverGridDelegate extends SliverGridDelegate {
/// Creates a delegate that makes grid layouts with a fixed number of tiles in
/// the cross axis.
///
/// All of the arguments must not be null. The `mainAxisSpacing` and
/// `crossAxisSpacing` arguments must not be negative. The `crossAxisCount`
/// and `childAspectRatio` arguments must be greater than zero.
const SpanableSliverGridDelegate(
this.crossAxisCount,
{this.mainAxisSpacing: 0.0,
this.crossAxisSpacing: 0.0,
}) : assert(crossAxisCount != null && crossAxisCount > 0),
assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
assert(crossAxisSpacing != null && crossAxisSpacing >= 0);
/// The number of children in the cross axis.
final int crossAxisCount;
/// The number of logical pixels between each child along the main axis.
final double mainAxisSpacing;
/// The number of logical pixels between each child along the cross axis.
final double crossAxisSpacing;
bool _debugAssertIsValid() {
assert(crossAxisCount > 0);
assert(mainAxisSpacing >= 0.0);
assert(crossAxisSpacing >= 0.0);
return true;
}
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
assert(_debugAssertIsValid());
final double usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
return new SpanableSliverGridLayout(
crossAxisCount,
childCrossAxisExtent,
childCrossAxisExtent + crossAxisSpacing,
mainAxisSpacing,
getCrossAxisSpan,
getMainAxisExtent,
);
}
int getCrossAxisSpan(int index);
double getMainAxisExtent(int index);
@override
bool shouldRelayout(SpanableSliverGridDelegate oldDelegate) {
return oldDelegate.crossAxisCount != crossAxisCount
|| oldDelegate.mainAxisSpacing != mainAxisSpacing
|| oldDelegate.crossAxisSpacing != crossAxisSpacing;
}
}
import 'package:flutter/material.dart';
import 'spanablelayout.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class HomeChildDelegate extends SliverChildDelegate {
@override
Widget build(BuildContext context, int index) {
if(index >= 20)
return null;
Color color = Colors.red;
if(index == 0)
color = Colors.blue;
else if(index == 1 || index == 10)
color = Colors.cyan;
else if(index < 10)
color = Colors.green;;
return new Container(decoration: new BoxDecoration(color: color , shape: BoxShape.rectangle));
}
@override
bool shouldRebuild(SliverChildDelegate oldDelegate) => true;
@override
int get estimatedChildCount => 20;
}
class HomeGridDelegate extends SpanableSliverGridDelegate {
HomeGridDelegate() : super(3, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0);
@override
int getCrossAxisSpan(int index) {
if(index > 1 && index < 10)
return 1;
return 3;
}
@override
double getMainAxisExtent(int index) {
if(index == 0)
return 220.0;
if(index == 1 || index == 10)
return 50.0;
return 100.0;
}
}
class _MyHomePageState extends State<MyHomePage> {
_MyHomePageState() ;
Widget _buildBody(BuildContext context) {
return new GridView.custom(
gridDelegate: new HomeGridDelegate(),
childrenDelegate: new HomeChildDelegate(),
padding: new EdgeInsets.all(12.0),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
body: _buildBody(context),
);
}
}
@UglyBob79
Copy link

This can only span on cross axis though, not on the main axis as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment