Flutter ListView Expand And Flip Animation
Tags: flutter, android, ios, webdev
Published: 2021-03-30 19:36:38

After doing a [fade-in and expand animation](https://dev.to/devjourney/flutter-how-to-expandable-list-tile-3p53) I wanted to have something that is really unique to Flutter's tool-set.
The animation was mainly inspired by this great talk [Marcin Szałek | Flutter Europe](https://youtu.be/FCyoHclCqc8)

###### Flip and Expand animation
![The flip animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rjfec8n5k3zq03h8e9ym.gif)

The code is available in the same GitHub project under [expand_with_flip](https://github.com/fourman-alex/animated_expansion_list/tree/expand_with_flip) branch

Here is the main class for convenience
{% collapsible ExpandListTile %} 
```dart
import 'package:flutter/material.dart';
import 'dart:math' as math;

class ExpandListTile extends StatefulWidget {
  const ExpandListTile(
      {Key key,
      this.expandedChild,
      this.collapsedChild,
      this.expandedHeight = 300.0,
      this.collapsedHeight = 70.0,
      this.duration = const Duration(milliseconds: 500),
      this.curve = Curves.ease})
      : super(key: key);

  final Widget expandedChild;
  final Widget collapsedChild;
  final double expandedHeight;
  final double collapsedHeight;
  final Duration duration;
  final Curve curve;

  @override
  _ExpandListTileState createState() => _ExpandListTileState();
}

class _ExpandListTileState extends State<ExpandListTile>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation _flipAnimation;
  Animation _expandAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
    _flipAnimation = CurvedAnimation(
        parent: _controller, curve: Interval(0.5, 1.0, curve: Curves.ease));
    _expandAnimation = CurvedAnimation(
        parent: _controller, curve: Interval(0.0, 0.5, curve: Curves.ease));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior
          .translucent, //IMPORTANT: allows "empty" spaces to respond to events
      onTap: (() => setState(() {
            _toggle();
            // _isExpanded = !_isExpanded;
          })),
      child: AnimatedBuilder(
        animation: _flipAnimation,
        builder: (context, child) {
          var currentHeight = widget.collapsedHeight +
              (widget.expandedHeight - widget.collapsedHeight) *
                  _expandAnimation.value;
          return SizedBox(
            // height: widget.expandedHeight,
            height: currentHeight,
            child: Stack(
              children: [
                Transform.translate(
                  offset: Offset(
                      0, -widget.collapsedHeight * (_flipAnimation.value)),
                  child: Transform(
                    alignment: Alignment.bottomCenter,
                    transform: Matrix4.identity()
                      ..setEntry(2, 3, 0.001)
                      ..rotateX(-math.pi / 2 * _flipAnimation.value),
                    child: widget.collapsedChild,
                  ),
                ),
                Transform.translate(
                  offset: Offset(
                      0, -widget.collapsedHeight * (_flipAnimation.value - 1)),
                  child: Transform(
                    alignment: Alignment.topCenter,
                    transform: Matrix4.identity()
                      ..setEntry(2, 3, 0.001)
                      ..rotateX(math.pi * (1 - _flipAnimation.value) / 2),
                    child: OverflowBox(
                      maxHeight: widget.expandedHeight,
                      child: widget.expandedChild,
                    ),
                  ),
                )
              ],
            ),
          );
        },
      ),
    );
  }

  void _toggle() {
    if (_controller.status == AnimationStatus.completed ||
        _controller.status == AnimationStatus.forward) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }
}

```
{% endcollapsible %}

I'm keeping this post short but I can add more details on the implementation if this brings up some interest in people.