Let CupertinoTabScaffold handle keyboard insets too (#25593)

diff --git a/packages/flutter/lib/src/cupertino/page_scaffold.dart b/packages/flutter/lib/src/cupertino/page_scaffold.dart
index 9fdf8f2..d9f0b07 100644
--- a/packages/flutter/lib/src/cupertino/page_scaffold.dart
+++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart
@@ -57,7 +57,7 @@
   /// 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 @@
           ? 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 @@
       // 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 @@
             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 40658c8..09c12b1 100644
--- a/packages/flutter/lib/src/cupertino/tab_scaffold.dart
+++ b/packages/flutter/lib/src/cupertino/tab_scaffold.dart
@@ -97,6 +97,8 @@
     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 @@
   /// 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 @@
   Widget build(BuildContext context) {
     final List<Widget> stacked = <Widget>[];
 
+    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 @@
           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 @@
 
     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 6620e00..7dc7ee0 100644
--- a/packages/flutter/test/cupertino/scaffold_test.dart
+++ b/packages/flutter/test/cupertino/scaffold_test.dart
@@ -42,6 +42,7 @@
 
     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 @@
           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 c256e72..206e6a8 100644
--- a/packages/flutter/test/cupertino/tab_scaffold_test.dart
+++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart
@@ -312,6 +312,89 @@
     ));
     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 }) {