configurable_expansion_tile.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import 'package:flutter/material.dart';
  2. /// A configurable Expansion Tile edited from the flutter material implementation
  3. /// that allows for customization of most of the behaviour. Includes providing colours,
  4. /// replacement widgets on expansion, and animating preceding/following widgets.
  5. ///
  6. /// See:
  7. /// [ExpansionTile]
  8. class ConfigurableExpansionTile extends StatefulWidget {
  9. /// Creates a a [Widget] with an optional [animatedWidgetPrecedingHeader] and/or
  10. /// [animatedWidgetFollowingHeader]. Optionally, the header can change on the
  11. /// expanded state by proving a [Widget] in [headerExpanded]. Colors can also
  12. /// be specified for the animated transitions/states. [children] are revealed
  13. /// when the expansion tile is expanded.
  14. const ConfigurableExpansionTile(
  15. {Key key,
  16. this.headerBackgroundColorStart = Colors.transparent,
  17. this.onExpansionChanged,
  18. this.children = const <Widget>[],
  19. this.initiallyExpanded = false,
  20. @required this.header,
  21. this.animatedWidgetFollowingHeader,
  22. this.animatedWidgetPrecedingHeader,
  23. this.expandedBackgroundColor,
  24. this.borderColorStart = Colors.transparent,
  25. this.borderColorEnd = Colors.transparent,
  26. this.topBorderOn = true,
  27. this.bottomBorderOn = true,
  28. this.kExpand = const Duration(milliseconds: 200),
  29. this.headerBackgroundColorEnd,
  30. this.headerExpanded,
  31. this.headerAnimationTween,
  32. this.borderAnimationTween,
  33. this.animatedWidgetTurnTween,
  34. this.animatedWidgetTween})
  35. : assert(initiallyExpanded != null),
  36. super(key: key);
  37. /// Called when the tile expands or collapses.
  38. ///
  39. /// When the tile starts expanding, this function is called with the value
  40. /// true. When the tile starts collapsing, this function is called with
  41. /// the value false.
  42. final ValueChanged<bool> onExpansionChanged;
  43. /// The widgets that are displayed when the tile expands.
  44. ///
  45. /// Typically [ListTile] widgets.
  46. final List<Widget> children;
  47. /// The color of the header, useful to set if your animating widgets are
  48. /// larger than the header widget, or you want an animating color, in which
  49. /// case your header widget should be transparent
  50. final Color headerBackgroundColorStart;
  51. /// The [Color] the header will transition to on expand
  52. final Color headerBackgroundColorEnd;
  53. /// The [Color] of the background of the [children] when expanded
  54. final Color expandedBackgroundColor;
  55. /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
  56. final bool initiallyExpanded;
  57. /// The header for the expansion tile
  58. final Widget header;
  59. /// An optional widget to replace [header] with if the list is expanded
  60. final Widget headerExpanded;
  61. /// A widget to rotate following the [header] (ie an arrow)
  62. final Widget animatedWidgetFollowingHeader;
  63. /// A widget to rotate preceding the [header] (ie an arrow)
  64. final Widget animatedWidgetPrecedingHeader;
  65. /// The duration of the animations
  66. final Duration kExpand;
  67. /// The color the border start, before the list is expanded
  68. final Color borderColorStart;
  69. /// The color of the border at the end of animation, after the list is expanded
  70. final Color borderColorEnd;
  71. /// Turns the top border of the list is on/off
  72. final bool topBorderOn;
  73. /// Turns the bottom border of the list on/off
  74. final bool bottomBorderOn;
  75. /// Header transition tween
  76. final Animatable<double> headerAnimationTween;
  77. /// Border animation tween
  78. final Animatable<double> borderAnimationTween;
  79. /// Tween for turning [animatedWidgetFollowingHeader] and [animatedWidgetPrecedingHeader]
  80. final Animatable<double> animatedWidgetTurnTween;
  81. /// [animatedWidgetFollowingHeader] and [animatedWidgetPrecedingHeader] transition tween
  82. final Animatable<double> animatedWidgetTween;
  83. static final Animatable<double> _easeInTween =
  84. CurveTween(curve: Curves.easeIn);
  85. static final Animatable<double> _halfTween =
  86. Tween<double>(begin: 0.0, end: 0.5);
  87. static final Animatable<double> _easeOutTween =
  88. CurveTween(curve: Curves.easeOut);
  89. @override
  90. _ConfigurableExpansionTileState createState() =>
  91. _ConfigurableExpansionTileState();
  92. }
  93. class _ConfigurableExpansionTileState extends State<ConfigurableExpansionTile>
  94. with SingleTickerProviderStateMixin {
  95. AnimationController _controller;
  96. Animation<double> _iconTurns;
  97. Animation<double> _heightFactor;
  98. Animation<Color> _borderColor;
  99. Animation<Color> _headerColor;
  100. final ColorTween _borderColorTween = ColorTween();
  101. final ColorTween _headerColorTween = ColorTween();
  102. bool _isExpanded = false;
  103. @override
  104. void initState() {
  105. super.initState();
  106. _controller = AnimationController(duration: widget.kExpand, vsync: this);
  107. _heightFactor = _controller.drive(ConfigurableExpansionTile._easeInTween);
  108. _iconTurns = _controller.drive(
  109. (widget.animatedWidgetTurnTween ?? ConfigurableExpansionTile._halfTween)
  110. .chain(widget.animatedWidgetTween ??
  111. ConfigurableExpansionTile._easeInTween));
  112. _borderColor = _controller.drive(_borderColorTween.chain(
  113. widget.borderAnimationTween ??
  114. ConfigurableExpansionTile._easeOutTween));
  115. _borderColorTween.end = widget.borderColorEnd;
  116. _headerColor = _controller.drive(_headerColorTween.chain(
  117. widget.headerAnimationTween ?? ConfigurableExpansionTile._easeInTween));
  118. _headerColorTween.end =
  119. widget.headerBackgroundColorEnd ?? widget.headerBackgroundColorStart;
  120. _isExpanded =
  121. PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
  122. if (_isExpanded) _controller.value = 1.0;
  123. }
  124. @override
  125. void dispose() {
  126. _controller.dispose();
  127. super.dispose();
  128. }
  129. void _handleTap() {
  130. setState(() {
  131. _isExpanded = !_isExpanded;
  132. if (_isExpanded) {
  133. _controller.forward();
  134. } else {
  135. _controller.reverse().then<void>((void value) {
  136. if (!mounted) return;
  137. setState(() {
  138. // Rebuild without widget.children.
  139. });
  140. });
  141. }
  142. PageStorage.of(context)?.writeState(context, _isExpanded);
  143. });
  144. if (widget.onExpansionChanged != null)
  145. widget.onExpansionChanged(_isExpanded);
  146. }
  147. Widget _buildChildren(BuildContext context, Widget child) {
  148. final Color borderSideColor = _borderColor.value ?? widget.borderColorStart;
  149. final Color headerColor =
  150. _headerColor?.value ?? widget.headerBackgroundColorStart;
  151. return Container(
  152. decoration: BoxDecoration(
  153. border: Border(
  154. top: BorderSide(
  155. color: widget.topBorderOn ? borderSideColor : Colors.transparent),
  156. bottom: BorderSide(
  157. color:
  158. widget.bottomBorderOn ? borderSideColor : Colors.transparent),
  159. )),
  160. child: Column(
  161. children: <Widget>[
  162. GestureDetector(
  163. onTap: _handleTap,
  164. child: Container(
  165. color: headerColor,
  166. child: Row(
  167. children: <Widget>[
  168. RotationTransition(
  169. turns: _iconTurns,
  170. child:
  171. widget.animatedWidgetPrecedingHeader ?? Container(),
  172. ),
  173. Expanded(child: _getHeader()),
  174. RotationTransition(
  175. turns: _iconTurns,
  176. child:
  177. widget.animatedWidgetFollowingHeader ?? Container(),
  178. )
  179. ],
  180. ))),
  181. ClipRect(
  182. child: Align(
  183. heightFactor: _heightFactor.value,
  184. child: child,
  185. ),
  186. ),
  187. ],
  188. ),
  189. );
  190. }
  191. /// Retrieves the header to display for the tile, derived from [_isExpanded] state
  192. Widget _getHeader() {
  193. if (!_isExpanded) {
  194. return widget.header;
  195. } else {
  196. return widget.headerExpanded ?? widget.header;
  197. }
  198. }
  199. @override
  200. Widget build(BuildContext context) {
  201. final bool closed = !_isExpanded && _controller.isDismissed;
  202. return AnimatedBuilder(
  203. animation: _controller.view,
  204. builder: _buildChildren,
  205. child: closed
  206. ? null
  207. : Container(
  208. color: widget.expandedBackgroundColor ?? Colors.transparent,
  209. child: Column(children: widget.children)),
  210. );
  211. }
  212. }