From b0f5c8ce03e26bca3f5f36c494a0194a9fdcb758 Mon Sep 17 00:00:00 2001 From: Kishan Rathore <34465683+rkishan516@users.noreply.github.com> Date: Wed, 7 May 2025 22:22:38 +0530 Subject: [PATCH] Feat: Animate fill for material app bar (#163913) Feat: Animate fill for material app bar fixes: #162988 ## 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. - [x] 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]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../flutter/lib/src/material/app_bar.dart | 5 +++ .../flutter/lib/src/material/material.dart | 6 ++- .../flutter/test/material/app_bar_test.dart | 37 +++++++++++++++++++ .../flutter/test/material/app_bar_utils.dart | 8 ++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index b84d0de767d..b0bb50381f9 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -221,6 +221,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { this.useDefaultSemanticsOrder = true, this.clipBehavior, this.actionsPadding, + this.animateColor = false, }) : assert(elevation == null || elevation >= 0.0), preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height); @@ -773,6 +774,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { /// {@endtemplate} final EdgeInsetsGeometry? actionsPadding; + /// Whether the color should be animated. + final bool animateColor; + bool _getEffectiveCenterTitle(ThemeData theme) { bool platformCenter() { switch (theme.platform) { @@ -1213,6 +1217,7 @@ class _AppBarState extends State { ?? (theme.useMaterial3 ? theme.colorScheme.surfaceTint : null), shape: widget.shape ?? appBarTheme.shape ?? defaults.shape, + animateColor: widget.animateColor, child: Semantics(explicitChildNodes: true, child: appBar), ), ), diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index b9fe0523589..b074c2047ca 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -204,6 +204,7 @@ class Material extends StatefulWidget { this.clipBehavior = Clip.none, this.animationDuration = kThemeChangeDuration, this.child, + this.animateColor = false, }) : assert(elevation >= 0.0), assert(!(shape != null && borderRadius != null)), assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))); @@ -218,6 +219,9 @@ class Material extends StatefulWidget { /// the shape is rectangular, and the default color. final MaterialType type; + /// Whether the color should be animated. + final bool animateColor; + /// {@template flutter.material.material.elevation} /// The z-coordinate at which to place this material relative to its parent. /// @@ -522,7 +526,7 @@ class _MaterialState extends State with TickerProviderStateMixin { elevation: widget.elevation, color: color, shadowColor: modelShadowColor, - animateColor: false, + animateColor: widget.animateColor, child: contents, ); } diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 12995e66a40..4d89faf51a3 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -2244,17 +2244,21 @@ void main() { required double contentHeight, bool reverse = false, bool includeFlexibleSpace = false, + bool animateColor = false, + double? scrolledUnderElevation, }) { return MaterialApp( home: Scaffold( appBar: AppBar( elevation: 0, + scrolledUnderElevation: scrolledUnderElevation, backgroundColor: MaterialStateColor.resolveWith((Set states) { return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor; }), title: const Text('AppBar'), flexibleSpace: includeFlexibleSpace ? const FlexibleSpaceBar(title: Text('FlexibleSpace')) : null, + animateColor: animateColor, ), body: ListView( reverse: reverse, @@ -2339,6 +2343,39 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); + testWidgets('backgroundColor animation', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar(contentHeight: 1200.0, scrolledUnderElevation: 0, animateColor: true), + ); + + expect(getAppBarAnimatedBackgroundColor(tester), defaultColor); + + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await gesture.up(); + await tester.pump(); + + expect(getAppBarAnimatedBackgroundColor(tester), defaultColor); + await tester.pumpAndSettle(); + expect(getAppBarAnimatedBackgroundColor(tester), scrolledColor); + + gesture = await tester.startGesture(const Offset(50.0, 300.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pump(); + + expect(getAppBarAnimatedBackgroundColor(tester), scrolledColor); + + // Check intermediate color values. + await tester.pump(const Duration(milliseconds: 50)); + expect(getAppBarAnimatedBackgroundColor(tester), isSameColorAs(const Color(0xFF00C33C))); + await tester.pump(const Duration(milliseconds: 50)); + expect(getAppBarAnimatedBackgroundColor(tester), isSameColorAs(const Color(0xFF0039C6))); + + await tester.pumpAndSettle(); + expect(getAppBarAnimatedBackgroundColor(tester), defaultColor); + }); + testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async { await tester.pumpWidget(buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true)); diff --git a/packages/flutter/test/material/app_bar_utils.dart b/packages/flutter/test/material/app_bar_utils.dart index bc3d847e775..d71990a0c8d 100644 --- a/packages/flutter/test/material/app_bar_utils.dart +++ b/packages/flutter/test/material/app_bar_utils.dart @@ -5,6 +5,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +Finder findAppBarPhysicalModel() { + return find.descendant(of: find.byType(AppBar), matching: find.byType(PhysicalModel)).first; +} + +Color? getAppBarAnimatedBackgroundColor(WidgetTester tester) { + return tester.widget(findAppBarPhysicalModel()).color; +} + Finder findAppBarMaterial() { return find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first; }