// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'icons.dart'; import 'list_tile.dart'; import 'theme.dart'; import 'theme_data.dart'; const Duration _kExpand = const 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 /// <https://material.io/guidelines/components/lists-controls.html>. class ExpansionTile extends StatefulWidget { /// Creates a single-line [ListTile] with a trailing button that expands or collapses /// the tile to reveal or hide the [children]. const ExpansionTile({ Key key, this.leading, @required this.title, this.backgroundColor, this.onExpansionChanged, this.children: const <Widget>[], this.trailing, }) : super(key: key); /// 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<bool> onExpansionChanged; /// The widgets that are displayed when the tile expands. /// /// Typically [ListTile] widgets. final List<Widget> 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; @override _ExpansionTileState createState() => new _ExpansionTileState(); } class _ExpansionTileState extends State<ExpansionTile> 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(begin: Colors.transparent); _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) ?? false; 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<Null>((Null 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 = _borderColor.evaluate(_easeOutAnimation); final Color titleColor = _headerColor.evaluate(_easeInAnimation); return new Container( decoration: new BoxDecoration( color: _backgroundColor.evaluate(_easeOutAnimation), 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: titleColor), 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 ..begin = Colors.transparent ..end = widget.backgroundColor ?? Colors.transparent; final bool closed = !_isExpanded && _controller.isDismissed; return new AnimatedBuilder( animation: _controller.view, builder: _buildChildren, child: closed ? null : new Column(children: widget.children), ); } }