Add ExpansionTile.controlAffinity (#80360)
This commit is contained in:
parent
e467018d06
commit
ae9766655d
@ -12,7 +12,7 @@ import 'theme.dart';
|
|||||||
|
|
||||||
const Duration _kExpand = Duration(milliseconds: 200);
|
const Duration _kExpand = Duration(milliseconds: 200);
|
||||||
|
|
||||||
/// A single-line [ListTile] with a trailing button that expands or collapses
|
/// A single-line [ListTile] with an expansion arrow icon that expands or collapses
|
||||||
/// the tile to reveal or hide the [children].
|
/// the tile to reveal or hide the [children].
|
||||||
///
|
///
|
||||||
/// This widget is typically used with [ListView] to create an
|
/// This widget is typically used with [ListView] to create an
|
||||||
@ -26,6 +26,57 @@ const Duration _kExpand = Duration(milliseconds: 200);
|
|||||||
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
|
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
|
||||||
/// between [textColor] and [collapsedTextColor].
|
/// between [textColor] and [collapsedTextColor].
|
||||||
///
|
///
|
||||||
|
/// The expansion arrow icon is shown on the right by default in left-to-right languages
|
||||||
|
/// (i.e. the trailing edge). This can be changed using [controlAffinity]. This maps
|
||||||
|
/// to the [leading] and [trailing] properties of [ExpansionTile].
|
||||||
|
///
|
||||||
|
/// {@tool dartpad --template=stateful_widget_scaffold}
|
||||||
|
///
|
||||||
|
/// This example demonstrates different configurations of ExpansionTile.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// bool _customTileExpanded = false;
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return Column(
|
||||||
|
/// children: <Widget>[
|
||||||
|
/// const ExpansionTile(
|
||||||
|
/// title: Text('ExpansionTile 1'),
|
||||||
|
/// subtitle: Text('Trailing expansion arrow icon'),
|
||||||
|
/// children: <Widget>[
|
||||||
|
/// ListTile(title: Text('This is tile number 1')),
|
||||||
|
/// ],
|
||||||
|
/// ),
|
||||||
|
/// ExpansionTile(
|
||||||
|
/// title: const Text('ExpansionTile 2'),
|
||||||
|
/// subtitle: const Text('Custom expansion arrow icon'),
|
||||||
|
/// trailing: Icon(
|
||||||
|
/// _customTileExpanded
|
||||||
|
/// ? Icons.arrow_drop_down_circle
|
||||||
|
/// : Icons.arrow_drop_down,
|
||||||
|
/// ),
|
||||||
|
/// children: const <Widget>[
|
||||||
|
/// ListTile(title: Text('This is tile number 2')),
|
||||||
|
/// ],
|
||||||
|
/// onExpansionChanged: (bool expanded) {
|
||||||
|
/// setState(() => _customTileExpanded = expanded);
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
/// const ExpansionTile(
|
||||||
|
/// title: Text('ExpansionTile 3'),
|
||||||
|
/// subtitle: Text('Leading expansion arrow icon'),
|
||||||
|
/// controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
/// children: <Widget>[
|
||||||
|
/// ListTile(title: Text('This is tile number 3')),
|
||||||
|
/// ],
|
||||||
|
/// ),
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [ListTile], useful for creating expansion tile [children] when the
|
/// * [ListTile], useful for creating expansion tile [children] when the
|
||||||
@ -33,7 +84,7 @@ const Duration _kExpand = Duration(milliseconds: 200);
|
|||||||
/// * The "Expand and collapse" section of
|
/// * The "Expand and collapse" section of
|
||||||
/// <https://material.io/components/lists#types>
|
/// <https://material.io/components/lists#types>
|
||||||
class ExpansionTile extends StatefulWidget {
|
class ExpansionTile extends StatefulWidget {
|
||||||
/// Creates a single-line [ListTile] with a trailing button that expands or collapses
|
/// Creates a single-line [ListTile] with an expansion arrow icon that expands or collapses
|
||||||
/// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
|
/// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
|
||||||
/// be non-null.
|
/// be non-null.
|
||||||
const ExpansionTile({
|
const ExpansionTile({
|
||||||
@ -56,6 +107,7 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
this.collapsedTextColor,
|
this.collapsedTextColor,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
this.collapsedIconColor,
|
this.collapsedIconColor,
|
||||||
|
this.controlAffinity,
|
||||||
}) : assert(initiallyExpanded != null),
|
}) : assert(initiallyExpanded != null),
|
||||||
assert(maintainState != null),
|
assert(maintainState != null),
|
||||||
assert(
|
assert(
|
||||||
@ -68,6 +120,9 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
/// A widget to display before the title.
|
/// A widget to display before the title.
|
||||||
///
|
///
|
||||||
/// Typically a [CircleAvatar] widget.
|
/// Typically a [CircleAvatar] widget.
|
||||||
|
///
|
||||||
|
/// Note that depending on the value of [controlAffinity], the [leading] widget
|
||||||
|
/// may replace the rotating expansion arrow icon.
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
|
|
||||||
/// The primary content of the list item.
|
/// The primary content of the list item.
|
||||||
@ -98,7 +153,10 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
/// When not null, defines the background color of tile when the sublist is collapsed.
|
/// When not null, defines the background color of tile when the sublist is collapsed.
|
||||||
final Color? collapsedBackgroundColor;
|
final Color? collapsedBackgroundColor;
|
||||||
|
|
||||||
/// A widget to display instead of a rotating arrow icon.
|
/// A widget to display after the title.
|
||||||
|
///
|
||||||
|
/// Note that depending on the value of [controlAffinity], the [trailing] widget
|
||||||
|
/// may replace the rotating expansion arrow icon.
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
|
|
||||||
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
|
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
|
||||||
@ -157,14 +215,12 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
/// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero].
|
/// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero].
|
||||||
final EdgeInsetsGeometry? childrenPadding;
|
final EdgeInsetsGeometry? childrenPadding;
|
||||||
|
|
||||||
/// The icon color of tile's [trailing] expansion icon when the
|
/// The icon color of tile's expansion arrow icon when the sublist is expanded.
|
||||||
/// sublist is expanded.
|
|
||||||
///
|
///
|
||||||
/// Used to override to the [ListTileTheme.iconColor].
|
/// Used to override to the [ListTileTheme.iconColor].
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
|
|
||||||
/// The icon color of tile's [trailing] expansion icon when the
|
/// The icon color of tile's expansion arrow icon when the sublist is collapsed.
|
||||||
/// sublist is collapsed.
|
|
||||||
///
|
///
|
||||||
/// Used to override to the [ListTileTheme.iconColor].
|
/// Used to override to the [ListTileTheme.iconColor].
|
||||||
final Color? collapsedIconColor;
|
final Color? collapsedIconColor;
|
||||||
@ -180,6 +236,12 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
/// Used to override to the [ListTileTheme.textColor].
|
/// Used to override to the [ListTileTheme.textColor].
|
||||||
final Color? collapsedTextColor;
|
final Color? collapsedTextColor;
|
||||||
|
|
||||||
|
/// Typically used to force the expansion arrow icon to the tile's leading or trailing edge.
|
||||||
|
///
|
||||||
|
/// By default, the value of `controlAffinity` is [ListTileControlAffinity.platform],
|
||||||
|
/// which means that the expansion arrow icon will appear on the tile's trailing edge.
|
||||||
|
final ListTileControlAffinity? controlAffinity;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ExpansionTile> createState() => _ExpansionTileState();
|
State<ExpansionTile> createState() => _ExpansionTileState();
|
||||||
}
|
}
|
||||||
@ -245,6 +307,36 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
widget.onExpansionChanged?.call(_isExpanded);
|
widget.onExpansionChanged?.call(_isExpanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform or null affinity defaults to trailing.
|
||||||
|
ListTileControlAffinity _effectiveAffinity(ListTileControlAffinity? affinity) {
|
||||||
|
switch (affinity ?? ListTileControlAffinity.trailing) {
|
||||||
|
case ListTileControlAffinity.leading:
|
||||||
|
return ListTileControlAffinity.leading;
|
||||||
|
case ListTileControlAffinity.trailing:
|
||||||
|
case ListTileControlAffinity.platform:
|
||||||
|
return ListTileControlAffinity.trailing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _buildIcon(BuildContext context) {
|
||||||
|
return RotationTransition(
|
||||||
|
turns: _iconTurns,
|
||||||
|
child: const Icon(Icons.expand_more),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _buildLeadingIcon(BuildContext context) {
|
||||||
|
if (_effectiveAffinity(widget.controlAffinity) != ListTileControlAffinity.leading)
|
||||||
|
return null;
|
||||||
|
return _buildIcon(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _buildTrailingIcon(BuildContext context) {
|
||||||
|
if (_effectiveAffinity(widget.controlAffinity) != ListTileControlAffinity.trailing)
|
||||||
|
return null;
|
||||||
|
return _buildIcon(context);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildChildren(BuildContext context, Widget? child) {
|
Widget _buildChildren(BuildContext context, Widget? child) {
|
||||||
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
|
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
|
||||||
|
|
||||||
@ -265,13 +357,10 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
onTap: _handleTap,
|
onTap: _handleTap,
|
||||||
contentPadding: widget.tilePadding,
|
contentPadding: widget.tilePadding,
|
||||||
leading: widget.leading,
|
leading: widget.leading ?? _buildLeadingIcon(context),
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
subtitle: widget.subtitle,
|
subtitle: widget.subtitle,
|
||||||
trailing: widget.trailing ?? RotationTransition(
|
trailing: widget.trailing ?? _buildTrailingIcon(context),
|
||||||
turns: _iconTurns,
|
|
||||||
child: const Icon(Icons.expand_more),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ClipRect(
|
ClipRect(
|
||||||
|
@ -221,6 +221,8 @@ class ListTileTheme extends InheritedTheme {
|
|||||||
/// * [CheckboxListTile], which combines a [ListTile] with a [Checkbox].
|
/// * [CheckboxListTile], which combines a [ListTile] with a [Checkbox].
|
||||||
/// * [RadioListTile], which combines a [ListTile] with a [Radio] button.
|
/// * [RadioListTile], which combines a [ListTile] with a [Radio] button.
|
||||||
/// * [SwitchListTile], which combines a [ListTile] with a [Switch].
|
/// * [SwitchListTile], which combines a [ListTile] with a [Switch].
|
||||||
|
/// * [ExpansionTile], which combines a [ListTile] with a button that expands
|
||||||
|
/// or collapses the tile to reveal or hide the children.
|
||||||
enum ListTileControlAffinity {
|
enum ListTileControlAffinity {
|
||||||
/// Position the control on the leading edge, and the secondary widget, if
|
/// Position the control on the leading edge, and the secondary widget, if
|
||||||
/// any, on the trailing edge.
|
/// any, on the trailing edge.
|
||||||
|
@ -560,4 +560,64 @@ void main() {
|
|||||||
expect(getIconColor(), iconColor);
|
expect(getIconColor(), iconColor);
|
||||||
expect(getTextColor(), textColor);
|
expect(getTextColor(), textColor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('ExpansionTile platform controlAffinity test', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
title: Text('Title'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ListTile listTile = tester.widget(find.byType(ListTile));
|
||||||
|
expect(listTile.leading, isNull);
|
||||||
|
expect(listTile.trailing.runtimeType, RotationTransition);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ExpansionTile trailing controlAffinity test', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
title: Text('Title'),
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ListTile listTile = tester.widget(find.byType(ListTile));
|
||||||
|
expect(listTile.leading, isNull);
|
||||||
|
expect(listTile.trailing.runtimeType, RotationTransition);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ExpansionTile leading controlAffinity test', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
title: Text('Title'),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ListTile listTile = tester.widget(find.byType(ListTile));
|
||||||
|
expect(listTile.leading.runtimeType, RotationTransition);
|
||||||
|
expect(listTile.trailing, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ExpansionTile override rotating icon test', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
title: Text('Title'),
|
||||||
|
leading: Icon(Icons.info),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ListTile listTile = tester.widget(find.byType(ListTile));
|
||||||
|
expect(listTile.leading.runtimeType, Icon);
|
||||||
|
expect(listTile.trailing, isNull);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user