Skip to content

Commit

Permalink
Add AppBar.forceMaterialTransparency (#101248) (#116867)
Browse files Browse the repository at this point in the history
* Add AppBar.forceMaterialTransparency (#101248)

Allows gestures to reach widgets beneath the AppBar (when Scaffold.extendBodyBehindAppBar is true).
  • Loading branch information
monkeyswarm committed Dec 13, 2022
1 parent 96597c2 commit 0c7d84a
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 1 deletion.
32 changes: 31 additions & 1 deletion packages/flutter/lib/src/material/app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
}) : assert(automaticallyImplyLeading != null),
assert(elevation == null || elevation >= 0.0),
assert(notificationPredicate != null),
Expand Down Expand Up @@ -789,6 +790,21 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// * [SystemChrome.setSystemUIOverlayStyle]
final SystemUiOverlayStyle? systemOverlayStyle;

/// {@template flutter.material.appbar.forceMaterialTransparency}
/// Forces the AppBar's Material widget type to be [MaterialType.transparency]
/// (instead of Material's default type).
///
/// This will remove the visual display of [backgroundColor] and [elevation],
/// and affect other characteristics of the AppBar's Material widget.
///
/// Provided for cases where the app bar is to be transparent, and gestures
/// must pass through the app bar to widgets beneath the app bar (i.e. with
/// [Scaffold.extendBodyBehindAppBar] set to true).
///
/// Defaults to false.
/// {@endtemplate}
final bool forceMaterialTransparency;

bool _getEffectiveCenterTitle(ThemeData theme) {
bool platformCenter() {
assert(theme.platform != null);
Expand Down Expand Up @@ -1193,6 +1209,9 @@ class _AppBarState extends State<AppBar> {
child: Material(
color: backgroundColor,
elevation: effectiveElevation,
type: widget.forceMaterialTransparency
? MaterialType.transparency
: MaterialType.canvas,
shadowColor: widget.shadowColor
?? appBarTheme.shadowColor
?? defaults.shadowColor,
Expand Down Expand Up @@ -1249,6 +1268,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
required this.toolbarTextStyle,
required this.titleTextStyle,
required this.systemOverlayStyle,
required this.forceMaterialTransparency,
}) : assert(primary || topPadding == 0.0),
assert(
!floating || (snapConfiguration == null && showOnScreenConfiguration == null) || vsync != null,
Expand Down Expand Up @@ -1290,6 +1310,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final double _bottomHeight;
final bool forceMaterialTransparency;

@override
double get minExtent => collapsedHeight;
Expand Down Expand Up @@ -1362,6 +1383,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
forceMaterialTransparency: forceMaterialTransparency,
),
);
return appBar;
Expand Down Expand Up @@ -1401,7 +1423,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| backwardsCompatibility != oldDelegate.backwardsCompatibility
|| toolbarTextStyle != oldDelegate.toolbarTextStyle
|| titleTextStyle != oldDelegate.titleTextStyle
|| systemOverlayStyle != oldDelegate.systemOverlayStyle;
|| systemOverlayStyle != oldDelegate.systemOverlayStyle
|| forceMaterialTransparency != oldDelegate.forceMaterialTransparency;
}

@override
Expand Down Expand Up @@ -1550,6 +1573,7 @@ class SliverAppBar extends StatefulWidget {
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
}) : assert(automaticallyImplyLeading != null),
assert(forceElevated != null),
assert(primary != null),
Expand Down Expand Up @@ -2038,6 +2062,11 @@ class SliverAppBar extends StatefulWidget {
/// This property is used to configure an [AppBar].
final SystemUiOverlayStyle? systemOverlayStyle;

/// {@macro flutter.material.appbar.forceMaterialTransparency}
///
/// This property is used to configure an [AppBar].
final bool forceMaterialTransparency;

@override
State<SliverAppBar> createState() => _SliverAppBarState();
}
Expand Down Expand Up @@ -2146,6 +2175,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
),
),
);
Expand Down
163 changes: 163 additions & 0 deletions packages/flutter/test/material/app_bar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,95 @@ void main() {
});
});

group('SliverAppBar.forceMaterialTransparency', () {
Material getSliverAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(find
.descendant(of: find.byType(SliverAppBar), matching: find.byType(Material))
.first);
}

// Generates a MaterialApp with a SliverAppBar in a CustomScrollView.
// The first cell of the scroll view contains a button at its top, and is
// initially scrolled so that it is beneath the SliverAppBar.
Widget buildWidget({
required bool forceMaterialTransparency,
required VoidCallback onPressed
}) {
const double appBarHeight = 120;
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
controller: ScrollController(initialScrollOffset:appBarHeight),
slivers: <Widget>[
SliverAppBar(
collapsedHeight: appBarHeight,
expandedHeight: appBarHeight,
pinned: true,
elevation: 0,
backgroundColor: Colors.transparent,
forceMaterialTransparency: forceMaterialTransparency,
title: const Text('AppBar'),
),
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return SizedBox(
height: appBarHeight,
child: index == 0
? Align(
alignment: Alignment.topCenter,
child: TextButton(onPressed: onPressed, child: const Text('press')))
: const SizedBox(),
);
},
childCount: 20,
),
),
]),
),
);
}

testWidgets(
'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
bool buttonWasPressed = false;
final Widget widget = buildWidget(
forceMaterialTransparency:true,
onPressed:() { buttonWasPressed = true; },
);
await tester.pumpWidget(widget);

final Material material = getSliverAppBarMaterial(tester);
expect(material.type, MaterialType.transparency);

final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder);
await tester.pump();
expect(buttonWasPressed, isTrue);
});

testWidgets(
'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async {
// Set this, and tester.tap(warnIfMissed:false), to suppress
// errors/warning that the button is not hittable (which is expected).
WidgetController.hitTestWarningShouldBeFatal = false;

bool buttonWasPressed = false;
final Widget widget = buildWidget(
forceMaterialTransparency:false,
onPressed:() { buttonWasPressed = true; },
);
await tester.pumpWidget(widget);

final Material material = getSliverAppBarMaterial(tester);
expect(material.type, MaterialType.canvas);

final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder, warnIfMissed:false);
await tester.pump();
expect(buttonWasPressed, isFalse);
});
});

testWidgets('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async {
const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0));

Expand Down Expand Up @@ -3760,4 +3849,78 @@ void main() {
expect(tester.getTopLeft(find.byKey(titleKey)).dx, leadingWidth + 16.0);
expect(tester.getSize(find.byKey(leadingKey)).width, leadingWidth);
});

group('AppBar.forceMaterialTransparency', () {
Material getAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(find
.descendant(of: find.byType(AppBar), matching: find.byType(Material))
.first);
}

// Generates a MaterialApp with an AppBar with a TextButton beneath it
// (via extendBodyBehindAppBar = true).
Widget buildWidget({
required bool forceMaterialTransparency,
required VoidCallback onPressed
}) {
return MaterialApp(
home: Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
forceMaterialTransparency: forceMaterialTransparency,
elevation: 3,
backgroundColor: Colors.red,
title: const Text('AppBar'),
),
body: Align(
alignment: Alignment.topCenter,
child: TextButton(
onPressed: onPressed,
child: const Text('press me'),
),
),
),
);
}

testWidgets(
'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
bool buttonWasPressed = false;
final Widget widget = buildWidget(
forceMaterialTransparency:true,
onPressed:() { buttonWasPressed = true; },
);
await tester.pumpWidget(widget);

final Material material = getAppBarMaterial(tester);
expect(material.type, MaterialType.transparency);

final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder);
await tester.pump();
expect(buttonWasPressed, isTrue);
});

testWidgets(
'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async {
// Set this, and tester.tap(warnIfMissed:false), to suppress
// errors/warning that the button is not hittable (which is expected).
WidgetController.hitTestWarningShouldBeFatal = false;

bool buttonWasPressed = false;
final Widget widget = buildWidget(
forceMaterialTransparency:false,
onPressed:() { buttonWasPressed = true; },
);
await tester.pumpWidget(widget);

final Material material = getAppBarMaterial(tester);
expect(material.type, MaterialType.canvas);

final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder, warnIfMissed:false);
await tester.pump();
expect(buttonWasPressed, isFalse);
});
});
}

0 comments on commit 0c7d84a

Please sign in to comment.