diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 4ae16d6827..af23cfa5ef 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -162,6 +162,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { this.bottom, this.elevation, this.scrolledUnderElevation, + this.notificationPredicate = defaultScrollNotificationPredicate, this.shadowColor, this.surfaceTintColor, this.shape, @@ -197,6 +198,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { this.systemOverlayStyle, }) : assert(automaticallyImplyLeading != null), assert(elevation == null || elevation >= 0.0), + assert(notificationPredicate != null), assert(primary != null), assert(toolbarOpacity != null), assert(bottomOpacity != null), @@ -421,6 +423,13 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { /// shadow. final double? scrolledUnderElevation; + /// A check that specifies which child's [ScrollNotification]s should be + /// listened to. + /// + /// By default, checks whether `notification.depth == 0`. Set it to something + /// else for more complicated layouts. + final ScrollNotificationPredicate notificationPredicate; + /// {@template flutter.material.appbar.shadowColor} /// The color of the shadow below the app bar. /// @@ -809,7 +818,7 @@ class _AppBarState extends State { } void _handleScrollNotification(ScrollNotification notification) { - if (notification is ScrollUpdateNotification && notification.depth == 0) { + if (notification is ScrollUpdateNotification && widget.notificationPredicate(notification)) { final bool oldScrolledUnder = _scrolledUnder; final ScrollMetrics metrics = notification.metrics; switch (metrics.axisDirection) { diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 635e17c694..1aec5d4539 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -1011,6 +1011,53 @@ void main() { expect(getMaterial().elevation, 10); }); + testWidgets('scrolledUnderElevation with nested scroll view', (WidgetTester tester) async { + Widget buildAppBar({double? scrolledUnderElevation}) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + appBar: AppBar( + title: const Text('Title'), + scrolledUnderElevation: scrolledUnderElevation, + notificationPredicate: (ScrollNotification notification) { + return notification.depth == 1; + }, + ), + body: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 4, + itemBuilder: (BuildContext context, int index) { + return SizedBox( + height: 600.0, + width: 800.0, + child: ListView.builder( + itemCount: 100, + itemBuilder: (BuildContext context, int index) => + ListTile(title: Text('Item $index')), + ), + ); + }, + ), + ), + ); + } + + Material getMaterial() => tester.widget(find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + )); + + await tester.pumpWidget(buildAppBar(scrolledUnderElevation: 10)); + // Starts with the base elevation. + expect(getMaterial().elevation, 0.0); + + await tester.fling(find.text('Item 2'), const Offset(0.0, -600.0), 2000.0); + await tester.pumpAndSettle(); + + // After scrolling it should be the scrolledUnderElevation. + expect(getMaterial().elevation, 10); + }); + group('SliverAppBar elevation', () { Widget buildSliverAppBar(bool forceElevated, {double? elevation, double? themeElevation}) { return MaterialApp(