| 
 | 1 | +// Copyright 2017 The Chromium Authors. All rights reserved.  | 
 | 2 | +// Use of this source code is governed by a BSD-style license that can be  | 
 | 3 | +// found in the LICENSE file.  | 
 | 4 | + | 
 | 5 | +import 'package:flutter/material.dart';  | 
 | 6 | +import 'package:flutter/widgets.dart';  | 
 | 7 | + | 
 | 8 | + | 
 | 9 | +const Duration _kExpand = Duration(milliseconds: 200);  | 
 | 10 | + | 
 | 11 | +/// A single-line [ListTile] with a trailing button that expands or collapses  | 
 | 12 | +/// the tile to reveal or hide the [children].  | 
 | 13 | +///  | 
 | 14 | +/// This widget is typically used with [ListView] to create an  | 
 | 15 | +/// "expand / collapse" list entry. When used with scrolling widgets like  | 
 | 16 | +/// [ListView], a unique [PageStorageKey] must be specified to enable the  | 
 | 17 | +/// [NoBorderExpansionTile] to save and restore its expanded state when it is scrolled  | 
 | 18 | +/// in and out of view.  | 
 | 19 | +///  | 
 | 20 | +/// See also:  | 
 | 21 | +///  | 
 | 22 | +///  * [ListTile], useful for creating expansion tile [children] when the  | 
 | 23 | +///    expansion tile represents a sublist.  | 
 | 24 | +///  * The "Expand/collapse" section of  | 
 | 25 | +///    <https://material.io/guidelines/components/lists-controls.html>.  | 
 | 26 | +class NoBorderExpansionTile extends StatefulWidget {  | 
 | 27 | +  /// Creates a single-line [ListTile] with a trailing button that expands or collapses  | 
 | 28 | +  /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must  | 
 | 29 | +  /// be non-null.  | 
 | 30 | +  const NoBorderExpansionTile({  | 
 | 31 | +    Key key,  | 
 | 32 | +    this.leading,  | 
 | 33 | +    @required this.title,  | 
 | 34 | +    this.subtitle,  | 
 | 35 | +    this.backgroundColor,  | 
 | 36 | +    this.onExpansionChanged,  | 
 | 37 | +    this.children = const <Widget>[],  | 
 | 38 | +    this.trailing,  | 
 | 39 | +    this.initiallyExpanded = false,  | 
 | 40 | +  }) : assert(initiallyExpanded != null),  | 
 | 41 | +        super(key: key);  | 
 | 42 | + | 
 | 43 | +  /// A widget to display before the title.  | 
 | 44 | +  ///  | 
 | 45 | +  /// Typically a [CircleAvatar] widget.  | 
 | 46 | +  final Widget leading;  | 
 | 47 | + | 
 | 48 | +  /// The primary content of the list item.  | 
 | 49 | +  ///  | 
 | 50 | +  /// Typically a [Text] widget.  | 
 | 51 | +  final Widget title;  | 
 | 52 | + | 
 | 53 | +  /// Additional content displayed below the title.  | 
 | 54 | +  ///  | 
 | 55 | +  /// Typically a [Text] widget.  | 
 | 56 | +  final Widget subtitle;  | 
 | 57 | + | 
 | 58 | +  /// Called when the tile expands or collapses.  | 
 | 59 | +  ///  | 
 | 60 | +  /// When the tile starts expanding, this function is called with the value  | 
 | 61 | +  /// true. When the tile starts collapsing, this function is called with  | 
 | 62 | +  /// the value false.  | 
 | 63 | +  final ValueChanged<bool> onExpansionChanged;  | 
 | 64 | + | 
 | 65 | +  /// The widgets that are displayed when the tile expands.  | 
 | 66 | +  ///  | 
 | 67 | +  /// Typically [ListTile] widgets.  | 
 | 68 | +  final List<Widget> children;  | 
 | 69 | + | 
 | 70 | +  /// The color to display behind the sublist when expanded.  | 
 | 71 | +  final Color backgroundColor;  | 
 | 72 | + | 
 | 73 | +  /// A widget to display instead of a rotating arrow icon.  | 
 | 74 | +  final Widget trailing;  | 
 | 75 | + | 
 | 76 | +  /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).  | 
 | 77 | +  final bool initiallyExpanded;  | 
 | 78 | + | 
 | 79 | +  @override  | 
 | 80 | +  _NoBorderExpansionTileState createState() => _NoBorderExpansionTileState();  | 
 | 81 | +}  | 
 | 82 | + | 
 | 83 | +class _NoBorderExpansionTileState extends State<NoBorderExpansionTile> with SingleTickerProviderStateMixin {  | 
 | 84 | +  static final Animatable<double> _easeOutTween = CurveTween(curve: Curves.easeOut);  | 
 | 85 | +  static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn);  | 
 | 86 | +  static final Animatable<double> _halfTween = Tween<double>(begin: 0.0, end: 0.5);  | 
 | 87 | + | 
 | 88 | +  final ColorTween _borderColorTween = ColorTween();  | 
 | 89 | +  final ColorTween _headerColorTween = ColorTween();  | 
 | 90 | +  final ColorTween _iconColorTween = ColorTween();  | 
 | 91 | +  final ColorTween _backgroundColorTween = ColorTween();  | 
 | 92 | + | 
 | 93 | +  AnimationController _controller;  | 
 | 94 | +  Animation<double> _iconTurns;  | 
 | 95 | +  Animation<double> _heightFactor;  | 
 | 96 | +  Animation<Color> _borderColor;  | 
 | 97 | +  Animation<Color> _headerColor;  | 
 | 98 | +  Animation<Color> _iconColor;  | 
 | 99 | +  Animation<Color> _backgroundColor;  | 
 | 100 | + | 
 | 101 | +  bool _isExpanded = false;  | 
 | 102 | + | 
 | 103 | +  @override  | 
 | 104 | +  void initState() {  | 
 | 105 | +    super.initState();  | 
 | 106 | +    _controller = AnimationController(duration: _kExpand, vsync: this);  | 
 | 107 | +    _heightFactor = _controller.drive(_easeInTween);  | 
 | 108 | +    _iconTurns = _controller.drive(_halfTween.chain(_easeInTween));  | 
 | 109 | +    _borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));  | 
 | 110 | +    _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));  | 
 | 111 | +    _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));  | 
 | 112 | +    _backgroundColor = _controller.drive(_backgroundColorTween.chain(_easeOutTween));  | 
 | 113 | + | 
 | 114 | +    _isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;  | 
 | 115 | +    if (_isExpanded)  | 
 | 116 | +      _controller.value = 1.0;  | 
 | 117 | +  }  | 
 | 118 | + | 
 | 119 | + | 
 | 120 | +  @override  | 
 | 121 | +  void dispose() {  | 
 | 122 | +    _controller.dispose();  | 
 | 123 | +    super.dispose();  | 
 | 124 | +  }  | 
 | 125 | + | 
 | 126 | +  void _handleTap() {  | 
 | 127 | +    setState(() {  | 
 | 128 | +      _isExpanded = !_isExpanded;  | 
 | 129 | +      if (_isExpanded) {  | 
 | 130 | +        _controller.forward();  | 
 | 131 | +      } else {  | 
 | 132 | +        _controller.reverse().then<void>((void value) {  | 
 | 133 | +          if (!mounted)  | 
 | 134 | +            return;  | 
 | 135 | +          setState(() {  | 
 | 136 | +            // Rebuild without widget.children.  | 
 | 137 | +          });  | 
 | 138 | +        });  | 
 | 139 | +      }  | 
 | 140 | +      PageStorage.of(context)?.writeState(context, _isExpanded);  | 
 | 141 | +    });  | 
 | 142 | +    if (widget.onExpansionChanged != null)  | 
 | 143 | +      widget.onExpansionChanged(_isExpanded);  | 
 | 144 | +  }  | 
 | 145 | + | 
 | 146 | +  Widget _buildChildren(BuildContext context, Widget child) {  | 
 | 147 | + | 
 | 148 | +    return Container(  | 
 | 149 | +      color: _backgroundColor.value ?? Colors.transparent,  | 
 | 150 | +      child: Column(  | 
 | 151 | +        mainAxisSize: MainAxisSize.min,  | 
 | 152 | +        children: <Widget>[  | 
 | 153 | +          ListTileTheme.merge(  | 
 | 154 | +            iconColor: _iconColor.value,  | 
 | 155 | +            textColor: _headerColor.value,  | 
 | 156 | +            child: ListTile(  | 
 | 157 | +              onTap: _handleTap,  | 
 | 158 | +              leading: widget.leading,  | 
 | 159 | +              title: widget.title,  | 
 | 160 | +              subtitle: widget.subtitle,  | 
 | 161 | +              trailing: widget.trailing ?? RotationTransition(  | 
 | 162 | +                turns: _iconTurns,  | 
 | 163 | +                child: const Icon(Icons.expand_more),  | 
 | 164 | +              ),  | 
 | 165 | +            ),  | 
 | 166 | +          ),  | 
 | 167 | +          ClipRect(  | 
 | 168 | +            child: Align(  | 
 | 169 | +//              alignment: Alignment.topCenter,  | 
 | 170 | +              heightFactor: _heightFactor.value,  | 
 | 171 | +              child: child,  | 
 | 172 | +            ),  | 
 | 173 | +          ),  | 
 | 174 | +        ],  | 
 | 175 | +      ),  | 
 | 176 | +    );  | 
 | 177 | +  }  | 
 | 178 | + | 
 | 179 | +  @override  | 
 | 180 | +  void didChangeDependencies() {  | 
 | 181 | +    final ThemeData theme = Theme.of(context);  | 
 | 182 | +    _borderColorTween  | 
 | 183 | +      ..end = theme.dividerColor;  | 
 | 184 | +    _headerColorTween  | 
 | 185 | +      ..begin = theme.textTheme.subhead.color  | 
 | 186 | +      ..end = theme.accentColor;  | 
 | 187 | +    _iconColorTween  | 
 | 188 | +      ..begin = theme.unselectedWidgetColor  | 
 | 189 | +      ..end = theme.accentColor;  | 
 | 190 | +    _backgroundColorTween  | 
 | 191 | +      ..end = widget.backgroundColor;  | 
 | 192 | +    super.didChangeDependencies();  | 
 | 193 | +  }  | 
 | 194 | + | 
 | 195 | +  @override  | 
 | 196 | +  Widget build(BuildContext context) {  | 
 | 197 | +    final bool closed = !_isExpanded && _controller.isDismissed;  | 
 | 198 | +    return AnimatedBuilder(  | 
 | 199 | +      animation: _controller.view,  | 
 | 200 | +      builder: _buildChildren,  | 
 | 201 | +      child: closed ? null : Column(children: widget.children),  | 
 | 202 | +    );  | 
 | 203 | + | 
 | 204 | +  }  | 
 | 205 | +}  | 
0 commit comments