mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Persistent CupertinoListTile leading and trailing (#166799)
The bug occurs because the background color passed into the underlying `Container` changes from null to `backgroundColorActivated`. Under the hood, the `Container` wraps its child in a `ColoredBox` only if the color is non-null. So the extra wrapping with a `ColoredBox` happening mid-animation breaks reparenting, causing the widgets to be replaced/inflated instead of updated. ### Before https://github.com/user-attachments/assets/ca0b657a-1340-405f-8c1d-34b34366b994 ### After https://github.com/user-attachments/assets/8445c55c-0d5d-4b5f-96d2-4f12d908bdec Fixes [CupertinoListTile animations are not running when pressing longer](https://github.com/flutter/flutter/issues/153225) <details> <summary>Sample code</summary> ```dart import 'package:flutter/cupertino.dart'; void main() => runApp(const ListTileApp()); class ListTileApp extends StatelessWidget { const ListTileApp({super.key}); @override Widget build(BuildContext context) { return CupertinoApp(home: const ListTileExample()); } } class ListTileExample extends StatefulWidget { const ListTileExample({super.key}); @override State<ListTileExample> createState() => _ListTileExampleState(); } class _ListTileExampleState extends State<ListTileExample> { bool _pushedToggle = false; void _toggle() { setState(() { _pushedToggle = !_pushedToggle; }); } @override Widget build(BuildContext context) { return CupertinoPageScaffold( child: Center( child: SizedBox( height: 40, child: CupertinoListTile( onTap: _toggle, title: Center( child: Text( 'Toggle', ), ), leading: CupertinoSwitch( value: _pushedToggle, onChanged: (_) {}, ), trailing: CupertinoSwitch( value: _pushedToggle, onChanged: (_) {}, )), ), ), ); } } ``` </details>
This commit is contained in:
parent
ca758ac49b
commit
77c42fbd22
@ -308,7 +308,7 @@ class _CupertinoListTileState extends State<CupertinoListTile> {
|
||||
// null and it will resolve to the correct color provided by context. But if
|
||||
// the tile was tapped, it is set to what user provided or if null to the
|
||||
// default color that matched the iOS-style.
|
||||
Color? backgroundColor = widget.backgroundColor;
|
||||
Color backgroundColor = widget.backgroundColor ?? CupertinoColors.transparent;
|
||||
if (_tapped) {
|
||||
backgroundColor =
|
||||
widget.backgroundColorActivated ?? CupertinoColors.systemGrey4.resolveFrom(context);
|
||||
@ -321,44 +321,46 @@ class _CupertinoListTileState extends State<CupertinoListTile> {
|
||||
_CupertinoListTileType.notched => _kNotchedMinHeightWithoutLeading,
|
||||
};
|
||||
|
||||
final Widget child = Container(
|
||||
final Widget child = ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: double.infinity, minHeight: minHeight),
|
||||
color: backgroundColor,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (widget.leading case final Widget leading) ...<Widget>[
|
||||
SizedBox.square(dimension: widget.leadingSize, child: Center(child: leading)),
|
||||
SizedBox(width: widget.leadingToTitle),
|
||||
] else
|
||||
SizedBox(height: widget.leadingSize),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
title,
|
||||
if (widget.subtitle case final Widget subtitle) ...<Widget>[
|
||||
const SizedBox(height: _kNotchedTitleToSubtitle),
|
||||
DefaultTextStyle(
|
||||
style: coloredStyle.copyWith(
|
||||
fontSize: baseType ? _kSubtitleFontSize : _kNotchedSubtitleFontSize,
|
||||
child: ColoredBox(
|
||||
color: backgroundColor,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (widget.leading case final Widget leading) ...<Widget>[
|
||||
SizedBox.square(dimension: widget.leadingSize, child: Center(child: leading)),
|
||||
SizedBox(width: widget.leadingToTitle),
|
||||
] else
|
||||
SizedBox(height: widget.leadingSize),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
title,
|
||||
if (widget.subtitle case final Widget subtitle) ...<Widget>[
|
||||
const SizedBox(height: _kNotchedTitleToSubtitle),
|
||||
DefaultTextStyle(
|
||||
style: coloredStyle.copyWith(
|
||||
fontSize: baseType ? _kSubtitleFontSize : _kNotchedSubtitleFontSize,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: subtitle,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: subtitle,
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.additionalInfo case final Widget additionalInfo) ...<Widget>[
|
||||
DefaultTextStyle(style: coloredStyle, maxLines: 1, child: additionalInfo),
|
||||
if (widget.trailing != null) const SizedBox(width: _kAdditionalInfoToTrailing),
|
||||
if (widget.additionalInfo case final Widget additionalInfo) ...<Widget>[
|
||||
DefaultTextStyle(style: coloredStyle, maxLines: 1, child: additionalInfo),
|
||||
if (widget.trailing != null) const SizedBox(width: _kAdditionalInfoToTrailing),
|
||||
],
|
||||
if (widget.trailing != null) widget.trailing!,
|
||||
],
|
||||
if (widget.trailing != null) widget.trailing!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -89,9 +89,10 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
final Container container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColor);
|
||||
final ColoredBox coloredBox = tester.widget<ColoredBox>(
|
||||
find.descendant(of: find.byType(CupertinoListTile), matching: find.byType(ColoredBox)),
|
||||
);
|
||||
expect(coloredBox.color, backgroundColor);
|
||||
});
|
||||
|
||||
testWidgets('does not change backgroundColor when tapped if onTap is not provided', (
|
||||
@ -121,9 +122,10 @@ void main() {
|
||||
await tester.tap(find.byType(CupertinoListTile));
|
||||
await tester.pump();
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
final Container container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColor);
|
||||
final ColoredBox coloredBox = tester.widget<ColoredBox>(
|
||||
find.descendant(of: find.byType(CupertinoListTile), matching: find.byType(ColoredBox)),
|
||||
);
|
||||
expect(coloredBox.color, backgroundColor);
|
||||
});
|
||||
|
||||
testWidgets('changes backgroundColor when tapped if onTap is provided', (
|
||||
@ -153,17 +155,19 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
Container container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColor);
|
||||
ColoredBox coloredBox = tester.widget<ColoredBox>(
|
||||
find.descendant(of: find.byType(CupertinoListTile), matching: find.byType(ColoredBox)),
|
||||
);
|
||||
expect(coloredBox.color, backgroundColor);
|
||||
|
||||
// Pump only one frame so the color change persists.
|
||||
await tester.tap(find.byType(CupertinoListTile));
|
||||
await tester.pump();
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColorActivated);
|
||||
coloredBox = tester.widget<ColoredBox>(
|
||||
find.descendant(of: find.byType(CupertinoListTile), matching: find.byType(ColoredBox)),
|
||||
);
|
||||
expect(coloredBox.color, backgroundColorActivated);
|
||||
|
||||
// Pump the rest of the frames to complete the test.
|
||||
await tester.pumpAndSettle();
|
||||
@ -184,7 +188,6 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
expect(find.byType(GestureDetector), findsNothing);
|
||||
});
|
||||
|
||||
@ -203,7 +206,6 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
expect(find.byType(GestureDetector), findsOneWidget);
|
||||
});
|
||||
|
||||
@ -251,9 +253,10 @@ void main() {
|
||||
await tester.tap(find.byType(CupertinoButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
final Container container = tester.widget<Container>(find.byType(Container));
|
||||
expect(container.color, backgroundColor);
|
||||
final ColoredBox coloredBox = tester.widget<ColoredBox>(
|
||||
find.descendant(of: find.byType(CupertinoListTile), matching: find.byType(ColoredBox)),
|
||||
);
|
||||
expect(coloredBox.color, backgroundColor);
|
||||
});
|
||||
|
||||
group('alignment of widgets for left-to-right', () {
|
||||
@ -494,4 +497,49 @@ void main() {
|
||||
|
||||
expect(tester.takeException(), null);
|
||||
});
|
||||
|
||||
testWidgets('Leading and trailing animate on listtile long press', (WidgetTester tester) async {
|
||||
bool value = false;
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return CupertinoListTile(
|
||||
title: const Text(''),
|
||||
onTap:
|
||||
() => setState(() {
|
||||
value = !value;
|
||||
}),
|
||||
leading: CupertinoSwitch(value: value, onChanged: (_) {}),
|
||||
trailing: CupertinoSwitch(value: value, onChanged: (_) {}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final CurvedAnimation firstPosition =
|
||||
(tester.state(find.byType(CupertinoSwitch).first) as dynamic).position as CurvedAnimation;
|
||||
final CurvedAnimation lastPosition =
|
||||
(tester.state(find.byType(CupertinoSwitch).last) as dynamic).position as CurvedAnimation;
|
||||
|
||||
expect(firstPosition.value, 0.0);
|
||||
expect(lastPosition.value, 0.0);
|
||||
|
||||
await tester.longPress(find.byType(CupertinoListTile));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 65));
|
||||
|
||||
expect(firstPosition.value, greaterThan(0.0));
|
||||
expect(lastPosition.value, greaterThan(0.0));
|
||||
|
||||
expect(firstPosition.value, lessThan(1.0));
|
||||
expect(lastPosition.value, lessThan(1.0));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(firstPosition.value, 1.0);
|
||||
expect(lastPosition.value, 1.0);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user