Skip to content

Commit e0f89e7

Browse files
authored
Fix out-of-sync ExpansionPanel animation (#105024)
* Increase minimum height of headerWidget in ExpansionPanel to smooth the animation. Signed-off-by: Morris Kurz <[email protected]> * Add regression tests that check for equal height of header elements in ExpansionPanel. Signed-off-by: Morris Kurz <[email protected]> * Clarify comment. Signed-off-by: Morris Kurz <[email protected]> * Reduce padding in ExpandIcon to 12px s.t. header height is 48px. Signed-off-by: Morris Kurz <[email protected]> * Update testcases to new header height (56px -> 48px). Signed-off-by: Morris Kurz <[email protected]> * Test for header height equal to 48px. Signed-off-by: Morris Kurz <[email protected]> * Change issue number to link in comment * Add periods to comments Signed-off-by: Morris Kurz <[email protected]>
1 parent 6d7c507 commit e0f89e7

File tree

2 files changed

+74
-24
lines changed

2 files changed

+74
-24
lines changed

packages/flutter/lib/src/material/expansion_panel.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
1616
const EdgeInsets _kPanelHeaderExpandedDefaultPadding = EdgeInsets.symmetric(
1717
vertical: 64.0 - _kPanelHeaderCollapsedHeight,
1818
);
19+
const EdgeInsets _kExpandIconPadding = EdgeInsets.all(12.0);
1920

2021
class _SaltedKey<S, V> extends LocalKey {
2122
const _SaltedKey(this.salt, this.value);
@@ -363,7 +364,7 @@ class _ExpansionPanelListState extends State<ExpansionPanelList> {
363364
child: ExpandIcon(
364365
color: widget.expandIconColor,
365366
isExpanded: _isChildExpanded(index),
366-
padding: const EdgeInsets.all(16.0),
367+
padding: _kExpandIconPadding,
367368
onPressed: !child.canTapOnHeader
368369
? (bool isExpanded) => _handlePressed(isExpanded, index)
369370
: null,

packages/flutter/test/material/expansion_panel_test.dart

+72-23
Original file line numberDiff line numberDiff line change
@@ -310,52 +310,52 @@ void main() {
310310

311311
await tester.pumpWidget(build(false, false, false));
312312
expect(tester.renderObjectList(find.byType(AnimatedSize)), hasLength(3));
313-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
314-
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 113.0, 800.0, 0.0));
315-
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 170.0, 800.0, 0.0));
313+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
314+
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 97.0, 800.0, 0.0));
315+
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 146.0, 800.0, 0.0));
316316

317317
await tester.pump(const Duration(milliseconds: 200));
318-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
319-
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 113.0, 800.0, 0.0));
320-
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 170.0, 800.0, 0.0));
318+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
319+
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 97.0, 800.0, 0.0));
320+
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 146.0, 800.0, 0.0));
321321

322322
await tester.pumpWidget(build(false, true, false));
323-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
324-
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 113.0, 800.0, 0.0));
325-
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 170.0, 800.0, 0.0));
323+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
324+
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 97.0, 800.0, 0.0));
325+
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 146.0, 800.0, 0.0));
326326

327327
await tester.pump(kSizeAnimationDuration ~/ 2);
328-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
328+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
329329
final Rect rect1 = tester.getRect(find.byType(AnimatedSize).at(1));
330330
expect(rect1.left, 0.0);
331-
expect(rect1.top, inExclusiveRange(113.0, 113.0 + 16.0 + 32.0)); // 16.0 material gap, plus 16.0 top and bottom margins added to the header
331+
expect(rect1.top, inExclusiveRange(113.0, 113.0 + 16.0 + 24.0)); // 16.0 material gap, plus 12.0 top and bottom margins added to the header
332332
expect(rect1.width, 800.0);
333333
expect(rect1.height, inExclusiveRange(0.0, 100.0));
334334
final Rect rect2 = tester.getRect(find.byType(AnimatedSize).at(2));
335-
expect(rect2, Rect.fromLTWH(0.0, rect1.bottom + 16.0 + 56.0, 800.0, 0.0)); // the 16.0 comes from the MaterialGap being introduced, the 56.0 is the header height.
335+
expect(rect2, Rect.fromLTWH(0.0, rect1.bottom + 16.0 + 48.0, 800.0, 0.0)); // the 16.0 comes from the MaterialGap being introduced, the 48.0 is the header height.
336336

337337
await tester.pumpWidget(build(false, false, false));
338-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
338+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
339339
expect(tester.getRect(find.byType(AnimatedSize).at(1)), rect1);
340340
expect(tester.getRect(find.byType(AnimatedSize).at(2)), rect2);
341341

342342
await tester.pumpWidget(build(false, false, true));
343-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
343+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
344344
expect(tester.getRect(find.byType(AnimatedSize).at(1)), rect1);
345345
expect(tester.getRect(find.byType(AnimatedSize).at(2)), rect2);
346346

347347
// a few no-op pumps to make sure there's nothing fishy going on
348348
await tester.pump();
349349
await tester.pump();
350350
await tester.pump();
351-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
351+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
352352
expect(tester.getRect(find.byType(AnimatedSize).at(1)), rect1);
353353
expect(tester.getRect(find.byType(AnimatedSize).at(2)), rect2);
354354

355355
await tester.pumpAndSettle();
356-
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
357-
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 56.0 + 1.0 + 56.0, 800.0, 0.0));
358-
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 56.0 + 1.0 + 56.0 + 16.0 + 16.0 + 48.0 + 16.0, 800.0, 100.0));
356+
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 48.0, 800.0, 0.0));
357+
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 48.0 + 1.0 + 48.0, 800.0, 0.0));
358+
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 48.0 + 1.0 + 48.0 + 16.0 + 16.0 + 48.0 + 16.0, 800.0, 100.0));
359359
});
360360

361361
testWidgets('Radio mode has max of one panel open at a time', (WidgetTester tester) async {
@@ -1286,7 +1286,7 @@ void main() {
12861286
// No padding applied to closed header
12871287
RenderBox box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
12881288
expect(box.size.height, equals(48.0)); // _kPanelHeaderCollapsedHeight
1289-
expect(box.size.width, equals(736.0));
1289+
expect(box.size.width, equals(744.0));
12901290

12911291
// Now, expand the child panel.
12921292
await tester.tap(find.byKey(firstPanelKey));
@@ -1298,8 +1298,57 @@ void main() {
12981298

12991299
// Padding is added to expanded header
13001300
box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
1301-
expect(box.size.height, equals(80.0)); // _kPanelHeaderCollapsedHeight + 32.0 (double default padding)
1302-
expect(box.size.width, equals(736.0));
1301+
expect(box.size.height, equals(80.0)); // _kPanelHeaderCollapsedHeight + 24.0 (double default padding)
1302+
expect(box.size.width, equals(744.0));
1303+
});
1304+
1305+
// Regression test for https://github.com/flutter/flutter/issues/5848.
1306+
testWidgets('The AnimatedContainer and IconButton have the same height of 48px', (WidgetTester tester) async {
1307+
const Key firstPanelKey = Key('firstPanelKey');
1308+
1309+
await tester.pumpWidget(
1310+
const MaterialApp(
1311+
home: SingleChildScrollView(
1312+
child: SimpleExpansionPanelListTestWidget(
1313+
firstPanelKey: firstPanelKey,
1314+
canTapOnHeader: true,
1315+
),
1316+
),
1317+
),
1318+
);
1319+
1320+
// The panel is closed.
1321+
expect(find.text('A'), findsOneWidget);
1322+
expect(find.text('B'), findsNothing);
1323+
1324+
// No padding applied to closed header.
1325+
final RenderBox boxOfContainer = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
1326+
final RenderBox boxOfIconButton = tester.renderObject(find.byType(IconButton).first);
1327+
expect(boxOfContainer.size.height, equals(boxOfIconButton.size.height));
1328+
expect(boxOfContainer.size.height, equals(48.0)); // Header should have 48px height according to Material 2 Design spec.
1329+
});
1330+
1331+
testWidgets("The AnimatedContainer's height is at least kMinInteractiveDimension", (WidgetTester tester) async {
1332+
const Key firstPanelKey = Key('firstPanelKey');
1333+
1334+
await tester.pumpWidget(
1335+
const MaterialApp(
1336+
home: SingleChildScrollView(
1337+
child: SimpleExpansionPanelListTestWidget(
1338+
firstPanelKey: firstPanelKey,
1339+
canTapOnHeader: true,
1340+
),
1341+
),
1342+
),
1343+
);
1344+
1345+
// The panel is closed
1346+
expect(find.text('A'), findsOneWidget);
1347+
expect(find.text('B'), findsNothing);
1348+
1349+
// No padding applied to closed header
1350+
final RenderBox box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
1351+
expect(box.size.height, greaterThanOrEqualTo(kMinInteractiveDimension));
13031352
});
13041353

13051354
testWidgets('Correct custom header padding', (WidgetTester tester) async {
@@ -1324,7 +1373,7 @@ void main() {
13241373
// No padding applied to closed header
13251374
RenderBox box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
13261375
expect(box.size.height, equals(48.0)); // _kPanelHeaderCollapsedHeight
1327-
expect(box.size.width, equals(736.0));
1376+
expect(box.size.width, equals(744.0));
13281377

13291378
// Now, expand the child panel.
13301379
await tester.tap(find.byKey(firstPanelKey));
@@ -1337,7 +1386,7 @@ void main() {
13371386
// Padding is added to expanded header
13381387
box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
13391388
expect(box.size.height, equals(128.0)); // _kPanelHeaderCollapsedHeight + 80.0 (double padding)
1340-
expect(box.size.width, equals(736.0));
1389+
expect(box.size.width, equals(744.0));
13411390
});
13421391

13431392
testWidgets('ExpansionPanelList respects dividerColor', (WidgetTester tester) async {

0 commit comments

Comments
 (0)