Skip to content

Commit a8a61a1

Browse files
Add button icon support for animation duration (flutter#162667)
Fixes [Default foreground color animation duration doesn't apply on icon of `Button` widgets](flutter#162301) Fixes [Implement similar widget to``AnimatedDefaultTextStyle`` but for child ``Icon``](flutter#137251) ### Description This PR adds``AnimatedTheme` to `ButtonStyleButton` which is extended by buttons. It animates the button icon when changing icon color and size, similar to button text. ### Code Sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @OverRide Widget build(BuildContext context) { return const MaterialApp( home: HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); @OverRide Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, spacing: 20, children: <Widget>[ ElevatedButton.icon( icon: const Icon(Icons.favorite_rounded, size: 50), label: const Text('Button', style: TextStyle(fontSize: 36)), onPressed: () {}, style: const ButtonStyle( iconColor: WidgetStateProperty<Color>.fromMap( <WidgetStatesConstraint, Color>{ WidgetState.pressed: Color(0XFFFF0000), WidgetState.any: Color(0XFF000000), }, ), foregroundColor: WidgetStateProperty<Color>.fromMap( <WidgetStatesConstraint, Color>{ WidgetState.pressed: Color(0XFFFF0000), WidgetState.any: Color(0XFF000000), }, ), ), ), ElevatedButton.icon( icon: const Icon(Icons.favorite_rounded, size: 50), label: const Text('Button', style: TextStyle(fontSize: 36)), onPressed: () {}, style: const ButtonStyle( animationDuration: Duration(seconds: 2), iconColor: WidgetStateProperty<Color>.fromMap( <WidgetStatesConstraint, Color>{ WidgetState.pressed: Color(0XFFFF0000), WidgetState.any: Color(0XFF000000), }, ), foregroundColor: WidgetStateProperty<Color>.fromMap( <WidgetStatesConstraint, Color>{ WidgetState.pressed: Color(0XFFFF0000), WidgetState.any: Color(0XFF000000), }, ), ), ) ], ), ), ); } } ``` </details> ### Before https://github.com/user-attachments/assets/86fcab94-1147-4c49-b362-12f804a5d540 ### After https://github.com/user-attachments/assets/12a49de8-06d6-46c5-976f-5ce182d60423 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md Co-authored-by: Qun Cheng <[email protected]>
1 parent 3900a25 commit a8a61a1

11 files changed

+402
-82
lines changed

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

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'material.dart';
2525
import 'material_state.dart';
2626
import 'outlined_button.dart';
2727
import 'text_button.dart';
28+
import 'theme.dart';
2829
import 'theme_data.dart';
2930
import 'tooltip.dart';
3031

@@ -372,6 +373,8 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
372373

373374
@override
374375
Widget build(BuildContext context) {
376+
final ThemeData theme = Theme.of(context);
377+
final IconThemeData iconTheme = IconTheme.of(context);
375378
final ButtonStyle? widgetStyle = widget.style;
376379
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
377380
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
@@ -545,23 +548,26 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
545548
result = resolvedBackgroundBuilder(context, statesController.value, result);
546549
}
547550

548-
result = InkWell(
549-
onTap: widget.onPressed,
550-
onLongPress: widget.onLongPress,
551-
onHover: widget.onHover,
552-
mouseCursor: mouseCursor,
553-
enableFeedback: resolvedEnableFeedback,
554-
focusNode: widget.focusNode,
555-
canRequestFocus: widget.enabled,
556-
onFocusChange: widget.onFocusChange,
557-
autofocus: widget.autofocus,
558-
splashFactory: resolvedSplashFactory,
559-
overlayColor: overlayColor,
560-
highlightColor: Colors.transparent,
561-
customBorder: resolvedShape!.copyWith(side: resolvedSide),
562-
statesController: statesController,
563-
child: IconTheme.merge(
564-
data: IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
551+
result = AnimatedTheme(
552+
duration: resolvedAnimationDuration,
553+
data: theme.copyWith(
554+
iconTheme: iconTheme.merge(IconThemeData(color: resolvedIconColor, size: resolvedIconSize)),
555+
),
556+
child: InkWell(
557+
onTap: widget.onPressed,
558+
onLongPress: widget.onLongPress,
559+
onHover: widget.onHover,
560+
mouseCursor: mouseCursor,
561+
enableFeedback: resolvedEnableFeedback,
562+
focusNode: widget.focusNode,
563+
canRequestFocus: widget.enabled,
564+
onFocusChange: widget.onFocusChange,
565+
autofocus: widget.autofocus,
566+
splashFactory: resolvedSplashFactory,
567+
overlayColor: overlayColor,
568+
highlightColor: Colors.transparent,
569+
customBorder: resolvedShape!.copyWith(side: resolvedSide),
570+
statesController: statesController,
565571
child: result,
566572
),
567573
);

packages/flutter/test/material/app_bar_theme_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,11 @@ void main() {
484484
}
485485

486486
await tester.pumpWidget(buildFrame(appIconColor: Colors.lime));
487+
await tester.pumpAndSettle();
487488
expect(getIconText().text.style!.color, Colors.lime);
488489

489490
await tester.pumpWidget(buildFrame(appIconColor: Colors.lime, appBarIconColor: Colors.purple));
491+
await tester.pumpAndSettle();
490492
expect(getIconText().text.style!.color, Colors.purple);
491493
});
492494

packages/flutter/test/material/elevated_button_test.dart

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ void main() {
1717
return iconRichText.text.style!;
1818
}
1919

20+
Color textColor(WidgetTester tester, String text) {
21+
return tester.renderObject<RenderParagraph>(find.text(text)).text.style!.color!;
22+
}
23+
2024
testWidgets('ElevatedButton, ElevatedButton.icon defaults', (WidgetTester tester) async {
2125
const ColorScheme colorScheme = ColorScheme.light();
2226
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
@@ -435,8 +439,8 @@ void main() {
435439
testWidgets('ElevatedButton uses stateful color for text color in different states', (
436440
WidgetTester tester,
437441
) async {
442+
const String buttonText = 'ElevatedButton';
438443
final FocusNode focusNode = FocusNode();
439-
440444
const Color pressedColor = Color(0x00000001);
441445
const Color hoverColor = Color(0x00000002);
442446
const Color focusedColor = Color(0x00000003);
@@ -470,7 +474,7 @@ void main() {
470474
return ElevatedButton(
471475
onPressed: () {},
472476
focusNode: focusNode,
473-
child: const Text('ElevatedButton'),
477+
child: const Text(buttonText),
474478
);
475479
},
476480
),
@@ -480,33 +484,29 @@ void main() {
480484
),
481485
);
482486

483-
Color textColor() {
484-
return tester.renderObject<RenderParagraph>(find.text('ElevatedButton')).text.style!.color!;
485-
}
486-
487487
// Default, not disabled.
488-
expect(textColor(), equals(defaultColor));
488+
expect(textColor(tester, buttonText), equals(defaultColor));
489489

490490
// Focused.
491491
focusNode.requestFocus();
492492
await tester.pumpAndSettle();
493-
expect(textColor(), focusedColor);
493+
expect(textColor(tester, buttonText), focusedColor);
494494

495495
// Hovered.
496496
final Offset center = tester.getCenter(find.byType(ElevatedButton));
497497
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
498498
await gesture.addPointer();
499499
await gesture.moveTo(center);
500500
await tester.pumpAndSettle();
501-
expect(textColor(), hoverColor);
501+
expect(textColor(tester, buttonText), hoverColor);
502502

503503
// Highlighted (pressed).
504504
await gesture.down(center);
505505
await tester.pump(); // Start the splash and highlight animations.
506506
await tester.pump(
507507
const Duration(milliseconds: 800),
508508
); // Wait for splash and highlight to be well under way.
509-
expect(textColor(), pressedColor);
509+
expect(textColor(tester, buttonText), pressedColor);
510510

511511
focusNode.dispose();
512512
});
@@ -2504,6 +2504,7 @@ void main() {
25042504

25052505
// Test disabled button.
25062506
await tester.pumpWidget(buildButton(enabled: false));
2507+
await tester.pumpAndSettle();
25072508
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
25082509

25092510
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
@@ -2533,4 +2534,80 @@ void main() {
25332534
);
25342535
expect(iconStyle(tester, Icons.add).color, foregroundColor);
25352536
});
2537+
2538+
testWidgets('ElevatedButton text and icon respect animation duration', (
2539+
WidgetTester tester,
2540+
) async {
2541+
const String buttonText = 'Button';
2542+
const IconData buttonIcon = Icons.add;
2543+
const Color hoveredColor = Color(0xFFFF0000);
2544+
const Color idleColor = Color(0xFF000000);
2545+
2546+
Widget buildButton({Duration? animationDuration}) {
2547+
return MaterialApp(
2548+
home: Material(
2549+
child: Center(
2550+
child: ElevatedButton.icon(
2551+
style: ButtonStyle(
2552+
animationDuration: animationDuration,
2553+
iconColor: const WidgetStateProperty<Color>.fromMap(<WidgetStatesConstraint, Color>{
2554+
WidgetState.hovered: hoveredColor,
2555+
WidgetState.any: idleColor,
2556+
}),
2557+
foregroundColor: const WidgetStateProperty<Color>.fromMap(
2558+
<WidgetStatesConstraint, Color>{
2559+
WidgetState.hovered: hoveredColor,
2560+
WidgetState.any: idleColor,
2561+
},
2562+
),
2563+
),
2564+
onPressed: () {},
2565+
icon: const Icon(buttonIcon),
2566+
label: const Text(buttonText),
2567+
),
2568+
),
2569+
),
2570+
);
2571+
}
2572+
2573+
// Test default animation duration.
2574+
await tester.pumpWidget(buildButton());
2575+
2576+
expect(textColor(tester, buttonText), idleColor);
2577+
expect(iconStyle(tester, buttonIcon).color, idleColor);
2578+
2579+
final Offset buttonCenter = tester.getCenter(find.text(buttonText));
2580+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2581+
await gesture.addPointer();
2582+
addTearDown(gesture.removePointer);
2583+
await gesture.moveTo(buttonCenter);
2584+
2585+
await tester.pump();
2586+
await tester.pump(const Duration(milliseconds: 100));
2587+
expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
2588+
expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
2589+
2590+
await tester.pump();
2591+
await tester.pump(const Duration(milliseconds: 200));
2592+
expect(textColor(tester, buttonText), hoveredColor);
2593+
expect(iconStyle(tester, buttonIcon).color, hoveredColor);
2594+
2595+
await gesture.removePointer();
2596+
2597+
// Test custom animation duration.
2598+
await tester.pumpWidget(buildButton(animationDuration: const Duration(seconds: 2)));
2599+
await tester.pumpAndSettle();
2600+
2601+
await gesture.moveTo(buttonCenter);
2602+
2603+
await tester.pump();
2604+
await tester.pump(const Duration(seconds: 1));
2605+
expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
2606+
expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
2607+
2608+
await tester.pump();
2609+
await tester.pump(const Duration(seconds: 1));
2610+
expect(textColor(tester, buttonText), hoveredColor);
2611+
expect(iconStyle(tester, buttonIcon).color, hoveredColor);
2612+
});
25362613
}

packages/flutter/test/material/filled_button_test.dart

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ void main() {
1717
return iconRichText.text.style!;
1818
}
1919

20+
Color textColor(WidgetTester tester, String text) {
21+
return tester.renderObject<RenderParagraph>(find.text(text)).text.style!.color!;
22+
}
23+
2024
testWidgets('FilledButton, FilledButton.icon defaults', (WidgetTester tester) async {
2125
const ColorScheme colorScheme = ColorScheme.light();
2226
final ThemeData theme = ThemeData.from(useMaterial3: false, colorScheme: colorScheme);
@@ -620,8 +624,8 @@ void main() {
620624
testWidgets('FilledButton uses stateful color for text color in different states', (
621625
WidgetTester tester,
622626
) async {
627+
const String buttonText = 'FilledButton';
623628
final FocusNode focusNode = FocusNode();
624-
625629
const Color pressedColor = Color(0x00000001);
626630
const Color hoverColor = Color(0x00000002);
627631
const Color focusedColor = Color(0x00000003);
@@ -655,7 +659,7 @@ void main() {
655659
return FilledButton(
656660
onPressed: () {},
657661
focusNode: focusNode,
658-
child: const Text('FilledButton'),
662+
child: const Text(buttonText),
659663
);
660664
},
661665
),
@@ -665,33 +669,29 @@ void main() {
665669
),
666670
);
667671

668-
Color textColor() {
669-
return tester.renderObject<RenderParagraph>(find.text('FilledButton')).text.style!.color!;
670-
}
671-
672672
// Default, not disabled.
673-
expect(textColor(), equals(defaultColor));
673+
expect(textColor(tester, buttonText), equals(defaultColor));
674674

675675
// Focused.
676676
focusNode.requestFocus();
677677
await tester.pumpAndSettle();
678-
expect(textColor(), focusedColor);
678+
expect(textColor(tester, buttonText), focusedColor);
679679

680680
// Hovered.
681681
final Offset center = tester.getCenter(find.byType(FilledButton));
682682
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
683683
await gesture.addPointer();
684684
await gesture.moveTo(center);
685685
await tester.pumpAndSettle();
686-
expect(textColor(), hoverColor);
686+
expect(textColor(tester, buttonText), hoverColor);
687687

688688
// Highlighted (pressed).
689689
await gesture.down(center);
690690
await tester.pump(); // Start the splash and highlight animations.
691691
await tester.pump(
692692
const Duration(milliseconds: 800),
693693
); // Wait for splash and highlight to be well under way.
694-
expect(textColor(), pressedColor);
694+
expect(textColor(tester, buttonText), pressedColor);
695695
focusNode.dispose();
696696
});
697697

@@ -2750,6 +2750,7 @@ void main() {
27502750

27512751
// Test disabled button.
27522752
await tester.pumpWidget(buildButton(enabled: false));
2753+
await tester.pumpAndSettle();
27532754
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
27542755

27552756
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
@@ -2790,4 +2791,78 @@ void main() {
27902791
expect(iconStyle(tester, Icons.add).color, foregroundColor);
27912792
expect(iconStyle(tester, Icons.mail).color, foregroundColor);
27922793
});
2794+
2795+
testWidgets('FilledButton text and icon respect animation duration', (WidgetTester tester) async {
2796+
const String buttonText = 'Button';
2797+
const IconData buttonIcon = Icons.add;
2798+
const Color hoveredColor = Color(0xFFFF0000);
2799+
const Color idleColor = Color(0xFF000000);
2800+
2801+
Widget buildButton({Duration? animationDuration}) {
2802+
return MaterialApp(
2803+
home: Material(
2804+
child: Center(
2805+
child: FilledButton.icon(
2806+
style: ButtonStyle(
2807+
animationDuration: animationDuration,
2808+
iconColor: const WidgetStateProperty<Color>.fromMap(<WidgetStatesConstraint, Color>{
2809+
WidgetState.hovered: hoveredColor,
2810+
WidgetState.any: idleColor,
2811+
}),
2812+
foregroundColor: const WidgetStateProperty<Color>.fromMap(
2813+
<WidgetStatesConstraint, Color>{
2814+
WidgetState.hovered: hoveredColor,
2815+
WidgetState.any: idleColor,
2816+
},
2817+
),
2818+
),
2819+
onPressed: () {},
2820+
icon: const Icon(buttonIcon),
2821+
label: const Text(buttonText),
2822+
),
2823+
),
2824+
),
2825+
);
2826+
}
2827+
2828+
// Test default animation duration.
2829+
await tester.pumpWidget(buildButton());
2830+
2831+
expect(textColor(tester, buttonText), idleColor);
2832+
expect(iconStyle(tester, buttonIcon).color, idleColor);
2833+
2834+
final Offset buttonCenter = tester.getCenter(find.text(buttonText));
2835+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2836+
await gesture.addPointer();
2837+
addTearDown(gesture.removePointer);
2838+
await gesture.moveTo(buttonCenter);
2839+
2840+
await tester.pump();
2841+
await tester.pump(const Duration(milliseconds: 100));
2842+
expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
2843+
expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
2844+
2845+
await tester.pump();
2846+
await tester.pump(const Duration(milliseconds: 200));
2847+
expect(textColor(tester, buttonText), hoveredColor);
2848+
expect(iconStyle(tester, buttonIcon).color, hoveredColor);
2849+
2850+
await gesture.removePointer();
2851+
2852+
// Test custom animation duration.
2853+
await tester.pumpWidget(buildButton(animationDuration: const Duration(seconds: 2)));
2854+
await tester.pumpAndSettle();
2855+
2856+
await gesture.moveTo(buttonCenter);
2857+
2858+
await tester.pump();
2859+
await tester.pump(const Duration(seconds: 1));
2860+
expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
2861+
expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
2862+
2863+
await tester.pump();
2864+
await tester.pump(const Duration(seconds: 1));
2865+
expect(textColor(tester, buttonText), hoveredColor);
2866+
expect(iconStyle(tester, buttonIcon).color, hoveredColor);
2867+
});
27932868
}

0 commit comments

Comments
 (0)