I want to achieve something like below (animation style doesn't matter, I'm looking for the way to do this)
However, all resources and question only explain how to create item addition or removal animations.
My current code (I use BLoC pattern)
class _MembersPageState extends State<MembersPage> {
@override
Widget build(BuildContext context) {
return BlocProvider<MembersPageBloc>(
create: (context) =>
MembersPageBloc(userRepository: UserRepository.instance)..add(MembersPageShowed()),
child: BlocBuilder<MembersPageBloc, MembersPageState>(
builder: (context, state) {
if (state is MembersPageSuccess) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
User user = state.users[index];
return ListTile(
isThreeLine: true,
leading: Icon(Icons.person, size: 36),
title: Text(user.name),
subtitle: Text(user.username),
onTap: () => null,
);
},
);
} else
return Text("I don't care");
},
),
);
}
}
Animated widgets like AnimatedOpacity
and AnimatedPositioned
can do it. You can use different animation widgets, curves, begin and end values to have different animations with this way.
However, lifecycle of children widgets in a Listview is a bit complex. They get destroyed and recreated according to the scroll position. If the child widget has an animation that starts on initialization, it will reanimate whenever the child gets visible to the UI.
Here is my hacky solution. I used a static boolean to indicate whether it's the first time or recreation state and simply ignore the recration. You can qucikly try this in Dartpad.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: Colors.black54),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: ListView(
children: List.generate(
25, (i) => AnimatedListItem(i, key: ValueKey<int>(i))),
)),
);
}
}
class AnimatedListItem extends StatefulWidget {
final int index;
AnimatedListItem(this.index, {Key key}) : super(key: key);
@override
_AnimatedListItemState createState() => _AnimatedListItemState();
}
class _AnimatedListItemState extends State<AnimatedListItem> {
bool _animate = false;
static bool _isStart = true;
@override
void initState() {
super.initState();
_isStart
? Future.delayed(Duration(milliseconds: widget.index * 100), () {
setState(() {
_animate = true;
_isStart = false;
});
})
: _animate = true;
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: Duration(milliseconds: 1000),
opacity: _animate ? 1 : 0,
curve: Curves.easeInOutQuart,
child: AnimatedPadding(
duration: Duration(milliseconds: 1000),
padding: _animate
? const EdgeInsets.all(4.0)
: const EdgeInsets.only(top: 10),
child: Container(
constraints: BoxConstraints.expand(height: 100),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.index.toString(),
style: TextStyle(fontSize: 24),
),
),
),
),
),
);
}
}