Last active
April 13, 2025 09:48
-
-
Save lukepighetti/df460db180b9f6cb3410e3cc91ed74e6 to your computer and use it in GitHub Desktop.
A simple animated grid in Flutter. See: https://twitter.com/luke_pighetti/status/1366151664567255041
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import '../extensions/extensions.dart'; | |
typedef AnimatedGridBuilder<T> = Widget Function( | |
BuildContext, T item, AnimatedGridDetails details); | |
class AnimatedGrid<T> extends StatelessWidget { | |
/// An animated grid the animates when the items change sort. | |
const AnimatedGrid({ | |
Key key, | |
@required this.itemHeight, | |
@required this.items, | |
@required this.keyBuilder, | |
@required this.builder, | |
this.columns = 2, | |
this.duration = const Duration(milliseconds: 750), | |
this.curve = Curves.elasticOut, | |
}) : super(key: key); | |
/// The grid items. Should all be the same height. | |
final List<T> items; | |
/// Construct keys given the item provided. Each key must be unique. | |
final Key Function(T item) keyBuilder; | |
/// Build a widget given a context, the current item, and the column and row index. | |
final AnimatedGridBuilder<T> builder; | |
/// The number of columns wide to display. | |
final int columns; | |
/// The height of each child. | |
final double itemHeight; | |
/// The duration of the sort animation. | |
final Duration duration; | |
/// The curve of the sort animation. | |
final Curve curve; | |
static int _rows(int columns, int count) => (count / columns).ceil(); | |
@visibleForTesting | |
static List<int> gridIndicies(int index, int columns, int count) { | |
final rows = _rows(columns, count); | |
final maxItemsForGridSize = columns * rows; | |
final xIndex = (index / maxItemsForGridSize * columns).floor(); | |
final yIndex = index % rows; | |
return [xIndex, yIndex]; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return LayoutBuilder( | |
builder: (context, constraints) { | |
assert(constraints.hasBoundedWidth); | |
assert(constraints.hasBoundedHeight == false); | |
final width = constraints.maxWidth; | |
final count = items.length; | |
final itemWidth = width / columns; | |
final rows = _rows(columns, count); | |
final gridHeight = rows * itemHeight; | |
return SizedBox( | |
height: gridHeight, | |
child: Stack( | |
alignment: Alignment.topLeft, | |
children: [ | |
for (var i = 0; i <= items.lastIndex; i++) | |
Builder( | |
key: keyBuilder(items[i]), | |
builder: (context) { | |
final item = items[i]; | |
final indicies = gridIndicies(i, columns, count); | |
assert(indicies.length == 2); | |
final xIndex = indicies.first; | |
final yIndex = indicies.last; | |
final offset = | |
Offset(xIndex * itemWidth, yIndex * itemHeight); | |
return TweenAnimationBuilder( | |
tween: Tween<Offset>(end: offset), | |
duration: duration, | |
curve: curve, | |
builder: (context, offset, child) { | |
return Transform.translate( | |
offset: offset, | |
child: child, | |
); | |
}, | |
child: SizedBox( | |
height: itemHeight, | |
width: itemWidth, | |
child: builder( | |
context, | |
item, | |
AnimatedGridDetails( | |
index: i, | |
columnIndex: xIndex, | |
rowIndex: yIndex, | |
columns: columns, | |
rows: rows, | |
), | |
), | |
), | |
); | |
}, | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
} | |
class AnimatedGridDetails { | |
/// A collection of details currently being used by [AnimatedGrid] | |
AnimatedGridDetails({ | |
@required this.index, | |
@required this.columnIndex, | |
@required this.rowIndex, | |
@required this.columns, | |
@required this.rows, | |
}); | |
/// The current index | |
final int index; | |
/// The current column index | |
final int columnIndex; | |
/// The current row index | |
final int rowIndex; | |
/// The number of columns | |
final int columns; | |
/// The number of rows | |
final int rows; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter_test/flutter_test.dart'; | |
import 'package:vgl/widgets/animated_grid.dart'; | |
main() { | |
group('AnimatedGrid', () { | |
test('gridIndicies', () { | |
/// index 0 | |
/// | |
/// ``` | |
/// 0 2 | |
/// 1 | |
/// ``` | |
expect( | |
AnimatedGrid.gridIndicies(0, 2, 3), | |
equals([0, 0]), | |
); | |
/// index 2 | |
/// | |
/// ``` | |
/// 0 2 | |
/// 1 | |
/// ``` | |
expect( | |
AnimatedGrid.gridIndicies(2, 2, 3), | |
equals([1, 0]), | |
); | |
/// index 9 | |
/// | |
/// ``` | |
/// 0 4 8 | |
/// 1 5 9 | |
/// 2 6 | |
/// 3 7 | |
/// ``` | |
expect( | |
AnimatedGrid.gridIndicies(9, 3, 10), | |
equals([2, 1]), | |
); | |
/// index 7 | |
/// | |
/// ``` | |
/// 0 4 8 | |
/// 1 5 9 | |
/// 2 6 | |
/// 3 7 | |
/// ``` | |
expect( | |
AnimatedGrid.gridIndicies(7, 3, 10), | |
equals([1, 3]), | |
); | |
/// index 6 | |
/// | |
/// ``` | |
/// 0 4 8 | |
/// 1 5 9 | |
/// 2 6 | |
/// 3 7 | |
/// ``` | |
expect( | |
AnimatedGrid.gridIndicies(6, 3, 10), | |
equals([1, 2]), | |
); | |
/// index 3 | |
/// | |
/// ``` | |
/// 0 4 8 | |
/// 1 5 9 | |
/// 2 6 | |
/// 3 7 | |
/// ``` | |
expect( | |
AnimatedGrid.gridIndicies(3, 3, 10), | |
equals([0, 3]), | |
); | |
}); | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extension IterableX<T> on Iterable<T> { | |
/// The last index on this iterable. | |
/// | |
/// Ie `[A,B,C].lastIndex == 2` | |
int get lastIndex => length == 0 | |
? throw RangeError('Cannot find the last index of an empty iterable') | |
: length - 1; | |
} |
No problem, it happened due to keys mismatch on removing items. I modified your code now in order to make item-key relation more clear.
Btw I think it'd be better if the code for generating grid indices will match the standard GridView sorting:
1 2
3 4
instead of currently provided:
1 3
2 4
I rewrote it like this for that purpose:
@visibleForTesting
static List<int> generateGridIndices(int index, int columns, int count) {
final rows = getRowsCount(columns, count);
final maxItemsForGridSize = columns * rows;
final yIndex = (index / maxItemsForGridSize * rows).floor();
final xIndex = index % columns;
return [xIndex, yIndex];
}
@lukepighetti Could u plz provide a small example of how to use it...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm not sure what you're expecting or seeing