import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; const Duration _kExpand = Duration(milliseconds: 200); /// A single-line [ListTile] with a trailing button that expands or collapses /// the tile to reveal or hide the [children]. /// /// This widget is typically used with [ListView] to create an /// "expand / collapse" list entry. When used with scrolling widgets like /// [ListView], a unique [PageStorageKey] must be specified to enable the /// [ExpansionTile] to save and restore its expanded state when it is scrolled /// in and out of view. /// /// See also: /// /// * [ListTile], useful for creating expansion tile [children] when the /// expansion tile represents a sublist. /// * The "Expand/collapse" section of /// . class ExpansionCard extends StatefulWidget { /// Creates a single-line [ListTile] with a trailing button that expands or collapses /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must /// be non-null. const ExpansionCard({ Key key, this.leading, @required this.title, this.background, this.backgroundColor, this.margin = const EdgeInsets.only(top: 30), this.borderRadius = 30.0, this.onExpansionChanged, this.children = const [], this.trailing, this.initiallyExpanded = false, this.color, }) : assert(initiallyExpanded != null), super(key: key); /// Adds margin to content of the card. final EdgeInsets margin; /// Provides CircularRadius to the border of the card. final double borderRadius; /// A widget to add background. /// it can be a gif or image. final Widget background; /// A widget to display before the title. /// /// Typically a [CircleAvatar] widget. final Widget leading; /// The primary content of the list item. /// /// Typically a [Text] widget. final Widget title; /// Called when the tile expands or collapses. /// /// When the tile starts expanding, this function is called with the value /// true. When the tile starts collapsing, this function is called with /// the value false. final ValueChanged onExpansionChanged; /// The widgets that are displayed when the tile expands. /// /// Typically [ListTile] widgets. final List children; /// The color to display behind the sublist when expanded. final Color backgroundColor; /// A widget to display instead of a rotating arrow icon. final Widget trailing; /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default). final bool initiallyExpanded; /// Color of the expanded heading and icon final Color color; @override _ExpansionTileState createState() => _ExpansionTileState(); } class _ExpansionTileState extends State with SingleTickerProviderStateMixin { static final Animatable _easeOutTween = CurveTween(curve: Curves.easeOut); static final Animatable _easeInTween = CurveTween(curve: Curves.easeIn); static final Animatable _halfTween = Tween(begin: 0.0, end: 0.5); final ColorTween _borderColorTween = ColorTween(); final ColorTween _headerColorTween = ColorTween(); final ColorTween _iconColorTween = ColorTween(); final ColorTween _backgroundColorTween = ColorTween(); AnimationController _controller; Animation _iconTurns; Animation _heightFactor; Animation _headerColor; Animation _iconColor; Animation _backgroundColor; bool _isExpanded = false; @override void initState() { super.initState(); _controller = AnimationController(duration: _kExpand, vsync: this); _heightFactor = _controller.drive(_easeInTween); _iconTurns = _controller.drive(_halfTween.chain(_easeInTween)); _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween)); _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween)); _backgroundColor = _controller.drive(_backgroundColorTween.chain(_easeOutTween)); _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) { if (!mounted) return; 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; // _borderColor.value ?? return Stack( children: [ widget.background == null ? Container() : ClipRRect( borderRadius: BorderRadius.circular(widget.borderRadius), child: Align( heightFactor: _heightFactor.value < 0.5 ? 0.5 : _heightFactor.value, child: widget.background, ), ), Container( decoration: BoxDecoration( color: _backgroundColor.value ?? Colors.transparent, border: Border( top: BorderSide(color: borderSideColor), bottom: BorderSide(color: borderSideColor), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTileTheme.merge( iconColor: _iconColor.value, textColor: _headerColor.value, child: Container( margin: widget.margin, child: ListTile( onTap: _handleTap, leading: widget.leading, title: widget.title, trailing: widget.trailing ?? RotationTransition( turns: _iconTurns, child: const Icon(Icons.expand_more), ), ), )), ClipRect( child: Align( heightFactor: _heightFactor.value, child: child, ), ), ], ), ) ], ); } @override void didChangeDependencies() { final ThemeData theme = Theme.of(context); _borderColorTween..end = theme.dividerColor; _headerColorTween ..begin = Colors.white ..end = widget.color ?? Color(0xff60c9df); _iconColorTween ..begin = Colors.white ..end = widget.color ?? Color(0xff60c9df); _backgroundColorTween..end = widget.backgroundColor; super.didChangeDependencies(); } @override Widget build(BuildContext context) { final bool closed = !_isExpanded && _controller.isDismissed; return AnimatedBuilder( animation: _controller.view, builder: _buildChildren, child: closed ? null : Column(children: widget.children), ); } }