Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created June 18, 2025 19:02
Show Gist options
  • Save slightfoot/1ceb8afcfc22bb03ec9c5944384ad1b9 to your computer and use it in GitHub Desktop.
Save slightfoot/1ceb8afcfc22bb03ec9c5944384ad1b9 to your computer and use it in GitHub Desktop.
Animated Image Grid - by Simon Lightfoot :: #HumpdayQandA on 18th June 2025 :: https://www.youtube.com/watch?v=ZL3ZK55xhBA
// MIT License
//
// Copyright (c) 2025 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/material.dart';
import 'photos.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Material(
color: Colors.black,
child: StaggeredAnimatedImageGrid(
images: imageThumbUrls,
),
),
);
}
}
class StaggeredAnimatedImageGrid extends StatefulWidget {
const StaggeredAnimatedImageGrid({
super.key,
required this.images,
});
final List<String> images;
@override
State<StaggeredAnimatedImageGrid> createState() => _StaggeredAnimatedImageGridState();
}
class _StaggeredAnimatedImageGridState extends State<StaggeredAnimatedImageGrid> {
final _existingData = <String>[];
final _newData = <String>[];
@override
void initState() {
super.initState();
_existingData.addAll(widget.images.sublist(0, widget.images.length ~/ 2));
_newData.addAll(widget.images.sublist(widget.images.length ~/ 2));
}
void _onAdd() {
if (_newData.isNotEmpty) {
setState(() {
_existingData.insert(0, _newData.removeLast());
});
}
}
void _onDelete(String item) {
setState(() {
_existingData.remove(item);
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final width = constraints.maxWidth / 3;
final height = constraints.maxHeight / 3;
final children = _existingData.indexed.map((el) {
final index = el.$1;
final item = el.$2;
final x = index % 3;
final y = index ~/ 3;
return GridItem(
key: Key(item),
index: index,
item: item,
onDelete: _onDelete,
position: Rect.fromLTWH(x * width, y * height, width, height),
);
}).toList();
return Scaffold(
backgroundColor: Colors.black,
body: SingleChildScrollView(
child: SizedBox(
width: constraints.maxWidth,
height: _existingData.length / 3 * height,
child: Stack(
children: children,
),
),
),
floatingActionButton: _newData.isEmpty
? null
: FloatingActionButton(
onPressed: _onAdd,
child: Icon(Icons.add),
),
);
},
);
}
}
class GridItem extends StatelessWidget {
const GridItem({
super.key,
required this.index,
required this.item,
required this.onDelete,
required this.position,
});
final int index;
final String item;
final void Function(String item) onDelete;
final Rect position;
@override
Widget build(BuildContext context) {
return AnimatedPositioned.fromRect(
key: Key(item),
duration: Duration(milliseconds: 200 * index),
curve: Curves.fastLinearToSlowEaseIn,
rect: position,
child: Material(
color: Colors.black,
child: Ink.image(
image: NetworkImage(item),
fit: BoxFit.cover,
child: InkWell(
onTap: () => onDelete(item),
),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment