Commit 1157812b authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added ExpansionTile and deprecated TwoLevelList et al (#9938)

parent 5b18fca0
...@@ -4,6 +4,20 @@ ...@@ -4,6 +4,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum _MaterialListType {
/// A list tile that contains a single line of text.
oneLine,
/// A list tile that contains a [CircleAvatar] followed by a single line of text.
oneLineWithAvatar,
/// A list tile that contains two lines of text.
twoLine,
/// A list tile that contains three lines of text.
threeLine,
}
class ListDemo extends StatefulWidget { class ListDemo extends StatefulWidget {
const ListDemo({ Key key }) : super(key: key); const ListDemo({ Key key }) : super(key: key);
...@@ -17,7 +31,7 @@ class _ListDemoState extends State<ListDemo> { ...@@ -17,7 +31,7 @@ class _ListDemoState extends State<ListDemo> {
static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(); static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
PersistentBottomSheetController<Null> _bottomSheet; PersistentBottomSheetController<Null> _bottomSheet;
MaterialListType _itemType = MaterialListType.threeLine; _MaterialListType _itemType = _MaterialListType.threeLine;
bool _dense = false; bool _dense = false;
bool _showAvatars = true; bool _showAvatars = true;
bool _showIcons = false; bool _showIcons = false;
...@@ -27,7 +41,7 @@ class _ListDemoState extends State<ListDemo> { ...@@ -27,7 +41,7 @@ class _ListDemoState extends State<ListDemo> {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
]; ];
void changeItemType(MaterialListType type) { void changeItemType(_MaterialListType type) {
setState(() { setState(() {
_itemType = type; _itemType = type;
}); });
...@@ -47,8 +61,8 @@ class _ListDemoState extends State<ListDemo> { ...@@ -47,8 +61,8 @@ class _ListDemoState extends State<ListDemo> {
new ListTile( new ListTile(
dense: true, dense: true,
title: const Text('One-line'), title: const Text('One-line'),
trailing: new Radio<MaterialListType>( trailing: new Radio<_MaterialListType>(
value: _showAvatars ? MaterialListType.oneLineWithAvatar : MaterialListType.oneLine, value: _showAvatars ? _MaterialListType.oneLineWithAvatar : _MaterialListType.oneLine,
groupValue: _itemType, groupValue: _itemType,
onChanged: changeItemType, onChanged: changeItemType,
) )
...@@ -56,8 +70,8 @@ class _ListDemoState extends State<ListDemo> { ...@@ -56,8 +70,8 @@ class _ListDemoState extends State<ListDemo> {
new ListTile( new ListTile(
dense: true, dense: true,
title: const Text('Two-line'), title: const Text('Two-line'),
trailing: new Radio<MaterialListType>( trailing: new Radio<_MaterialListType>(
value: MaterialListType.twoLine, value: _MaterialListType.twoLine,
groupValue: _itemType, groupValue: _itemType,
onChanged: changeItemType, onChanged: changeItemType,
) )
...@@ -65,8 +79,8 @@ class _ListDemoState extends State<ListDemo> { ...@@ -65,8 +79,8 @@ class _ListDemoState extends State<ListDemo> {
new ListTile( new ListTile(
dense: true, dense: true,
title: const Text('Three-line'), title: const Text('Three-line'),
trailing: new Radio<MaterialListType>( trailing: new Radio<_MaterialListType>(
value: MaterialListType.threeLine, value: _MaterialListType.threeLine,
groupValue: _itemType, groupValue: _itemType,
onChanged: changeItemType, onChanged: changeItemType,
), ),
...@@ -143,15 +157,15 @@ class _ListDemoState extends State<ListDemo> { ...@@ -143,15 +157,15 @@ class _ListDemoState extends State<ListDemo> {
Widget buildListTile(BuildContext context, String item) { Widget buildListTile(BuildContext context, String item) {
Widget secondary; Widget secondary;
if (_itemType == MaterialListType.twoLine) { if (_itemType == _MaterialListType.twoLine) {
secondary = const Text("Additional item information."); secondary = const Text("Additional item information.");
} else if (_itemType == MaterialListType.threeLine) { } else if (_itemType == _MaterialListType.threeLine) {
secondary = const Text( secondary = const Text(
"Even more additional list item information appears on line three.", "Even more additional list item information appears on line three.",
); );
} }
return new ListTile( return new ListTile(
isThreeLine: _itemType == MaterialListType.threeLine, isThreeLine: _itemType == _MaterialListType.threeLine,
dense: _dense, dense: _dense,
leading: _showAvatars ? new CircleAvatar(child: new Text(item)) : null, leading: _showAvatars ? new CircleAvatar(child: new Text(item)) : null,
title: new Text('This item represents $item.'), title: new Text('This item represents $item.'),
...@@ -165,14 +179,14 @@ class _ListDemoState extends State<ListDemo> { ...@@ -165,14 +179,14 @@ class _ListDemoState extends State<ListDemo> {
final String layoutText = _dense ? " \u2013 Dense" : ""; final String layoutText = _dense ? " \u2013 Dense" : "";
String itemTypeText; String itemTypeText;
switch (_itemType) { switch (_itemType) {
case MaterialListType.oneLine: case _MaterialListType.oneLine:
case MaterialListType.oneLineWithAvatar: case _MaterialListType.oneLineWithAvatar:
itemTypeText = 'Single-line'; itemTypeText = 'Single-line';
break; break;
case MaterialListType.twoLine: case _MaterialListType.twoLine:
itemTypeText = 'Two-line'; itemTypeText = 'Two-line';
break; break;
case MaterialListType.threeLine: case _MaterialListType.threeLine:
itemTypeText = 'Three-line'; itemTypeText = 'Three-line';
break; break;
} }
......
...@@ -11,22 +11,21 @@ class TwoLevelListDemo extends StatelessWidget { ...@@ -11,22 +11,21 @@ class TwoLevelListDemo extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
appBar: new AppBar(title: const Text('Expand/collapse list control')), appBar: new AppBar(title: const Text('Expand/collapse list control')),
body: new TwoLevelList( body: new ListView(
type: MaterialListType.oneLine,
children: <Widget>[ children: <Widget>[
const TwoLevelListItem(title: const Text('Top')), const ListTile(title: const Text('Top')),
new TwoLevelSublist( new ExpansionTile(
title: const Text('Sublist'), title: const Text('Sublist'),
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025), backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
children: <Widget>[ children: <Widget>[
const TwoLevelListItem(title: const Text('One')), const ListTile(title: const Text('One')),
const TwoLevelListItem(title: const Text('Two')), const ListTile(title: const Text('Two')),
// https://en.wikipedia.org/wiki/Free_Four // https://en.wikipedia.org/wiki/Free_Four
const TwoLevelListItem(title: const Text('Free')), const ListTile(title: const Text('Free')),
const TwoLevelListItem(title: const Text('Four')) const ListTile(title: const Text('Four'))
] ]
), ),
const TwoLevelListItem(title: const Text('Bottom')) const ListTile(title: const Text('Bottom'))
] ]
) )
); );
......
...@@ -40,6 +40,7 @@ export 'src/material/drawer_header.dart'; ...@@ -40,6 +40,7 @@ export 'src/material/drawer_header.dart';
export 'src/material/dropdown.dart'; export 'src/material/dropdown.dart';
export 'src/material/expand_icon.dart'; export 'src/material/expand_icon.dart';
export 'src/material/expansion_panel.dart'; export 'src/material/expansion_panel.dart';
export 'src/material/expansion_tile.dart';
export 'src/material/flat_button.dart'; export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart'; export 'src/material/flexible_space_bar.dart';
export 'src/material/floating_action_button.dart'; export 'src/material/floating_action_button.dart';
......
// 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 'icon.dart';
import 'icon_theme.dart';
import 'icon_theme_data.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.
///
/// 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>[],
}) : 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;
@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();
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: 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;
return new AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: new Column(children: widget.children),
);
}
}
...@@ -13,27 +13,6 @@ import 'icon_theme_data.dart'; ...@@ -13,27 +13,6 @@ import 'icon_theme_data.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'theme.dart'; import 'theme.dart';
/// The kind of list tiles contained in a material design list.
///
/// See also:
///
/// * [ListTile]
/// * [kListTileExtent]
/// * <https://material.google.com/components/lists.html#lists-specs>
enum MaterialListType {
/// A list tile that contains a single line of text.
oneLine,
/// A list tile that contains a [CircleAvatar] followed by a single line of text.
oneLineWithAvatar,
/// A list tile that contains two lines of text.
twoLine,
/// A list tile that contains three lines of text.
threeLine,
}
/// Defines the title font used for [ListTile] descendants of a [ListTileTheme]. /// Defines the title font used for [ListTile] descendants of a [ListTileTheme].
/// ///
/// List tiles that appear in a [Drawer] use the theme's [TextTheme.body2] /// List tiles that appear in a [Drawer] use the theme's [TextTheme.body2]
...@@ -47,21 +26,6 @@ enum ListTileStyle { ...@@ -47,21 +26,6 @@ enum ListTileStyle {
drawer, drawer,
} }
/// The vertical extent of the different types of material list tiles.
///
/// See also:
///
/// * [MaterialListType]
/// * [ListTile]
/// * [kListTileExtent]
/// * <https://material.google.com/components/lists.html#lists-specs>
Map<MaterialListType, double> kListTileExtent = const <MaterialListType, double>{
MaterialListType.oneLine: 48.0,
MaterialListType.oneLineWithAvatar: 56.0,
MaterialListType.twoLine: 72.0,
MaterialListType.threeLine: 88.0,
};
/// An inherited widget that defines color and style parameters for [ListTile]s /// An inherited widget that defines color and style parameters for [ListTile]s
/// in this widget's subtree. /// in this widget's subtree.
/// ///
......
...@@ -14,19 +14,35 @@ import 'list_tile.dart'; ...@@ -14,19 +14,35 @@ import 'list_tile.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
/// This enum is deprecated. Please use [ListTileTheme] instead.
enum MaterialListType {
/// A list tile that contains a single line of text.
oneLine,
/// A list tile that contains a [CircleAvatar] followed by a single line of text.
oneLineWithAvatar,
/// A list tile that contains two lines of text.
twoLine,
/// A list tile that contains three lines of text.
threeLine,
}
/// This constant is deprecated. The [ListTile] class sizes itself based on
/// its content and [ListTileTheme].
@deprecated
Map<MaterialListType, double> kListTileExtent = const <MaterialListType, double>{
MaterialListType.oneLine: 48.0,
MaterialListType.oneLineWithAvatar: 56.0,
MaterialListType.twoLine: 72.0,
MaterialListType.threeLine: 88.0,
};
const Duration _kExpand = const Duration(milliseconds: 200); const Duration _kExpand = const Duration(milliseconds: 200);
/// An item in a [TwoLevelList] or a [TwoLevelSublist]. /// This class is deprecated. Please use [ListTile] instead.
/// @deprecated
/// A two-level list item is similar to a [ListTile], but a two-level list item
/// automatically sizes itself to fit properly within its ancestor
/// [TwoLevelList].
///
/// See also:
///
/// * [TwoLevelList]
/// * [TwoLevelSublist]
/// * [ListTile]
class TwoLevelListItem extends StatelessWidget { class TwoLevelListItem extends StatelessWidget {
/// Creates an item in a two-level list. /// Creates an item in a two-level list.
const TwoLevelListItem({ const TwoLevelListItem({
...@@ -91,16 +107,8 @@ class TwoLevelListItem extends StatelessWidget { ...@@ -91,16 +107,8 @@ class TwoLevelListItem extends StatelessWidget {
} }
} }
/// An item in a [TwoLevelList] that can expand and collapse. /// This class is deprecated. Please use [ExpansionTile] instead.
/// @deprecated
/// A two-level sublist is similar to a [ListTile], but the trailing widget is
/// a button that expands or collapses a sublist of items.
///
/// See also:
///
/// * [TwoLevelList]
/// * [TwoLevelListItem]
/// * [ListTile]
class TwoLevelSublist extends StatefulWidget { class TwoLevelSublist extends StatefulWidget {
/// Creates an item in a two-level list that can expland and collapse. /// Creates an item in a two-level list that can expland and collapse.
const TwoLevelSublist({ const TwoLevelSublist({
...@@ -141,6 +149,7 @@ class TwoLevelSublist extends StatefulWidget { ...@@ -141,6 +149,7 @@ class TwoLevelSublist extends StatefulWidget {
_TwoLevelSublistState createState() => new _TwoLevelSublistState(); _TwoLevelSublistState createState() => new _TwoLevelSublistState();
} }
@deprecated
class _TwoLevelSublistState extends State<TwoLevelSublist> with SingleTickerProviderStateMixin { class _TwoLevelSublistState extends State<TwoLevelSublist> with SingleTickerProviderStateMixin {
AnimationController _controller; AnimationController _controller;
CurvedAnimation _easeOutAnimation; CurvedAnimation _easeOutAnimation;
...@@ -247,13 +256,8 @@ class _TwoLevelSublistState extends State<TwoLevelSublist> with SingleTickerProv ...@@ -247,13 +256,8 @@ class _TwoLevelSublistState extends State<TwoLevelSublist> with SingleTickerProv
} }
} }
/// A scrollable list of items that can expand and collapse. /// This class is deprecated. Please use [ListView] and [ListTileTheme] instead.
/// @deprecated
/// See also:
///
/// * [TwoLevelSublist]
/// * [TwoLevelListItem]
/// * [ListView], for lists that only have one level.
class TwoLevelList extends StatelessWidget { class TwoLevelList extends StatelessWidget {
/// Creates a scrollable list of items that can expand and collapse. /// Creates a scrollable list of items that can expand and collapse.
/// ///
......
...@@ -7,24 +7,6 @@ import 'package:flutter/widgets.dart'; ...@@ -7,24 +7,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() { void main() {
testWidgets('TwoLevelList default control', (WidgetTester tester) async {
await tester.pumpWidget(const Center(child: const TwoLevelList()));
await tester.pumpWidget(
new Material(
child: new Center(
child: new TwoLevelList(
children: <Widget>[
const TwoLevelSublist(
title: const Text('Title'),
)
]
)
)
)
);
});
testWidgets('TwoLevelList basics', (WidgetTester tester) async { testWidgets('TwoLevelList basics', (WidgetTester tester) async {
final Key topKey = new UniqueKey(); final Key topKey = new UniqueKey();
final Key sublistKey = new UniqueKey(); final Key sublistKey = new UniqueKey();
...@@ -34,18 +16,18 @@ void main() { ...@@ -34,18 +16,18 @@ void main() {
'/': (_) { '/': (_) {
return new Material( return new Material(
child: new SingleChildScrollView( child: new SingleChildScrollView(
child: new TwoLevelList( child: new Column(
children: <Widget>[ children: <Widget>[
new TwoLevelListItem(title: const Text('Top'), key: topKey), new ListTile(title: const Text('Top'), key: topKey),
new TwoLevelSublist( new ExpansionTile(
key: sublistKey, key: sublistKey,
title: const Text('Sublist'), title: const Text('Sublist'),
children: <Widget>[ children: <Widget>[
const TwoLevelListItem(title: const Text('0')), const ListTile(title: const Text('0')),
const TwoLevelListItem(title: const Text('1')) const ListTile(title: const Text('1'))
] ]
), ),
new TwoLevelListItem(title: const Text('Bottom'), key: bottomKey) new ListTile(title: const Text('Bottom'), key: bottomKey)
] ]
) )
) )
...@@ -85,23 +67,23 @@ void main() { ...@@ -85,23 +67,23 @@ void main() {
expect(getY(bottomKey) - getY(sublistKey), greaterThan(getHeight(bottomKey))); expect(getY(bottomKey) - getY(sublistKey), greaterThan(getHeight(bottomKey)));
}); });
testWidgets('onOpenChanged callback', (WidgetTester tester) async { testWidgets('onExpansionChanged callback', (WidgetTester tester) async {
bool didChangeOpen; bool didChangeOpen;
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (_) { '/': (_) {
return new Material( return new Material(
child: new SingleChildScrollView( child: new SingleChildScrollView(
child: new TwoLevelList( child: new Column(
children: <Widget>[ children: <Widget>[
new TwoLevelSublist( new ExpansionTile(
title: const Text('Sublist'), title: const Text('Sublist'),
onOpenChanged: (bool opened) { onExpansionChanged: (bool opened) {
didChangeOpen = opened; didChangeOpen = opened;
}, },
children: <Widget>[ children: <Widget>[
const TwoLevelListItem(title: const Text('0')), const ListTile(title: const Text('0')),
const TwoLevelListItem(title: const Text('1')) const ListTile(title: const Text('1'))
] ]
), ),
] ]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment