69

How can I expand and collapse a widget when the user taps on different widget (sibling or parent) with an animation?

new Column(
    children: <Widget>[
        new header.IngridientHeader(
            new Icon(
                Icons.fiber_manual_record,
                color: AppColors.primaryColor
            ),
            'Voice Track 1'
        ),
        new Grid()
    ],
)

I want user to be able to tap on header.IngridientHeader and then Grid widget should toggle (hide if visible and vice versa).

I am trying to do something similar to Collapse in Bootstrap. getbootstrap.com/docs/4.0/components/collapse

The header.IngridientHeader widget should stay in place all the time. The grid is a scrollable (horizontal) widget.

KetZoomer
  • 2,701
  • 3
  • 15
  • 43
Dariusz Włodarz
  • 745
  • 1
  • 7
  • 10

6 Answers6

189

If you want to collapse a widget to zero height or zero width that has a child that overflow when collapsed, I would recommend SizeTransition or ScaleTransition.

Here is an example of the ScaleTransition widget being used to collapse the container for the four black buttons and status text. My ExpandedSection widget is used with a column to get the following structure. An example of the ScaleTransition widget

An example of a Widget that use animation with the SizeTransition widget:

class ExpandedSection extends StatefulWidget {

  final Widget child;
  final bool expand;
  ExpandedSection({this.expand = false, required this.child});

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

class _ExpandedSectionState extends State<ExpandedSection> with SingleTickerProviderStateMixin {
  late AnimationController expandController;
  late Animation<double> animation; 

  @override
  void initState() {
    super.initState();
    prepareAnimations();
    _runExpandCheck();
  }

  ///Setting up the animation
  void prepareAnimations() {
    expandController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 500)
    );
    animation = CurvedAnimation(
      parent: expandController,
      curve: Curves.fastOutSlowIn,
    );
  }

  void _runExpandCheck() {
    if(widget.expand) {
      expandController.forward();
    }
    else {
      expandController.reverse();
    }
  }

  @override
  void didUpdateWidget(ExpandedSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    _runExpandCheck();
  }

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

  @override
  Widget build(BuildContext context) {
    return SizeTransition(
      axisAlignment: 1.0,
      sizeFactor: animation,
      child: widget.child
    );
  }
}

AnimatedContainer also works but Flutter can complain about overflow if the child is not resizable to zero width or zero height.

Keenan
  • 179
  • 1
  • 9
Adam Jonsson
  • 2,064
  • 1
  • 13
  • 9
  • Hey can you please give the code that you used to create the animation. In my case the size transition is working but it is taking up space when the size is 0. – bnayagrawal Apr 22 '19 at 10:19
  • Maybe I misunderstand your request but I create the animation in prepareAnimations() in the code above. If you use the IntrinsicHeight or IntrinsicWidth widget as the parent for the widget that you are trying to animate, the widget is still going to take up space when size is 0. – Adam Jonsson Apr 25 '19 at 08:29
  • But can you please provide the source code of the above widget animation you have done? – bnayagrawal Apr 29 '19 at 03:58
  • 6
    I made a [working app](https://gist.github.com/AdamJonsson/57caa39ec3584343833ece556198187c) similar to the gif animation using the widget above I created. I can not unfortunately share the specific code for the app above because I do not own the project. – Adam Jonsson Apr 30 '19 at 06:34
  • 1
    Thanks much. For widget to be expanded on start, I had to use `forward` and `reverse` logic in `initState()` along with `didWidgetUpdate()` – sha Sep 04 '19 at 19:09
  • 1
    You don't need the `addListener()` and `setState`, as `SizeTransition` does this automatically. – Michel Feinstein Nov 26 '19 at 19:52
  • works like a charm for containers with dynamic height! thank you – saturov May 22 '20 at 09:39
  • 2
    I needed this for our project so adapted this code to make an ExpandableSection widget. You can customize the animation duration and curve. Caveat: it requires flutter_hooks to use. https://gist.github.com/venkatd/b820b3b34b09239d85bf51ccb7f2278a – Venkat D. Nov 23 '20 at 15:48
  • Thank you so much. I searching this answer from long time. You nailed it. – Sharath B Naik Mar 03 '22 at 03:23
  • This is better than animated container complaining about size issues. Thank you! – Wesley Barnes May 23 '22 at 23:32
  • This is good, Tks! – Kyo Huu Sep 30 '22 at 09:06
23

Alternatively you can just use an AnimatedContainer to mimic this behavior.

enter image description here

class AnimateContentExample extends StatefulWidget {
  @override
  _AnimateContentExampleState createState() => new _AnimateContentExampleState();
}

class _AnimateContentExampleState extends State<AnimateContentExample> {
  double _animatedHeight = 100.0;
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text("Animate Content"),),
      body: new Column(
        children: <Widget>[
          new Card(
            child: new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                new GestureDetector(
                  onTap: ()=>setState((){
                    _animatedHeight!=0.0?_animatedHeight=0.0:_animatedHeight=100.0;}),
                  child:  new Container(
                  child: new Text("CLICK ME"),
                  color: Colors.blueAccent,
                  height: 25.0,
                    width: 100.0,
                ),),
                new AnimatedContainer(duration: const Duration(milliseconds: 120),
                  child: new Text("Toggle Me"),
                  height: _animatedHeight,
                  color: Colors.tealAccent,
                  width: 100.0,
                )
              ],
            ) ,
          )
        ],
      ),
    );
  }
}
Keenan
  • 179
  • 1
  • 9
Shady Aziza
  • 50,824
  • 20
  • 115
  • 113
15

I think you are looking for ExpansionTile widget. This takes a title property which is equivalent to header and children property to which you can pass widgets to be shown or hidden on toggle. You can find an example of how to use it here.

Simple Example Usage:

new ExpansionTile(title: new Text("Numbers"),
      children: <Widget>[
        new Text("Number: 1"),
        new Text("Number: 2"),
        new Text("Number: 3"),
        new Text("Number: 4"),
        new Text("Number: 5")
      ],
),

Hope that helps!

Keenan
  • 179
  • 1
  • 9
Hemanth Raj
  • 32,555
  • 10
  • 92
  • 82
  • your answer would be perfect if not for a fact that my header.IngridientHeader ( new Text("Numbers") in your code ) must stay in place and Grid ( children in your code ) is scrollable widget. Sorry for not pointing that out earlyer – Dariusz Włodarz Feb 28 '18 at 14:38
  • 1
    I think this does the same. Just add a scrollable child to children. Is that what you wanted. Or can you point to some visual reference on what you are trying to achieve exactly. – Hemanth Raj Feb 28 '18 at 14:58
9

Output:

enter image description here


Code:

class FooPageState extends State<SoPage> {
  static const _duration = Duration(seconds: 1);
  int _flex1 = 1, _flex2 = 2, _flex3 = 1;

  @override
  Widget build(BuildContext context) {
    final total = _flex1 + _flex2 + _flex3;
    final height = MediaQuery.of(context).size.height;
    final height1 = (height * _flex1) / total;
    final height2 = (height * _flex2) / total;
    final height3 = (height * _flex3) / total;

    return Scaffold(
      body: Column(
        children: [
          AnimatedContainer(
            height: height1,
            duration: _duration,
            color: Colors.red,
          ),
          AnimatedContainer(
            height: height2,
            duration: _duration,
            color: Colors.green,
          ),
          AnimatedContainer(
            height: height3,
            duration: _duration,
            color: Colors.blue,
          ),
        ],
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
5

Thanks to @Adam Jonsson, his answer resolved my problem. And this is the demo about how to use ExpandedSection, hope to help you.

class ExpandedSection extends StatefulWidget {
  final Widget child;
  final bool expand;

  ExpandedSection({this.expand = false, this.child});

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

class _ExpandedSectionState extends State<ExpandedSection>
    with SingleTickerProviderStateMixin {
  AnimationController expandController;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    prepareAnimations();
    _runExpandCheck();
  }

  ///Setting up the animation
  void prepareAnimations() {
    expandController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 500));
    animation = CurvedAnimation(
      parent: expandController,
      curve: Curves.fastOutSlowIn,
    );
  }

  void _runExpandCheck() {
    if (widget.expand) {
      expandController.forward();
    } else {
      expandController.reverse();
    }
  }

  @override
  void didUpdateWidget(ExpandedSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    _runExpandCheck();
  }

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

  @override
  Widget build(BuildContext context) {
    return SizeTransition(
        axisAlignment: 1.0, sizeFactor: animation, child: widget.child);
  }
}
  
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Demo'),
        ),
        body: Home(),
      ),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  bool _expand = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Header(
          onTap: () {
            setState(() {
              _expand = !_expand;
            });
          },
        ),
        ExpandedSection(child: Content(), expand: _expand,)
      ],
    );
  }
}

class Header extends StatelessWidget {
  final VoidCallback onTap;

  Header({@required this.onTap});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        color: Colors.cyan,
        height: 100,
        width: double.infinity,
        child: Center(
          child: Text(
            'Header -- Tap me to expand!',
            style: TextStyle(color: Colors.white, fontSize: 20),
          ),
        ),
      ),
    );
  }
}

class Content extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.lightGreen,
      height: 400,
    );
  }
}
Yahoho
  • 420
  • 4
  • 9
2

Another solution that doesn't require an animation controller is using AnimatedSwitcher widget with SizeTransition as a transition builder.

here is a simple example:

AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  transitionBuilder: (child, animation) {
    return SizeTransition(sizeFactor: animation, child: child);
  },
  child: expanded ? YourWidget() : null,
)

Of course you can customize the curve and layout builder for the animation.

Ansshkki
  • 760
  • 8
  • 14