diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 92986d542bdd..26c882394327 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -1284,7 +1284,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { final double extraToolbarHeight = math.max(minExtent - _bottomHeight - topPadding - (toolbarHeight ?? kToolbarHeight), 0.0); final double visibleToolbarHeight = visibleMainHeight - _bottomHeight - extraToolbarHeight; - final bool isScrolledUnder = overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent); + final bool isScrolledUnder = overlapsContent || forceElevated || (pinned && shrinkOffset > maxExtent - minExtent); final bool isPinnedWithOpacityFade = pinned && floating && bottom != null && extraToolbarHeight == 0.0; final double toolbarOpacity = !pinned || isPinnedWithOpacityFade ? clampDouble(visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight), 0.0, 1.0) @@ -1308,7 +1308,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ) : flexibleSpace, bottom: bottom, - elevation: forceElevated || isScrolledUnder ? elevation : 0.0, + elevation: isScrolledUnder ? elevation : 0.0, scrolledUnderElevation: scrolledUnderElevation, shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index 7d366fd60f90..fdf7dd03f127 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -2552,8 +2552,175 @@ void main() { expect(scrollStarted, 2); expect(scrollEnded, 2); }); + + testWidgets('SliverAppBar.medium collapses in NestedScrollView', (WidgetTester tester) async { + final GlobalKey nestedScrollView = GlobalKey(); + const double collapsedAppBarHeight = 64; + const double expandedAppBarHeight = 112; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: NestedScrollView( + key: nestedScrollView, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.medium( + title: const Text('AppBar Title'), + ), + ), + ]; + }, + body: Builder( + builder: (BuildContext context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)), + SliverFixedExtentList( + itemExtent: 50.0, + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => ListTile(title: Text('Item $index')), + childCount: 30, + ), + ), + ], + ); + }, + ), + ), + ), + )); + + // There are two widgets for the title. + final Finder expandedTitle = find.text('AppBar Title').last; + final Finder expandedTitleClip = find.ancestor( + of: expandedTitle, + matching: find.byType(ClipRect), + ); + + // Default, fully expanded app bar. + expect(nestedScrollView.currentState?.outerController.offset, 0); + expect(nestedScrollView.currentState?.innerController.offset, 0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), expandedAppBarHeight); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + + // Scroll the expanded app bar partially out of view. + final Offset point1 = tester.getCenter(find.text('Item 5')); + await tester.dragFrom(point1, const Offset(0.0, -45.0)); + await tester.pump(); + expect(nestedScrollView.currentState?.outerController.offset, 45.0); + expect(nestedScrollView.currentState?.innerController.offset, 0.0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), expandedAppBarHeight - 45); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45); + + // Scroll so that it is completely collapsed. + await tester.dragFrom(point1, const Offset(0.0, -555.0)); + await tester.pump(); + expect(nestedScrollView.currentState?.outerController.offset, 48.0); + expect(nestedScrollView.currentState?.innerController.offset, 552.0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), collapsedAppBarHeight); + expect(tester.getSize(expandedTitleClip).height, 0); + + // Scroll back to fully expanded. + await tester.dragFrom(point1, const Offset(0.0, 600.0)); + await tester.pump(); + expect(nestedScrollView.currentState?.outerController.offset, 0); + expect(nestedScrollView.currentState?.innerController.offset, 0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), expandedAppBarHeight); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + }); + + testWidgets('SliverAppBar.large collapses in NestedScrollView', (WidgetTester tester) async { + final GlobalKey nestedScrollView = GlobalKey(); + const double collapsedAppBarHeight = 64; + const double expandedAppBarHeight = 152; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: NestedScrollView( + key: nestedScrollView, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + title: const Text('AppBar Title'), + forceElevated: innerBoxIsScrolled, + ), + ), + ]; + }, + body: Builder( + builder: (BuildContext context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)), + SliverFixedExtentList( + itemExtent: 50.0, + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => ListTile(title: Text('Item $index')), + childCount: 30, + ), + ), + ], + ); + }, + ), + ), + ), + )); + + // There are two widgets for the title. + final Finder expandedTitle = find.text('AppBar Title').last; + final Finder expandedTitleClip = find.ancestor( + of: expandedTitle, + matching: find.byType(ClipRect), + ); + + // Default, fully expanded app bar. + expect(nestedScrollView.currentState?.outerController.offset, 0); + expect(nestedScrollView.currentState?.innerController.offset, 0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), expandedAppBarHeight); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + + // Scroll the expanded app bar partially out of view. + final Offset point1 = tester.getCenter(find.text('Item 5')); + await tester.dragFrom(point1, const Offset(0.0, -45.0)); + await tester.pump(); + expect(nestedScrollView.currentState?.outerController.offset, 45.0); + expect(nestedScrollView.currentState?.innerController.offset, 0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), expandedAppBarHeight - 45); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45); + + // Scroll so that it is completely collapsed. + await tester.dragFrom(point1, const Offset(0.0, -555.0)); + await tester.pump(); + expect(nestedScrollView.currentState?.outerController.offset, 88.0); + expect(nestedScrollView.currentState?.innerController.offset, 512.0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), collapsedAppBarHeight); + expect(tester.getSize(expandedTitleClip).height, 0); + + // Scroll back to fully expanded. + await tester.dragFrom(point1, const Offset(0.0, 600.0)); + await tester.pump(); + expect(nestedScrollView.currentState?.outerController.offset, 0); + expect(nestedScrollView.currentState?.innerController.offset, 0); + expect(find.byType(SliverAppBar), findsOneWidget); + expect(appBarHeight(tester), expandedAppBarHeight); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + }); } +double appBarHeight(WidgetTester tester) => tester.getSize(find.byType(AppBar, skipOffstage: false)).height; + class TestHeader extends SliverPersistentHeaderDelegate { const TestHeader({ this.key }); final Key? key;