Skip to content

Commit 0c7d84a

Browse files
authored
Add AppBar.forceMaterialTransparency (#101248) (#116867)
* Add AppBar.forceMaterialTransparency (#101248) Allows gestures to reach widgets beneath the AppBar (when Scaffold.extendBodyBehindAppBar is true).
1 parent 96597c2 commit 0c7d84a

File tree

2 files changed

+194
-1
lines changed

2 files changed

+194
-1
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
220220
this.toolbarTextStyle,
221221
this.titleTextStyle,
222222
this.systemOverlayStyle,
223+
this.forceMaterialTransparency = false,
223224
}) : assert(automaticallyImplyLeading != null),
224225
assert(elevation == null || elevation >= 0.0),
225226
assert(notificationPredicate != null),
@@ -789,6 +790,21 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
789790
/// * [SystemChrome.setSystemUIOverlayStyle]
790791
final SystemUiOverlayStyle? systemOverlayStyle;
791792

793+
/// {@template flutter.material.appbar.forceMaterialTransparency}
794+
/// Forces the AppBar's Material widget type to be [MaterialType.transparency]
795+
/// (instead of Material's default type).
796+
///
797+
/// This will remove the visual display of [backgroundColor] and [elevation],
798+
/// and affect other characteristics of the AppBar's Material widget.
799+
///
800+
/// Provided for cases where the app bar is to be transparent, and gestures
801+
/// must pass through the app bar to widgets beneath the app bar (i.e. with
802+
/// [Scaffold.extendBodyBehindAppBar] set to true).
803+
///
804+
/// Defaults to false.
805+
/// {@endtemplate}
806+
final bool forceMaterialTransparency;
807+
792808
bool _getEffectiveCenterTitle(ThemeData theme) {
793809
bool platformCenter() {
794810
assert(theme.platform != null);
@@ -1193,6 +1209,9 @@ class _AppBarState extends State<AppBar> {
11931209
child: Material(
11941210
color: backgroundColor,
11951211
elevation: effectiveElevation,
1212+
type: widget.forceMaterialTransparency
1213+
? MaterialType.transparency
1214+
: MaterialType.canvas,
11961215
shadowColor: widget.shadowColor
11971216
?? appBarTheme.shadowColor
11981217
?? defaults.shadowColor,
@@ -1249,6 +1268,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
12491268
required this.toolbarTextStyle,
12501269
required this.titleTextStyle,
12511270
required this.systemOverlayStyle,
1271+
required this.forceMaterialTransparency,
12521272
}) : assert(primary || topPadding == 0.0),
12531273
assert(
12541274
!floating || (snapConfiguration == null && showOnScreenConfiguration == null) || vsync != null,
@@ -1290,6 +1310,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
12901310
final TextStyle? titleTextStyle;
12911311
final SystemUiOverlayStyle? systemOverlayStyle;
12921312
final double _bottomHeight;
1313+
final bool forceMaterialTransparency;
12931314

12941315
@override
12951316
double get minExtent => collapsedHeight;
@@ -1362,6 +1383,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
13621383
toolbarTextStyle: toolbarTextStyle,
13631384
titleTextStyle: titleTextStyle,
13641385
systemOverlayStyle: systemOverlayStyle,
1386+
forceMaterialTransparency: forceMaterialTransparency,
13651387
),
13661388
);
13671389
return appBar;
@@ -1401,7 +1423,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
14011423
|| backwardsCompatibility != oldDelegate.backwardsCompatibility
14021424
|| toolbarTextStyle != oldDelegate.toolbarTextStyle
14031425
|| titleTextStyle != oldDelegate.titleTextStyle
1404-
|| systemOverlayStyle != oldDelegate.systemOverlayStyle;
1426+
|| systemOverlayStyle != oldDelegate.systemOverlayStyle
1427+
|| forceMaterialTransparency != oldDelegate.forceMaterialTransparency;
14051428
}
14061429

14071430
@override
@@ -1550,6 +1573,7 @@ class SliverAppBar extends StatefulWidget {
15501573
this.toolbarTextStyle,
15511574
this.titleTextStyle,
15521575
this.systemOverlayStyle,
1576+
this.forceMaterialTransparency = false,
15531577
}) : assert(automaticallyImplyLeading != null),
15541578
assert(forceElevated != null),
15551579
assert(primary != null),
@@ -2038,6 +2062,11 @@ class SliverAppBar extends StatefulWidget {
20382062
/// This property is used to configure an [AppBar].
20392063
final SystemUiOverlayStyle? systemOverlayStyle;
20402064

2065+
/// {@macro flutter.material.appbar.forceMaterialTransparency}
2066+
///
2067+
/// This property is used to configure an [AppBar].
2068+
final bool forceMaterialTransparency;
2069+
20412070
@override
20422071
State<SliverAppBar> createState() => _SliverAppBarState();
20432072
}
@@ -2146,6 +2175,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
21462175
toolbarTextStyle: widget.toolbarTextStyle,
21472176
titleTextStyle: widget.titleTextStyle,
21482177
systemOverlayStyle: widget.systemOverlayStyle,
2178+
forceMaterialTransparency: widget.forceMaterialTransparency,
21492179
),
21502180
),
21512181
);

packages/flutter/test/material/app_bar_test.dart

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,95 @@ void main() {
13901390
});
13911391
});
13921392

1393+
group('SliverAppBar.forceMaterialTransparency', () {
1394+
Material getSliverAppBarMaterial(WidgetTester tester) {
1395+
return tester.widget<Material>(find
1396+
.descendant(of: find.byType(SliverAppBar), matching: find.byType(Material))
1397+
.first);
1398+
}
1399+
1400+
// Generates a MaterialApp with a SliverAppBar in a CustomScrollView.
1401+
// The first cell of the scroll view contains a button at its top, and is
1402+
// initially scrolled so that it is beneath the SliverAppBar.
1403+
Widget buildWidget({
1404+
required bool forceMaterialTransparency,
1405+
required VoidCallback onPressed
1406+
}) {
1407+
const double appBarHeight = 120;
1408+
return MaterialApp(
1409+
home: Scaffold(
1410+
body: CustomScrollView(
1411+
controller: ScrollController(initialScrollOffset:appBarHeight),
1412+
slivers: <Widget>[
1413+
SliverAppBar(
1414+
collapsedHeight: appBarHeight,
1415+
expandedHeight: appBarHeight,
1416+
pinned: true,
1417+
elevation: 0,
1418+
backgroundColor: Colors.transparent,
1419+
forceMaterialTransparency: forceMaterialTransparency,
1420+
title: const Text('AppBar'),
1421+
),
1422+
SliverList(
1423+
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
1424+
return SizedBox(
1425+
height: appBarHeight,
1426+
child: index == 0
1427+
? Align(
1428+
alignment: Alignment.topCenter,
1429+
child: TextButton(onPressed: onPressed, child: const Text('press')))
1430+
: const SizedBox(),
1431+
);
1432+
},
1433+
childCount: 20,
1434+
),
1435+
),
1436+
]),
1437+
),
1438+
);
1439+
}
1440+
1441+
testWidgets(
1442+
'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
1443+
bool buttonWasPressed = false;
1444+
final Widget widget = buildWidget(
1445+
forceMaterialTransparency:true,
1446+
onPressed:() { buttonWasPressed = true; },
1447+
);
1448+
await tester.pumpWidget(widget);
1449+
1450+
final Material material = getSliverAppBarMaterial(tester);
1451+
expect(material.type, MaterialType.transparency);
1452+
1453+
final Finder buttonFinder = find.byType(TextButton);
1454+
await tester.tap(buttonFinder);
1455+
await tester.pump();
1456+
expect(buttonWasPressed, isTrue);
1457+
});
1458+
1459+
testWidgets(
1460+
'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async {
1461+
// Set this, and tester.tap(warnIfMissed:false), to suppress
1462+
// errors/warning that the button is not hittable (which is expected).
1463+
WidgetController.hitTestWarningShouldBeFatal = false;
1464+
1465+
bool buttonWasPressed = false;
1466+
final Widget widget = buildWidget(
1467+
forceMaterialTransparency:false,
1468+
onPressed:() { buttonWasPressed = true; },
1469+
);
1470+
await tester.pumpWidget(widget);
1471+
1472+
final Material material = getSliverAppBarMaterial(tester);
1473+
expect(material.type, MaterialType.canvas);
1474+
1475+
final Finder buttonFinder = find.byType(TextButton);
1476+
await tester.tap(buttonFinder, warnIfMissed:false);
1477+
await tester.pump();
1478+
expect(buttonWasPressed, isFalse);
1479+
});
1480+
});
1481+
13931482
testWidgets('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async {
13941483
const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0));
13951484

@@ -3760,4 +3849,78 @@ void main() {
37603849
expect(tester.getTopLeft(find.byKey(titleKey)).dx, leadingWidth + 16.0);
37613850
expect(tester.getSize(find.byKey(leadingKey)).width, leadingWidth);
37623851
});
3852+
3853+
group('AppBar.forceMaterialTransparency', () {
3854+
Material getAppBarMaterial(WidgetTester tester) {
3855+
return tester.widget<Material>(find
3856+
.descendant(of: find.byType(AppBar), matching: find.byType(Material))
3857+
.first);
3858+
}
3859+
3860+
// Generates a MaterialApp with an AppBar with a TextButton beneath it
3861+
// (via extendBodyBehindAppBar = true).
3862+
Widget buildWidget({
3863+
required bool forceMaterialTransparency,
3864+
required VoidCallback onPressed
3865+
}) {
3866+
return MaterialApp(
3867+
home: Scaffold(
3868+
extendBodyBehindAppBar: true,
3869+
appBar: AppBar(
3870+
forceMaterialTransparency: forceMaterialTransparency,
3871+
elevation: 3,
3872+
backgroundColor: Colors.red,
3873+
title: const Text('AppBar'),
3874+
),
3875+
body: Align(
3876+
alignment: Alignment.topCenter,
3877+
child: TextButton(
3878+
onPressed: onPressed,
3879+
child: const Text('press me'),
3880+
),
3881+
),
3882+
),
3883+
);
3884+
}
3885+
3886+
testWidgets(
3887+
'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
3888+
bool buttonWasPressed = false;
3889+
final Widget widget = buildWidget(
3890+
forceMaterialTransparency:true,
3891+
onPressed:() { buttonWasPressed = true; },
3892+
);
3893+
await tester.pumpWidget(widget);
3894+
3895+
final Material material = getAppBarMaterial(tester);
3896+
expect(material.type, MaterialType.transparency);
3897+
3898+
final Finder buttonFinder = find.byType(TextButton);
3899+
await tester.tap(buttonFinder);
3900+
await tester.pump();
3901+
expect(buttonWasPressed, isTrue);
3902+
});
3903+
3904+
testWidgets(
3905+
'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async {
3906+
// Set this, and tester.tap(warnIfMissed:false), to suppress
3907+
// errors/warning that the button is not hittable (which is expected).
3908+
WidgetController.hitTestWarningShouldBeFatal = false;
3909+
3910+
bool buttonWasPressed = false;
3911+
final Widget widget = buildWidget(
3912+
forceMaterialTransparency:false,
3913+
onPressed:() { buttonWasPressed = true; },
3914+
);
3915+
await tester.pumpWidget(widget);
3916+
3917+
final Material material = getAppBarMaterial(tester);
3918+
expect(material.type, MaterialType.canvas);
3919+
3920+
final Finder buttonFinder = find.byType(TextButton);
3921+
await tester.tap(buttonFinder, warnIfMissed:false);
3922+
await tester.pump();
3923+
expect(buttonWasPressed, isFalse);
3924+
});
3925+
});
37633926
}

0 commit comments

Comments
 (0)