Created
March 5, 2019 18:55
-
-
Save brianegan/b990d75a9f1696002e7c7185987a2cca to your computer and use it in GitHub Desktop.
An AnimatedList that does all the hard work for ya.
This file contains 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
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, | |
); | |
} | |
} |
This file contains 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 '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