From 0c7d84aa789e649cb05b0f5f890c04e482f43737 Mon Sep 17 00:00:00 2001 From: Daniel Iglesia Date: Tue, 13 Dec 2022 14:45:50 -0800 Subject: [PATCH] Add AppBar.forceMaterialTransparency (#101248) (#116867) * Add AppBar.forceMaterialTransparency (#101248) Allows gestures to reach widgets beneath the AppBar (when Scaffold.extendBodyBehindAppBar is true). --- .../flutter/lib/src/material/app_bar.dart | 32 +++- .../flutter/test/material/app_bar_test.dart | 163 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 781da031627c..ac25e2222603 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -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), @@ -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); @@ -1193,6 +1209,9 @@ class _AppBarState extends State { child: Material( color: backgroundColor, elevation: effectiveElevation, + type: widget.forceMaterialTransparency + ? MaterialType.transparency + : MaterialType.canvas, shadowColor: widget.shadowColor ?? appBarTheme.shadowColor ?? defaults.shadowColor, @@ -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, @@ -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; @@ -1362,6 +1383,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { toolbarTextStyle: toolbarTextStyle, titleTextStyle: titleTextStyle, systemOverlayStyle: systemOverlayStyle, + forceMaterialTransparency: forceMaterialTransparency, ), ); return appBar; @@ -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 @@ -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), @@ -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 createState() => _SliverAppBarState(); } @@ -2146,6 +2175,7 @@ class _SliverAppBarState extends State with TickerProviderStateMix toolbarTextStyle: widget.toolbarTextStyle, titleTextStyle: widget.titleTextStyle, systemOverlayStyle: widget.systemOverlayStyle, + forceMaterialTransparency: widget.forceMaterialTransparency, ), ), ); diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index b99029cab578..97720224b9f7 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -1390,6 +1390,95 @@ void main() { }); }); + group('SliverAppBar.forceMaterialTransparency', () { + Material getSliverAppBarMaterial(WidgetTester tester) { + return tester.widget(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: [ + 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)); @@ -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(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); + }); + }); }