From d1ec126a3ccbe877ad3df5c6bc31ea3f4c4cfa0c Mon Sep 17 00:00:00 2001 From: xster Date: Thu, 20 Dec 2018 15:02:53 -0800 Subject: [PATCH] Let CupertinoTabScaffold handle keyboard insets too (#25593) --- .../lib/src/cupertino/page_scaffold.dart | 20 ++++- .../lib/src/cupertino/tab_scaffold.dart | 51 ++++++++++-- .../flutter/test/cupertino/scaffold_test.dart | 11 ++- .../test/cupertino/tab_scaffold_test.dart | 83 +++++++++++++++++++ 4 files changed, 151 insertions(+), 14 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/page_scaffold.dart b/packages/flutter/lib/src/cupertino/page_scaffold.dart index 9fdf8f2390..d9f0b07412 100644 --- a/packages/flutter/lib/src/cupertino/page_scaffold.dart +++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart @@ -57,7 +57,7 @@ class CupertinoPageScaffold extends StatelessWidget { /// scaffold, the body can be resized to avoid overlapping the keyboard, which /// prevents widgets inside the body from being obscured by the keyboard. /// - /// Defaults to true. + /// Defaults to true and cannot be null. final bool resizeToAvoidBottomInset; @override @@ -78,6 +78,12 @@ class CupertinoPageScaffold extends StatelessWidget { ? existingMediaQuery.viewInsets.bottom : 0.0; + final EdgeInsets newViewInsets = resizeToAvoidBottomInset + // The insets are consumed by the scaffolds and no longer exposed to + // the descendant subtree. + ? existingMediaQuery.viewInsets.copyWith(bottom: 0.0) + : existingMediaQuery.viewInsets; + final bool fullObstruction = navigationBar.fullObstruction ?? CupertinoTheme.of(context).barBackgroundColor.alpha == 0xFF; @@ -85,9 +91,14 @@ class CupertinoPageScaffold extends StatelessWidget { // down. If translucent, let main content draw behind navigation bar but hint the // obstructed area. if (fullObstruction) { - paddedContent = Padding( - padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), - child: child, + paddedContent = MediaQuery( + data: existingMediaQuery.copyWith( + viewInsets: newViewInsets, + ), + child: Padding( + padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), + child: child, + ), ); } else { paddedContent = MediaQuery( @@ -95,6 +106,7 @@ class CupertinoPageScaffold extends StatelessWidget { padding: existingMediaQuery.padding.copyWith( top: topPadding, ), + viewInsets: newViewInsets, ), child: Padding( padding: EdgeInsets.only(bottom: bottomPadding), diff --git a/packages/flutter/lib/src/cupertino/tab_scaffold.dart b/packages/flutter/lib/src/cupertino/tab_scaffold.dart index 40658c86ca..09c12b1257 100644 --- a/packages/flutter/lib/src/cupertino/tab_scaffold.dart +++ b/packages/flutter/lib/src/cupertino/tab_scaffold.dart @@ -97,6 +97,8 @@ class CupertinoTabScaffold extends StatefulWidget { Key key, @required this.tabBar, @required this.tabBuilder, + this.backgroundColor, + this.resizeToAvoidBottomInset = true, }) : assert(tabBar != null), assert(tabBuilder != null), super(key: key); @@ -138,6 +140,20 @@ class CupertinoTabScaffold extends StatefulWidget { /// Must not be null. final IndexedWidgetBuilder tabBuilder; + /// The color of the widget that underlies the entire scaffold. + /// + /// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null. + final Color backgroundColor; + + /// Whether the [child] should size itself to avoid the window's bottom inset. + /// + /// For example, if there is an onscreen keyboard displayed above the + /// scaffold, the body can be resized to avoid overlapping the keyboard, which + /// prevents widgets inside the body from being obscured by the keyboard. + /// + /// Defaults to true and cannot be null. + final bool resizeToAvoidBottomInset; + @override _CupertinoTabScaffoldState createState() => _CupertinoTabScaffoldState(); } @@ -163,15 +179,30 @@ class _CupertinoTabScaffoldState extends State { Widget build(BuildContext context) { final List stacked = []; + final MediaQueryData existingMediaQuery = MediaQuery.of(context); + MediaQueryData newMediaQuery = MediaQuery.of(context); + Widget content = _TabSwitchingView( currentTabIndex: _currentPage, tabNumber: widget.tabBar.items.length, tabBuilder: widget.tabBuilder, ); - if (widget.tabBar != null) { - final MediaQueryData existingMediaQuery = MediaQuery.of(context); + if (widget.resizeToAvoidBottomInset) { + // Remove the view inset and add it back as a padding in the inner content. + newMediaQuery = newMediaQuery.removeViewInsets(removeBottom: true); + content = Padding( + padding: EdgeInsets.only(bottom: existingMediaQuery.viewInsets.bottom), + child: content, + ); + } + if (widget.tabBar != null && + // Only pad the content with the height of the tab bar if the tab + // isn't already entirely obstructed by a keyboard or other view insets. + // Don't double pad. + (!widget.resizeToAvoidBottomInset || + widget.tabBar.preferredSize.height > existingMediaQuery.viewInsets.bottom)) { // TODO(xster): Use real size after partial layout instead of preferred size. // https://github.com/flutter/flutter/issues/12912 final double bottomPadding = @@ -186,17 +217,19 @@ class _CupertinoTabScaffoldState extends State { child: content, ); } else { - content = MediaQuery( - data: existingMediaQuery.copyWith( - padding: existingMediaQuery.padding.copyWith( - bottom: bottomPadding, - ), + newMediaQuery = newMediaQuery.copyWith( + padding: newMediaQuery.padding.copyWith( + bottom: bottomPadding, ), - child: content, ); } } + content = MediaQuery( + data: newMediaQuery, + child: content, + ); + // The main content being at the bottom is added to the stack first. stacked.add(content); @@ -222,7 +255,7 @@ class _CupertinoTabScaffoldState extends State { return DecoratedBox( decoration: BoxDecoration( - color: CupertinoTheme.of(context).scaffoldBackgroundColor + color: widget.backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor, ), child: Stack( children: stacked, diff --git a/packages/flutter/test/cupertino/scaffold_test.dart b/packages/flutter/test/cupertino/scaffold_test.dart index 6620e0018f..7dc7ee02cf 100644 --- a/packages/flutter/test/cupertino/scaffold_test.dart +++ b/packages/flutter/test/cupertino/scaffold_test.dart @@ -42,6 +42,7 @@ void main() { expect(tester.getSize(find.byType(Container)).height, 600.0 - 44.0 - 100.0); + BuildContext childContext; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( @@ -50,12 +51,20 @@ void main() { navigationBar: const CupertinoNavigationBar( middle: Text('Transparent'), ), - child: Container(), + child: Builder( + builder: (BuildContext context) { + childContext = context; + return Container(); + }, + ), ), ), )); expect(tester.getSize(find.byType(Container)).height, 600.0 - 100.0); + // The shouldn't see a media query view inset because it was consumed by + // the scaffold. + expect(MediaQuery.of(childContext).viewInsets.bottom, 0); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/cupertino/tab_scaffold_test.dart b/packages/flutter/test/cupertino/tab_scaffold_test.dart index c256e72c12..206e6a8d60 100644 --- a/packages/flutter/test/cupertino/tab_scaffold_test.dart +++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart @@ -312,6 +312,89 @@ void main() { )); expect(tab2.text.style.color, CupertinoColors.destructiveRed); }); + + testWidgets('Tab contents are padded when there are view insets', (WidgetTester tester) async { + BuildContext innerContext; + + await tester.pumpWidget( + CupertinoApp( + home: MediaQuery( + data: const MediaQueryData( + viewInsets: EdgeInsets.only(bottom: 200), + ), + child: CupertinoTabScaffold( + tabBar: _buildTabBar(), + tabBuilder: (BuildContext context, int index) { + innerContext = context; + return const Placeholder(); + }, + ), + ), + ), + ); + + expect(tester.getRect(find.byType(Placeholder)), Rect.fromLTWH(0, 0, 800, 400)); + // Don't generate more media query padding from the translucent bottom + // tab since the tab is behind the keyboard now. + expect(MediaQuery.of(innerContext).padding.bottom, 0); + }); + + testWidgets('Tab contents are not inset when resizeToAvoidBottomInset overriden', (WidgetTester tester) async { + BuildContext innerContext; + + await tester.pumpWidget( + CupertinoApp( + home: MediaQuery( + data: const MediaQueryData( + viewInsets: EdgeInsets.only(bottom: 200), + ), + child: CupertinoTabScaffold( + resizeToAvoidBottomInset: false, + tabBar: _buildTabBar(), + tabBuilder: (BuildContext context, int index) { + innerContext = context; + return const Placeholder(); + } + ), + ), + ), + ); + + expect(tester.getRect(find.byType(Placeholder)), Rect.fromLTWH(0, 0, 800, 600)); + // Media query padding shows up in the inner content because it wasn't masked + // by the view inset. + expect(MediaQuery.of(innerContext).padding.bottom, 50); + }); + + testWidgets('Tab and page scaffolds do not double stack view insets', (WidgetTester tester) async { + BuildContext innerContext; + + await tester.pumpWidget( + CupertinoApp( + home: MediaQuery( + data: const MediaQueryData( + viewInsets: EdgeInsets.only(bottom: 200), + ), + child: CupertinoTabScaffold( + tabBar: _buildTabBar(), + tabBuilder: (BuildContext context, int index) { + return CupertinoPageScaffold( + child: Builder( + builder: (BuildContext context) { + innerContext = context; + return const Placeholder(); + }, + ), + ); + }, + ), + ), + ), + ); + + expect(tester.getRect(find.byType(Placeholder)), Rect.fromLTWH(0, 0, 800, 400)); + expect(MediaQuery.of(innerContext).padding.bottom, 0); + }); } CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {