Skip to content

Instantly share code, notes, and snippets.

@brianegan
Created March 5, 2019 18:55
Show Gist options
  • Save brianegan/b990d75a9f1696002e7c7185987a2cca to your computer and use it in GitHub Desktop.
Save brianegan/b990d75a9f1696002e7c7185987a2cca to your computer and use it in GitHub Desktop.
An AnimatedList that does all the hard work for ya.
library ez_animated_list;
import 'package:flutter/widgets.dart';
typedef EzAnimatedItemBuilder<T> = Widget Function(
BuildContext context,
Animation<double> animation,
T item,
);
class EzAnimatedList<T> extends StatefulWidget {
final List<T> items;
final Duration insertDuration;
final Duration removeDuration;
final EzAnimatedItemBuilder<T> insertItemBuilder;
final EzAnimatedItemBuilder<T> removeItemBuilder;
final EdgeInsets padding;
final bool shrinkWrap;
final ScrollPhysics physics;
final bool primary;
final ScrollController controller;
final bool reverse;
final Axis scrollDirection;
const EzAnimatedList({
Key key,
@required this.items,
@required this.insertItemBuilder,
@required this.removeItemBuilder,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.insertDuration = const Duration(milliseconds: 500),
this.removeDuration = const Duration(milliseconds: 500),
}) : super(key: key);
EzAnimatedList.bouncy({
Key key,
@required this.items,
EzAnimatedItemBuilder<T> insertItemBuilder,
EzAnimatedItemBuilder<T> removeItemBuilder,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.insertDuration = const Duration(milliseconds: 1200),
this.removeDuration = const Duration(milliseconds: 400),
}) : this.insertItemBuilder = _createBouncyInsert(insertItemBuilder),
this.removeItemBuilder = _createBouncyRemove(removeItemBuilder),
super(key: key);
EzAnimatedList.easeInOut({
Key key,
@required this.items,
EzAnimatedItemBuilder<T> insertItemBuilder,
EzAnimatedItemBuilder<T> removeItemBuilder,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.insertDuration = const Duration(milliseconds: 500),
this.removeDuration = const Duration(milliseconds: 500),
}) : this.insertItemBuilder = _createEasyInOutInsert(insertItemBuilder),
this.removeItemBuilder = _createEasyInOutRemove(removeItemBuilder),
super(key: key);
static EzAnimatedItemBuilder _createBouncyInsert<T>(
EzAnimatedItemBuilder<T> cb,
) {
return (context, animation, item) {
return SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.bounceOut,
),
child: cb(context, animation, item),
);
};
}
static EzAnimatedItemBuilder _createBouncyRemove(
EzAnimatedItemBuilder cb,
) {
return (context, animation, item) {
return SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
),
child: cb(context, animation, item),
);
};
}
static EzAnimatedItemBuilder _createEasyInOutInsert<T>(
EzAnimatedItemBuilder<T> cb,
) {
return (context, animation, item) {
return SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
),
child: cb(context, animation, item),
);
};
}
static EzAnimatedItemBuilder _createEasyInOutRemove(
EzAnimatedItemBuilder cb,
) {
return (context, animation, item) {
return SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
),
child: cb(context, animation, item),
);
};
}
@override
EzAnimatedListState createState() => EzAnimatedListState();
}
class EzAnimatedListState<T> extends State<EzAnimatedList<T>> {
final _listKey = GlobalKey<AnimatedListState>();
List<T> _items = [];
@override
void initState() {
super.initState();
_items.addAll(widget.items);
}
@override
void didUpdateWidget(EzAnimatedList oldWidget) {
super.didUpdateWidget(oldWidget);
update();
}
AnimatedListState get _listState => _listKey.currentState;
void insert(int index, T item) {
_items.insert(index, item);
_listState.insertItem(index, duration: widget.insertDuration);
}
T removeAt(int index) {
final T removedItem = _items.removeAt(index);
if (removedItem != null) {
_listState.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return widget.removeItemBuilder(context, animation, removedItem);
},
duration: widget.removeDuration,
);
}
return removedItem;
}
T removeAtNoAnimation(int index) {
final T removedItem = _items.removeAt(index);
if (removedItem != null) {
_listState.removeItem(
index,
(BuildContext context, Animation<double> animation) => Container(),
duration: widget.removeDuration,
);
}
return removedItem;
}
void update() {
for (int i = _items.length - 1; i >= 0; i--) {
if (!widget.items.contains(_items[i])) {
removeAt(i);
}
}
for (int i = 0; i < widget.items.length; i++) {
final item = widget.items[i];
if (_items.contains(item)) {
var oldPosition = _items.indexOf(item);
if (i == oldPosition) {
continue;
} else {
removeAt(oldPosition);
insert(i, item);
}
} else {
insert(i, item);
}
}
}
@override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
itemBuilder: (context, index, animation) =>
widget.insertItemBuilder(context, animation, _items[index]),
initialItemCount: _items.length,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
padding: widget.padding,
);
}
}
import 'dart:math';
import 'package:english_words/english_words.dart';
import 'package:ez_animated_list/ez_animated_list.dart';
import 'package:flutter/material.dart';
void main() => runApp(AnimatedListsApp());
class AnimatedListsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'EzAnimatedList Demo',
theme: ThemeData(
primaryColor: Colors.white,
),
home: EzAnimatedListDemo(title: 'EzAnimatedList Demo'),
);
}
}
class EzAnimatedListDemo extends StatefulWidget {
EzAnimatedListDemo({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<EzAnimatedListDemo> {
List<WordPair> _list;
@override
void initState() {
super.initState();
_list = _list = generateWordPairs().take(5).toList();
}
void _shuffleWords() {
setState(() => _list..shuffle());
}
void _addWord() {
setState(() {
_list.insert(
Random().nextInt(_list.length + 1),
generateWordPairs().take(1).toList().first,
);
});
}
void _removeWord(WordPair pair) =>
setState(() => _list.removeAt(_list.indexOf(pair)));
void _removeRandomWord() =>
setState(() => _list.removeAt(Random().nextInt(_list.length)));
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
bottom: TabBar(tabs: [
Tab(text: 'Bouncy'),
Tab(text: 'Ease In / Out'),
Tab(text: 'Custom'),
]),
),
body: Stack(
children: <Widget>[
TabBarView(children: [
Bouncy(list: _list, removePair: _removeWord),
EaseInOut(list: _list),
Custom(list: _list),
]),
Positioned(
bottom: 24.0,
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FloatingActionButton(
child: Icon(Icons.remove),
onPressed: _removeRandomWord,
),
FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: _shuffleWords,
),
FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addWord,
),
],
),
)
],
),
),
);
}
}
class Bouncy extends StatelessWidget {
const Bouncy(
{Key key, @required List<WordPair> list, @required this.removePair})
: _list = list,
super(key: key);
final List<WordPair> _list;
final void Function(WordPair pair) removePair;
@override
Widget build(BuildContext context) {
return EzAnimatedList.bouncy(
items: _list,
insertItemBuilder: (context, animation, pair) {
return Container(
color: Colors.green[200],
child: Dismissible(
key: Key(pair.toString()),
onDismissed: (_) => removePair(pair),
child: ListTile(
title: Text(pair.toString()),
),
),
);
},
removeItemBuilder: (context, animation, pair) {
return Container(
color: Colors.red[200],
child: ListTile(
title: Text(pair.toString()),
),
);
},
);
}
}
class EaseInOut extends StatelessWidget {
const EaseInOut({
Key key,
@required List<WordPair> list,
}) : _list = list,
super(key: key);
final List<WordPair> _list;
@override
Widget build(BuildContext context) {
return EzAnimatedList.easeInOut(
items: _list,
insertItemBuilder: (context, animation, pair) {
return Container(
color: Colors.green[200],
child: ListTile(
title: Text(pair.toString()),
),
);
},
removeItemBuilder: (context, animation, pair) {
return Container(
color: Colors.red[200],
child: ListTile(
title: Text(pair.toString()),
),
);
},
);
}
}
class Custom extends StatelessWidget {
const Custom({
Key key,
@required List<WordPair> list,
}) : _list = list,
super(key: key);
final List<WordPair> _list;
@override
Widget build(BuildContext context) {
return EzAnimatedList(
items: _list,
removeDuration: Duration(milliseconds: 1500),
insertItemBuilder: (context, animation, pair) {
return SizeTransition(
sizeFactor: CurveTween(curve: Curves.easeOut).animate(animation),
child: Container(
color: Colors.green[200],
child: ListTile(
title: Text(pair.toString()),
),
),
);
},
removeItemBuilder: (context, animation, pair) {
final slideTween = SlideTween();
final sizeTween = SizeTween();
return SizeTransition(
sizeFactor: sizeTween.animate(animation),
child: SlideTransition(
position: slideTween.animate(animation),
child: Container(
color: Colors.red[200],
child: ListTile(
title: Text(pair.toString()),
),
),
),
);
},
);
}
}
class SlideTween extends Tween<Offset> {
SlideTween({Offset begin = const Offset(-1.0, 0.0), Offset end = Offset.zero})
: super(begin: begin, end: end);
@override
Offset evaluate(Animation<double> animation) {
return Offset.lerp(
begin,
end,
animation.value < 0.5
? 0.0
: Curves.bounceIn.transform((animation.value - 0.5) * 2));
}
}
class SizeTween extends Tween<double> {
SizeTween({double begin = 0.0, double end = 1.0})
: super(begin: begin, end: end);
@override
double evaluate(Animation<double> animation) {
if (animation.value > 0.25) {
return 1.0;
} else {
return Curves.bounceIn.transform(animation.value * 4);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment