37

I'm trying to recreate something like ExpansionTile but in a Card. When I click the card, its child renders and the card changes its height, so I want to animate that change.

I tried using AnimatedContainer and GlobalKey to know the final size of the card with its child rendered and then set the new height to AnimatedContainer but that didn't work.

Diego Francisco
  • 1,650
  • 1
  • 17
  • 31

4 Answers4

94

In the end I just had to use AnimatedSize. It replicates exactly the animation that I want.

AnimatedSize(
  vsync: this,
  duration: Duration(milliseconds: 150),
  curve: Curves.fastOutSlowIn,
  child: Container(
    child: Container(
      child: !_isExpanded
          ? null
          : FadeTransition(opacity: animationFade, child: widget.child),
    ),
  ),
);
Diego Francisco
  • 1,650
  • 1
  • 17
  • 31
  • 1
    Nice one. It works perfect when the height gets bigger. But if the height gets smaller, it only animates the widgets around the Container. Not the container itself. – Nk54 Apr 15 '19 at 12:31
  • 1
    Ok sorry, I changed the tree : now the widget with the background (a Container) is the parent of the AnimatedSize. When the height gets smaller, the animation plays well. – Nk54 Apr 15 '19 at 12:41
  • 1
    In my case `AnimatedSize` didn't work, so I used `AnimatedContainer` with dynamic `height` property. – Rodion Mostovoi Jul 02 '21 at 22:39
  • This did exactly what I wanted (in 2022) after I changed the "null" to "Container()". – John Weidner Sep 15 '22 at 15:02
3

You can use the AnimatedContainer for animations

class Animate extends StatefulWidget {
  @override
  _AnimateState createState() => _AnimateState();
}

class _AnimateState extends State<Animate> {
  var height = 200.0;

  @override
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;
    return Scaffold(
      body: Center(
        child: AnimatedContainer(
          color: Colors.amber,
          duration: new Duration(milliseconds: 500),
          height: height,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            if (height == 200.0) {
              height = 400.0;
            } else {
              height = 200.0;
            }
          });
        },
        child: Icon(Icons.settings),
      ),
    );
  }
}
Lakhwinder Singh
  • 6,799
  • 4
  • 25
  • 42
1

I tweaked the ExpansionTile, this has proper animation. Hope this helps

class _FixedExpansionTileState extends State<FixedExpansionTile> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  CurvedAnimation _easeOutAnimation;
  CurvedAnimation _easeInAnimation;
  ColorTween _borderColor;
  ColorTween _headerColor;
  ColorTween _iconColor;
  ColorTween _backgroundColor;
  Animation<double> _iconTurns;

  bool _isExpanded = false;

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(duration: _kExpand, vsync: this);
    _easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
    _easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
    _borderColor = new ColorTween();
    _headerColor = new ColorTween();
    _iconColor = new ColorTween();
    _iconTurns = new Tween<double>(begin: 0.0, end: 0.5).animate(_easeInAnimation);
    _backgroundColor = new ColorTween();

    _isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
    if (_isExpanded)
      _controller.value = 1.0;
  }

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

  void _handleTap() {
    setState(() {
      _isExpanded = !_isExpanded;
      if (_isExpanded)
        _controller.forward();
      else
        _controller.reverse().then<void>((value) {
          setState(() {
            // Rebuild without widget.children.
          });
        });
      PageStorage.of(context)?.writeState(context, _isExpanded);
    });
    if (widget.onExpansionChanged != null)
      widget.onExpansionChanged(_isExpanded);
  }

  Widget _buildChildren(BuildContext context, Widget child) {
    final Color borderSideColor =  Colors.transparent;
  //  final Color titleColor = _headerColor.evaluate(_easeInAnimation);

    return new Container(
      decoration: new BoxDecoration(
        color: _backgroundColor.evaluate(_easeOutAnimation) ?? Colors.transparent,
        border: new Border(
          top: new BorderSide(color: borderSideColor),
          bottom: new BorderSide(color: borderSideColor),
        )
      ),
      child: new Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          IconTheme.merge(
            data: new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)),
            child: new ListTile(
              onTap: _handleTap,
              leading: widget.leading,
              title: new DefaultTextStyle(
                style: Theme.of(context).textTheme.subhead.copyWith(color: Colors.transparent),
                child: widget.title,
              ),
              trailing: widget.trailing ?? new RotationTransition(
                turns: _iconTurns,
                child: const Icon(Icons.expand_more),
              ),
            ),
          ),
          new ClipRect(
            child: new Align(
              heightFactor: _easeInAnimation.value,
              child: child,
            ),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    _borderColor.end = theme.dividerColor;
    _headerColor
      ..begin = theme.textTheme.subhead.color
      ..end = theme.accentColor;
    _iconColor
      ..begin = theme.unselectedWidgetColor
      ..end = theme.accentColor;
    _backgroundColor.end = widget.backgroundColor;

    final bool closed = !_isExpanded && _controller.isDismissed;
    return new AnimatedBuilder(
      animation: _controller.view,
      builder: _buildChildren,
      child: closed ? null : new Column(children: widget.children),
    );

  }
}
GoPro
  • 642
  • 1
  • 10
  • 24
1

It works better for me

AnimatedCrossFade(
  duration: _controller.duration!,
  firstCurve: Curves.easeInOut,
  secondCurve: Curves.easeInOut,
  firstChild: Container(),
  secondChild: widget.content,
  crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
)
DK Kum
  • 23
  • 4
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 04 '22 at 22:00