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.
This commit is contained in:
Kishan Rathore 2025-05-07 22:22:38 +05:30 committed by GitHub
parent 708c0eb185
commit b0f5c8ce03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 1 deletions

View File

@ -221,6 +221,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.useDefaultSemanticsOrder = true, this.useDefaultSemanticsOrder = true,
this.clipBehavior, this.clipBehavior,
this.actionsPadding, this.actionsPadding,
this.animateColor = false,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height); preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height);
@ -773,6 +774,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// {@endtemplate} /// {@endtemplate}
final EdgeInsetsGeometry? actionsPadding; final EdgeInsetsGeometry? actionsPadding;
/// Whether the color should be animated.
final bool animateColor;
bool _getEffectiveCenterTitle(ThemeData theme) { bool _getEffectiveCenterTitle(ThemeData theme) {
bool platformCenter() { bool platformCenter() {
switch (theme.platform) { switch (theme.platform) {
@ -1213,6 +1217,7 @@ class _AppBarState extends State<AppBar> {
?? ??
(theme.useMaterial3 ? theme.colorScheme.surfaceTint : null), (theme.useMaterial3 ? theme.colorScheme.surfaceTint : null),
shape: widget.shape ?? appBarTheme.shape ?? defaults.shape, shape: widget.shape ?? appBarTheme.shape ?? defaults.shape,
animateColor: widget.animateColor,
child: Semantics(explicitChildNodes: true, child: appBar), child: Semantics(explicitChildNodes: true, child: appBar),
), ),
), ),

View File

@ -204,6 +204,7 @@ class Material extends StatefulWidget {
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.animationDuration = kThemeChangeDuration, this.animationDuration = kThemeChangeDuration,
this.child, this.child,
this.animateColor = false,
}) : assert(elevation >= 0.0), }) : assert(elevation >= 0.0),
assert(!(shape != null && borderRadius != null)), assert(!(shape != null && borderRadius != null)),
assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != 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. /// the shape is rectangular, and the default color.
final MaterialType type; final MaterialType type;
/// Whether the color should be animated.
final bool animateColor;
/// {@template flutter.material.material.elevation} /// {@template flutter.material.material.elevation}
/// The z-coordinate at which to place this material relative to its parent. /// The z-coordinate at which to place this material relative to its parent.
/// ///
@ -522,7 +526,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
elevation: widget.elevation, elevation: widget.elevation,
color: color, color: color,
shadowColor: modelShadowColor, shadowColor: modelShadowColor,
animateColor: false, animateColor: widget.animateColor,
child: contents, child: contents,
); );
} }

View File

@ -2244,17 +2244,21 @@ void main() {
required double contentHeight, required double contentHeight,
bool reverse = false, bool reverse = false,
bool includeFlexibleSpace = false, bool includeFlexibleSpace = false,
bool animateColor = false,
double? scrolledUnderElevation,
}) { }) {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
scrolledUnderElevation: scrolledUnderElevation,
backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) { backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor; return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor;
}), }),
title: const Text('AppBar'), title: const Text('AppBar'),
flexibleSpace: flexibleSpace:
includeFlexibleSpace ? const FlexibleSpaceBar(title: Text('FlexibleSpace')) : null, includeFlexibleSpace ? const FlexibleSpaceBar(title: Text('FlexibleSpace')) : null,
animateColor: animateColor,
), ),
body: ListView( body: ListView(
reverse: reverse, reverse: reverse,
@ -2339,6 +2343,39 @@ void main() {
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); 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 { testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async {
await tester.pumpWidget(buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true)); await tester.pumpWidget(buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true));

View File

@ -5,6 +5,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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<PhysicalModel>(findAppBarPhysicalModel()).color;
}
Finder findAppBarMaterial() { Finder findAppBarMaterial() {
return find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first; return find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first;
} }