parent
f7fb14ec58
commit
59c9d4e845
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// Flutter code sample for [ExpansionTile] and [ExpansionTileController]
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const ExpansionTileControllerApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpansionTileControllerApp extends StatefulWidget {
|
||||||
|
const ExpansionTileControllerApp({ super.key });
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExpansionTileControllerApp> createState() => _ExpansionTileControllerAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpansionTileControllerAppState extends State<ExpansionTileControllerApp> {
|
||||||
|
final ExpansionTileController controller = ExpansionTileController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Flutter Code Sample for ExpansionTileController.',
|
||||||
|
theme: ThemeData(useMaterial3: true),
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('ExpansionTileController Example')),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// A controller has been provided to the ExpansionTile because it's
|
||||||
|
// going to be accessed from a component that is not within the
|
||||||
|
// tile's BuildContext.
|
||||||
|
ExpansionTile(
|
||||||
|
controller: controller,
|
||||||
|
title: const Text('ExpansionTile with explicit controller.'),
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: const Text('ExpansionTile Contents'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Expand/Collapse the Tile Above'),
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.isExpanded) {
|
||||||
|
controller.collapse();
|
||||||
|
} else {
|
||||||
|
controller.expand();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
// A controller has not been provided to the ExpansionTile because
|
||||||
|
// the automatically created one can be retrieved via the tile's BuildContext.
|
||||||
|
ExpansionTile(
|
||||||
|
title: const Text('ExpansionTile with implicit controller.'),
|
||||||
|
children: <Widget>[
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: const Text('Collapse This Tile'),
|
||||||
|
onPressed: () {
|
||||||
|
return ExpansionTileController.of(context).collapse();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2014 The Flutter 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_api_samples/material/expansion_tile/expansion_tile.1.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Test the basics of ExpansionTileControllerApp', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.ExpansionTileControllerApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('ExpansionTile Contents'), findsNothing);
|
||||||
|
expect(find.text('Collapse This Tile'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Expand/Collapse the Tile Above'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('ExpansionTile Contents'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Expand/Collapse the Tile Above'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('ExpansionTile Contents'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('ExpansionTile with implicit controller.'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Collapse This Tile'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Collapse This Tile'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Collapse This Tile'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -15,6 +15,170 @@ import 'theme.dart';
|
|||||||
|
|
||||||
const Duration _kExpand = Duration(milliseconds: 200);
|
const Duration _kExpand = Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
/// Enables control over a single [ExpansionTile]'s expanded/collapsed state.
|
||||||
|
///
|
||||||
|
/// It can be useful to expand or collapse an [ExpansionTile]
|
||||||
|
/// programatically, for example to reconfigure an existing expansion
|
||||||
|
/// tile based on a system event. To do so, create an [ExpansionTile]
|
||||||
|
/// with an [ExpansionTileController] that's owned by a stateful widget
|
||||||
|
/// or look up the tile's automatically created [ExpansionTileController]
|
||||||
|
/// with [ExpansionTileController.of]
|
||||||
|
///
|
||||||
|
/// The controller's [expand] and [collapse] methods cause the
|
||||||
|
/// the [ExpansionTile] to rebuild, so they may not be called from
|
||||||
|
/// a build method.
|
||||||
|
class ExpansionTileController {
|
||||||
|
/// Create a controller to be used with [ExpansionTile.controller].
|
||||||
|
ExpansionTileController();
|
||||||
|
|
||||||
|
_ExpansionTileState? _state;
|
||||||
|
|
||||||
|
/// Whether the [ExpansionTile] built with this controller is in expanded state.
|
||||||
|
///
|
||||||
|
/// This property doesn't take the animation into account. It reports `true`
|
||||||
|
/// even if the expansion animation is not completed.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [expand], which expands the [ExpansionTile].
|
||||||
|
/// * [collapse], which collapses the [ExpansionTile].
|
||||||
|
/// * [ExpansionTile.controller] to create an ExpansionTile with a controller.
|
||||||
|
bool get isExpanded {
|
||||||
|
assert(_state != null);
|
||||||
|
return _state!._isExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands the [ExpansionTile] that was built with this controller;
|
||||||
|
///
|
||||||
|
/// Normally the tile is expanded automatically when the user taps on the header.
|
||||||
|
/// It is sometimes useful to trigger the expansion programmatically due
|
||||||
|
/// to external changes.
|
||||||
|
///
|
||||||
|
/// If the tile is already in the expanded state (see [isExpanded]), calling
|
||||||
|
/// this method has no effect.
|
||||||
|
///
|
||||||
|
/// Calling this method may cause the [ExpansionTile] to rebuild, so it may
|
||||||
|
/// not be called from a build method.
|
||||||
|
///
|
||||||
|
/// Calling this method will trigger an [ExpansionTile.onExpansionChanged] callback.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [collapse], which collapses the tile.
|
||||||
|
/// * [isExpanded] to check whether the tile is expanded.
|
||||||
|
/// * [ExpansionTile.controller] to create an ExpansionTile with a controller.
|
||||||
|
void expand() {
|
||||||
|
assert(_state != null);
|
||||||
|
if (!isExpanded) {
|
||||||
|
_state!._toggleExpansion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collapses the [ExpansionTile] that was built with this controller.
|
||||||
|
///
|
||||||
|
/// Normally the tile is collapsed automatically when the user taps on the header.
|
||||||
|
/// It can be useful sometimes to trigger the collapse programmatically due
|
||||||
|
/// to some external changes.
|
||||||
|
///
|
||||||
|
/// If the tile is already in the collapsed state (see [isExpanded]), calling
|
||||||
|
/// this method has no effect.
|
||||||
|
///
|
||||||
|
/// Calling this method may cause the [ExpansionTile] to rebuild, so it may
|
||||||
|
/// not be called from a build method.
|
||||||
|
///
|
||||||
|
/// Calling this method will trigger an [ExpansionTile.onExpansionChanged] callback.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [expand], which expands the tile.
|
||||||
|
/// * [isExpanded] to check whether the tile is expanded.
|
||||||
|
/// * [ExpansionTile.controller] to create an ExpansionTile with a controller.
|
||||||
|
void collapse() {
|
||||||
|
assert(_state != null);
|
||||||
|
if (isExpanded) {
|
||||||
|
_state!._toggleExpansion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the [ExpansionTileController] for the closest [ExpansionTile] instance
|
||||||
|
/// that encloses the given context.
|
||||||
|
///
|
||||||
|
/// If no [ExpansionTile] encloses the given context, calling this
|
||||||
|
/// method will cause an assert in debug mode, and throw an
|
||||||
|
/// exception in release mode.
|
||||||
|
///
|
||||||
|
/// To return null if there is no [ExpansionTile] use [maybeOf] instead.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// Typical usage of the [ExpansionTileController.of] function is to call it from within the
|
||||||
|
/// `build` method of a descendant of an [ExpansionTile].
|
||||||
|
///
|
||||||
|
/// When the [ExpansionTile] is actually created in the same `build`
|
||||||
|
/// function as the callback that refers to the controller, then the
|
||||||
|
/// `context` argument to the `build` function can't be used to find
|
||||||
|
/// the [ExpansionTileController] (since it's "above" the widget
|
||||||
|
/// being returned in the widget tree). In cases like that you can
|
||||||
|
/// add a [Builder] widget, which provides a new scope with a
|
||||||
|
/// [BuildContext] that is "under" the [ExpansionTile]:
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// A more efficient solution is to split your build function into
|
||||||
|
/// several widgets. This introduces a new context from which you
|
||||||
|
/// can obtain the [ExpansionTileController]. With this approach you
|
||||||
|
/// would have an outer widget that creates the [ExpansionTile]
|
||||||
|
/// populated by instances of your new inner widgets, and then in
|
||||||
|
/// these inner widgets you would use [ExpansionTileController.of].
|
||||||
|
static ExpansionTileController of(BuildContext context) {
|
||||||
|
final _ExpansionTileState? result = context.findAncestorStateOfType<_ExpansionTileState>();
|
||||||
|
if (result != null) {
|
||||||
|
return result._tileController;
|
||||||
|
}
|
||||||
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
|
ErrorSummary(
|
||||||
|
'ExpansionTileController.of() called with a context that does not contain a ExpansionTile.',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'No ExpansionTile ancestor could be found starting from the context that was passed to ExpansionTileController.of(). '
|
||||||
|
'This usually happens when the context provided is from the same StatefulWidget as that '
|
||||||
|
'whose build function actually creates the ExpansionTile widget being sought.',
|
||||||
|
),
|
||||||
|
ErrorHint(
|
||||||
|
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
|
||||||
|
'context that is "under" the ExpansionTile. For an example of this, please see the '
|
||||||
|
'documentation for ExpansionTileController.of():\n'
|
||||||
|
' https://api.flutter.dev/flutter/material/ExpansionTile/of.html',
|
||||||
|
),
|
||||||
|
ErrorHint(
|
||||||
|
'A more efficient solution is to split your build function into several widgets. This '
|
||||||
|
'introduces a new context from which you can obtain the ExpansionTile. In this solution, '
|
||||||
|
'you would have an outer widget that creates the ExpansionTile populated by instances of '
|
||||||
|
'your new inner widgets, and then in these inner widgets you would use ExpansionTileController.of().\n'
|
||||||
|
'An other solution is assign a GlobalKey to the ExpansionTile, '
|
||||||
|
'then use the key.currentState property to obtain the ExpansionTile rather than '
|
||||||
|
'using the ExpansionTileController.of() function.',
|
||||||
|
),
|
||||||
|
context.describeElement('The context used was'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the [ExpansionTile] from the closest instance of this class that
|
||||||
|
/// encloses the given context and returns its [ExpansionTileController].
|
||||||
|
///
|
||||||
|
/// If no [ExpansionTile] encloses the given context then return null.
|
||||||
|
/// To throw an exception instead, use [of] instead of this function.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [of], a similar function to this one that throws if no [ExpansionTile]
|
||||||
|
/// encloses the given context. Also includes some sample code in its
|
||||||
|
/// documentation.
|
||||||
|
static ExpansionTileController? maybeOf(BuildContext context) {
|
||||||
|
return context.findAncestorStateOfType<_ExpansionTileState>()?._tileController;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A single-line [ListTile] with an expansion arrow icon 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].
|
||||||
///
|
///
|
||||||
@ -40,6 +204,13 @@ const Duration _kExpand = Duration(milliseconds: 200);
|
|||||||
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
|
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
///
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example demonstrates how an [ExpansionTileController] can be used to
|
||||||
|
/// programatically expand or collapse an [ExpansionTile].
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [ListTile], useful for creating expansion tile [children] when the
|
/// * [ListTile], useful for creating expansion tile [children] when the
|
||||||
@ -74,6 +245,7 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
this.collapsedShape,
|
this.collapsedShape,
|
||||||
this.clipBehavior,
|
this.clipBehavior,
|
||||||
this.controlAffinity,
|
this.controlAffinity,
|
||||||
|
this.controller,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
|
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
|
||||||
'CrossAxisAlignment.baseline is not supported since the expanded children '
|
'CrossAxisAlignment.baseline is not supported since the expanded children '
|
||||||
@ -310,6 +482,13 @@ class ExpansionTile extends StatefulWidget {
|
|||||||
/// which means that the expansion arrow icon will appear on the tile's trailing edge.
|
/// which means that the expansion arrow icon will appear on the tile's trailing edge.
|
||||||
final ListTileControlAffinity? controlAffinity;
|
final ListTileControlAffinity? controlAffinity;
|
||||||
|
|
||||||
|
/// If provided, the controller can be used to expand and collapse tiles.
|
||||||
|
///
|
||||||
|
/// In cases were control over the tile's state is needed from a callback triggered
|
||||||
|
/// by a widget within the tile, [ExpansionTileController.of] may be more convenient
|
||||||
|
/// than supplying a controller.
|
||||||
|
final ExpansionTileController? controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ExpansionTile> createState() => _ExpansionTileState();
|
State<ExpansionTile> createState() => _ExpansionTileState();
|
||||||
}
|
}
|
||||||
@ -324,7 +503,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
final ColorTween _iconColorTween = ColorTween();
|
final ColorTween _iconColorTween = ColorTween();
|
||||||
final ColorTween _backgroundColorTween = ColorTween();
|
final ColorTween _backgroundColorTween = ColorTween();
|
||||||
|
|
||||||
late AnimationController _controller;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _iconTurns;
|
late Animation<double> _iconTurns;
|
||||||
late Animation<double> _heightFactor;
|
late Animation<double> _heightFactor;
|
||||||
late Animation<ShapeBorder?> _border;
|
late Animation<ShapeBorder?> _border;
|
||||||
@ -333,37 +512,43 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
late Animation<Color?> _backgroundColor;
|
late Animation<Color?> _backgroundColor;
|
||||||
|
|
||||||
bool _isExpanded = false;
|
bool _isExpanded = false;
|
||||||
|
late ExpansionTileController _tileController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = AnimationController(duration: _kExpand, vsync: this);
|
_animationController = AnimationController(duration: _kExpand, vsync: this);
|
||||||
_heightFactor = _controller.drive(_easeInTween);
|
_heightFactor = _animationController.drive(_easeInTween);
|
||||||
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
|
_iconTurns = _animationController.drive(_halfTween.chain(_easeInTween));
|
||||||
_border = _controller.drive(_borderTween.chain(_easeOutTween));
|
_border = _animationController.drive(_borderTween.chain(_easeOutTween));
|
||||||
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
|
_headerColor = _animationController.drive(_headerColorTween.chain(_easeInTween));
|
||||||
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
|
_iconColor = _animationController.drive(_iconColorTween.chain(_easeInTween));
|
||||||
_backgroundColor = _controller.drive(_backgroundColorTween.chain(_easeOutTween));
|
_backgroundColor = _animationController.drive(_backgroundColorTween.chain(_easeOutTween));
|
||||||
|
|
||||||
_isExpanded = PageStorage.maybeOf(context)?.readState(context) as bool? ?? widget.initiallyExpanded;
|
_isExpanded = PageStorage.maybeOf(context)?.readState(context) as bool? ?? widget.initiallyExpanded;
|
||||||
if (_isExpanded) {
|
if (_isExpanded) {
|
||||||
_controller.value = 1.0;
|
_animationController.value = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(widget.controller?._state == null);
|
||||||
|
_tileController = widget.controller ?? ExpansionTileController();
|
||||||
|
_tileController._state = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_tileController._state = null;
|
||||||
|
_animationController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTap() {
|
void _toggleExpansion() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isExpanded = !_isExpanded;
|
_isExpanded = !_isExpanded;
|
||||||
if (_isExpanded) {
|
if (_isExpanded) {
|
||||||
_controller.forward();
|
_animationController.forward();
|
||||||
} else {
|
} else {
|
||||||
_controller.reverse().then<void>((void value) {
|
_animationController.reverse().then<void>((void value) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -377,6 +562,10 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
widget.onExpansionChanged?.call(_isExpanded);
|
widget.onExpansionChanged?.call(_isExpanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTap() {
|
||||||
|
_toggleExpansion();
|
||||||
|
}
|
||||||
|
|
||||||
// Platform or null affinity defaults to trailing.
|
// Platform or null affinity defaults to trailing.
|
||||||
ListTileControlAffinity _effectiveAffinity(ListTileControlAffinity? affinity) {
|
ListTileControlAffinity _effectiveAffinity(ListTileControlAffinity? affinity) {
|
||||||
switch (affinity ?? ListTileControlAffinity.trailing) {
|
switch (affinity ?? ListTileControlAffinity.trailing) {
|
||||||
@ -491,7 +680,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
|
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
|
||||||
final bool closed = !_isExpanded && _controller.isDismissed;
|
final bool closed = !_isExpanded && _animationController.isDismissed;
|
||||||
final bool shouldRemoveChildren = closed && !widget.maintainState;
|
final bool shouldRemoveChildren = closed && !widget.maintainState;
|
||||||
|
|
||||||
final Widget result = Offstage(
|
final Widget result = Offstage(
|
||||||
@ -509,7 +698,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||||||
);
|
);
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _controller.view,
|
animation: _animationController.view,
|
||||||
builder: _buildChildren,
|
builder: _buildChildren,
|
||||||
child: shouldRemoveChildren ? null : result,
|
child: shouldRemoveChildren ? null : result,
|
||||||
);
|
);
|
||||||
|
@ -729,4 +729,119 @@ void main() {
|
|||||||
expect(getTextColor(), theme.colorScheme.primary);
|
expect(getTextColor(), theme.colorScheme.primary);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('ExpansionTileController isExpanded, expand() and collapse()', (WidgetTester tester) async {
|
||||||
|
final ExpansionTileController controller = ExpansionTileController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
controller: controller,
|
||||||
|
title: const Text('Title'),
|
||||||
|
children: const <Widget>[
|
||||||
|
Text('Child 0'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(find.text('Child 0'), findsNothing);
|
||||||
|
expect(controller.isExpanded, isFalse);
|
||||||
|
controller.expand();
|
||||||
|
expect(controller.isExpanded, isTrue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Child 0'), findsOneWidget);
|
||||||
|
expect(controller.isExpanded, isTrue);
|
||||||
|
controller.collapse();
|
||||||
|
expect(controller.isExpanded, isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Child 0'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Calling ExpansionTileController.expand/collapsed has no effect if it is already expanded/collapsed', (WidgetTester tester) async {
|
||||||
|
final ExpansionTileController controller = ExpansionTileController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
controller: controller,
|
||||||
|
title: const Text('Title'),
|
||||||
|
initiallyExpanded: true,
|
||||||
|
children: const <Widget>[
|
||||||
|
Text('Child 0'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(find.text('Child 0'), findsOneWidget);
|
||||||
|
expect(controller.isExpanded, isTrue);
|
||||||
|
controller.expand();
|
||||||
|
expect(controller.isExpanded, isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.hasRunningAnimations, isFalse);
|
||||||
|
expect(find.text('Child 0'), findsOneWidget);
|
||||||
|
controller.collapse();
|
||||||
|
expect(controller.isExpanded, isFalse);
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.hasRunningAnimations, isTrue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(controller.isExpanded, isFalse);
|
||||||
|
expect(find.text('Child 0'), findsNothing);
|
||||||
|
controller.collapse();
|
||||||
|
expect(controller.isExpanded, isFalse);
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.hasRunningAnimations, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Call to ExpansionTileController.of()', (WidgetTester tester) async {
|
||||||
|
final GlobalKey titleKey = GlobalKey();
|
||||||
|
final GlobalKey childKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: ExpansionTile(
|
||||||
|
initiallyExpanded: true,
|
||||||
|
title: Text('Title', key: titleKey),
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Child 0', key: childKey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ExpansionTileController controller1 = ExpansionTileController.of(childKey.currentContext!);
|
||||||
|
expect(controller1.isExpanded, isTrue);
|
||||||
|
|
||||||
|
final ExpansionTileController controller2 = ExpansionTileController.of(titleKey.currentContext!);
|
||||||
|
expect(controller2.isExpanded, isTrue);
|
||||||
|
|
||||||
|
expect(controller1, controller2);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Call to ExpansionTile.maybeOf()', (WidgetTester tester) async {
|
||||||
|
final GlobalKey titleKey = GlobalKey();
|
||||||
|
final GlobalKey nonDescendantKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
ExpansionTile(
|
||||||
|
title: Text('Title', key: titleKey),
|
||||||
|
children: const <Widget>[
|
||||||
|
Text('Child 0'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text('Non descendant', key: nonDescendantKey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final ExpansionTileController? controller1 = ExpansionTileController.maybeOf(titleKey.currentContext!);
|
||||||
|
expect(controller1, isNotNull);
|
||||||
|
expect(controller1?.isExpanded, isFalse);
|
||||||
|
|
||||||
|
final ExpansionTileController? controller2 = ExpansionTileController.maybeOf(nonDescendantKey.currentContext!);
|
||||||
|
expect(controller2, isNull);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user