Animate elements in ListView on initialization

14.1k Views Asked by At

I want to achieve something like below (animation style doesn't matter, I'm looking for the way to do this)

example

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");
        },
      ),
    );
  }
}

3

There are 3 best solutions below

5
On

Widgets like AnimatedOpacity and AnimatedPositioned can be used to achieve this. However, lifecycle of the 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 "recreation". You can copy and try it in Dartpad.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.brown,
        body: ListView(
          children: List.generate(
            25,
            (i) => AnimatedListItem(i, key: ValueKey<int>(i)),
          ),
        ),
      ),
    );
  }
}

class AnimatedListItem extends StatefulWidget {
  final int index;

  const AnimatedListItem(this.index, {Key? key}) : super(key: key);

  @override
  State<AnimatedListItem> createState() => _AnimatedListItemState();
}

class _AnimatedListItemState extends State<AnimatedListItem> {
  bool _animate = false;

  static bool _isStart = true;

  @override
  void initState() {
    super.initState();
    if (_isStart) {
      Future.delayed(Duration(milliseconds: widget.index * 100), () {
        setState(() {
          _animate = true;
          _isStart = false;
        });
      });
    } else {
      _animate = true;
    }
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      duration: const Duration(milliseconds: 1000),
      opacity: _animate ? 1 : 0,
      curve: Curves.easeInOutQuart,
      child: AnimatedPadding(
        duration: const Duration(milliseconds: 1000),
        padding: _animate
            ? const EdgeInsets.all(4.0)
            : const EdgeInsets.only(top: 10),
        child: Container(
          constraints: const BoxConstraints.expand(height: 100),
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(
                widget.index.toString(),
                style: const TextStyle(fontSize: 24),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
0
On
import 'package:flutter/material.dart';

class DifferentPage extends StatefulWidget {
  const DifferentPage({super.key});

  @override
  State<DifferentPage> createState() => DifferentStatePage();
}

class DifferentStatePage extends State<DifferentPage> {
  final _item = [];
  final GlobalKey<AnimatedListState> _key = GlobalKey();

  void _addItem() {
    _item.insert(0, 'Item ${_item.length + 1}');
    _key.currentState!.insertItem( 0,
      duration: const Duration(seconds: 1),
    );
  }

 void _removeItem(int index) {
   _key.currentState!.removeItem( index, (_, animation) {
     return SizeTransition(
       sizeFactor: animation,
       child: const Card(
         margin: EdgeInsets.all(10),
         color: Colors.red,
         child: ListTile(
           title: Text(
             'Deleted',
             style: TextStyle(fontSize: 22),
           ),
         ),
       ),
     );
   },
   duration: const Duration(milliseconds: 300),
 );
   _item.removeAt(index);
 }

 @override
 Widget build(BuildContext context) {
   return Column(
     children: [
       const SizedBox( height: 10 ),
     IconButton(
       onPressed: _addItem,
       icon: const Icon(Icons.add_box_rounded),
     ),
     Expanded(
       child: AnimatedList(
         key: _key,
         initialItemCount: 0,
         padding: const EdgeInsets.all(10),
         itemBuilder: (context, index, animation) {
           return SizeTransition(
             key: UniqueKey(),
             sizeFactor: animation,
             child: Card(
               margin: const EdgeInsets.all(10),
               color: Colors.amberAccent,
               child: ListTile(
                 title: Text(
                   _item[index],
                   style: const TextStyle(fontSize: 22),
                 ),
                 trailing: IconButton(
                   icon: const Icon(Icons.delete),
                   onPressed: () {
                     _removeItem(index);
                   },
                 ),
               ),
             ),
           );
         },
       ),
     )
   ],
 );
 }}
0
On

I created a gist on dartpad shows how to add initial animation for ListView

The key point is to create an AnimationController for each list item, cache it, and clean up when animation is complete.