Unverified Commit 34a85b39 authored by xster's avatar xster Committed by GitHub

Fix CupertinoTabView tree re-shape on view inset change (#29024)

parent 3af88c55
...@@ -194,14 +194,12 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> { ...@@ -194,14 +194,12 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
tabNumber: widget.tabBar.items.length, tabNumber: widget.tabBar.items.length,
tabBuilder: widget.tabBuilder, tabBuilder: widget.tabBuilder,
); );
EdgeInsets contentPadding = EdgeInsets.zero;
if (widget.resizeToAvoidBottomInset) { if (widget.resizeToAvoidBottomInset) {
// Remove the view inset and add it back as a padding in the inner content. // Remove the view inset and add it back as a padding in the inner content.
newMediaQuery = newMediaQuery.removeViewInsets(removeBottom: true); newMediaQuery = newMediaQuery.removeViewInsets(removeBottom: true);
content = Padding( contentPadding = EdgeInsets.only(bottom: existingMediaQuery.viewInsets.bottom);
padding: EdgeInsets.only(bottom: existingMediaQuery.viewInsets.bottom),
child: content,
);
} }
if (widget.tabBar != null && if (widget.tabBar != null &&
...@@ -219,10 +217,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> { ...@@ -219,10 +217,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
// translucent, let main content draw behind the tab bar but hint the // translucent, let main content draw behind the tab bar but hint the
// obstructed area. // obstructed area.
if (widget.tabBar.opaque(context)) { if (widget.tabBar.opaque(context)) {
content = Padding( contentPadding = EdgeInsets.only(bottom: bottomPadding);
padding: EdgeInsets.only(bottom: bottomPadding),
child: content,
);
} else { } else {
newMediaQuery = newMediaQuery.copyWith( newMediaQuery = newMediaQuery.copyWith(
padding: newMediaQuery.padding.copyWith( padding: newMediaQuery.padding.copyWith(
...@@ -234,7 +229,10 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> { ...@@ -234,7 +229,10 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
content = MediaQuery( content = MediaQuery(
data: newMediaQuery, data: newMediaQuery,
child: Padding(
padding: contentPadding,
child: content, child: content,
),
); );
// The main content being at the bottom is added to the stack first. // The main content being at the bottom is added to the stack first.
......
...@@ -3,5 +3,8 @@ ...@@ -3,5 +3,8 @@
Avoid importing the Material 'package:flutter/material.dart' in these tests as Avoid importing the Material 'package:flutter/material.dart' in these tests as
we're trying to test the Cupertino package in standalone scenarios. we're trying to test the Cupertino package in standalone scenarios.
Some tests may be replicated in the Material tests when Material reuses The 'material' subdirectory contains tests for cross-interactions of Material
Cupertino widgets in hybridized apps.
Some tests may also be replicated in the Material tests when Material reuses
Cupertino components on iOS such as page transitions and text editing. Cupertino components on iOS such as page transitions and text editing.
# Tests for the Cupertino+Material mixed usages
In this package, we test for interactions between Material and Cupertino
widgets.
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../painting/mocks_for_image_cache.dart';
List<int> selectedTabs;
void main() {
setUp(() {
selectedTabs = <int>[];
});
testWidgets('Last tab gets focus', (WidgetTester tester) async {
// 2 nodes for 2 tabs
final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return TextField(
focusNode: focusNodes[index],
autofocus: true,
);
},
),
),
),
);
expect(focusNodes[0].hasFocus, isTrue);
await tester.tap(find.text('Tab 2'));
await tester.pump();
expect(focusNodes[0].hasFocus, isFalse);
expect(focusNodes[1].hasFocus, isTrue);
await tester.tap(find.text('Tab 1'));
await tester.pump();
expect(focusNodes[0].hasFocus, isTrue);
expect(focusNodes[1].hasFocus, isFalse);
});
testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(), FocusNode(), FocusNode(), FocusNode(),
];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[
TextField(
focusNode: focusNodes[index * 2],
decoration: const InputDecoration(
hintText: 'TextField 1',
),
),
TextField(
focusNode: focusNodes[index * 2 + 1],
decoration: const InputDecoration(
hintText: 'TextField 2',
),
),
],
);
},
),
),
),
);
expect(
focusNodes.any((FocusNode node) => node.hasFocus),
isFalse,
);
await tester.tap(find.widgetWithText(TextField, 'TextField 2'));
expect(
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
1,
);
await tester.tap(find.text('Tab 2'));
await tester.pump();
await tester.tap(find.widgetWithText(TextField, 'TextField 1'));
expect(
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
2,
);
await tester.tap(find.text('Tab 1'));
await tester.pump();
// Upon going back to tab 1, the item it tab 1 that previously had the focus
// (TextField 2) gets it back.
expect(
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
1,
);
});
testWidgets('Tab bar respects themes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const Placeholder();
},
),
),
);
BoxDecoration tabDecoration = tester.widget<DecoratedBox>(find.descendant(
of: find.byType(CupertinoTabBar),
matching: find.byType(DecoratedBox),
)).decoration;
expect(tabDecoration.color, const Color(0xCCF8F8F8));
await tester.tap(find.text('Tab 2'));
await tester.pump();
// Pump again but with dark theme.
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(
brightness: Brightness.dark,
primaryColor: CupertinoColors.destructiveRed,
),
home: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const Placeholder();
},
),
),
);
tabDecoration = tester.widget<DecoratedBox>(find.descendant(
of: find.byType(CupertinoTabBar),
matching: find.byType(DecoratedBox),
)).decoration;
expect(tabDecoration.color, const Color(0xB7212121));
final RichText tab1 = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
// Tab 2 should still be selected after changing theme.
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
final RichText tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.destructiveRed);
});
testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async {
// Regression testing for https://github.com/flutter/flutter/issues/28457.
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 0),
),
child: MaterialApp(
home: Material(
child: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const TextField();
},
),
),
),
),
);
final EditableTextState editableState = tester.state<EditableTextState>(find.byType(EditableText));
await tester.enterText(find.byType(TextField), "don't lose me");
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 100),
),
child: MaterialApp(
home: Material(
child: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const TextField();
},
),
),
),
),
);
// The exact same state instance is still there.
expect(tester.state<EditableTextState>(find.byType(EditableText)), editableState);
expect(find.text("don't lose me"), findsOneWidget);
});
}
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
return CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 1'),
),
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 2'),
),
],
currentIndex: selectedTab,
onTap: (int newTab) => selectedTabs.add(newTab),
);
}
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../painting/mocks_for_image_cache.dart'; import '../painting/mocks_for_image_cache.dart';
...@@ -111,18 +110,16 @@ void main() { ...@@ -111,18 +110,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
home: Material( home: CupertinoTabScaffold(
child: CupertinoTabScaffold(
tabBar: _buildTabBar(), tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) { tabBuilder: (BuildContext context, int index) {
return TextField( return CupertinoTextField(
focusNode: focusNodes[index], focusNode: focusNodes[index],
autofocus: true, autofocus: true,
); );
}, },
), ),
), ),
),
); );
expect(focusNodes[0].hasFocus, isTrue); expect(focusNodes[0].hasFocus, isTrue);
...@@ -147,30 +144,24 @@ void main() { ...@@ -147,30 +144,24 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
home: Material( home: CupertinoTabScaffold(
child: CupertinoTabScaffold(
tabBar: _buildTabBar(), tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) { tabBuilder: (BuildContext context, int index) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
TextField( CupertinoTextField(
focusNode: focusNodes[index * 2], focusNode: focusNodes[index * 2],
decoration: const InputDecoration( placeholder: 'TextField 1',
hintText: 'TextField 1',
), ),
), CupertinoTextField(
TextField(
focusNode: focusNodes[index * 2 + 1], focusNode: focusNodes[index * 2 + 1],
decoration: const InputDecoration( placeholder: 'TextField 2',
hintText: 'TextField 2',
),
), ),
], ],
); );
}, },
), ),
), ),
),
); );
expect( expect(
...@@ -178,7 +169,7 @@ void main() { ...@@ -178,7 +169,7 @@ void main() {
isFalse, isFalse,
); );
await tester.tap(find.widgetWithText(TextField, 'TextField 2')); await tester.tap(find.widgetWithText(CupertinoTextField, 'TextField 2'));
expect( expect(
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
...@@ -188,7 +179,7 @@ void main() { ...@@ -188,7 +179,7 @@ void main() {
await tester.tap(find.text('Tab 2')); await tester.tap(find.text('Tab 2'));
await tester.pump(); await tester.pump();
await tester.tap(find.widgetWithText(TextField, 'TextField 1')); await tester.tap(find.widgetWithText(CupertinoTextField, 'TextField 1'));
expect( expect(
focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)),
...@@ -471,6 +462,50 @@ void main() { ...@@ -471,6 +462,50 @@ void main() {
expect(find.text('Page 2', skipOffstage: false), findsNothing); expect(find.text('Page 2', skipOffstage: false), findsNothing);
expect(find.text('Page 4', skipOffstage: false), findsNothing); expect(find.text('Page 4', skipOffstage: false), findsNothing);
}); });
testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async {
// Regression testing for https://github.com/flutter/flutter/issues/28457.
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 0),
),
child: CupertinoApp(
home: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const CupertinoTextField();
},
),
),
),
);
final EditableTextState editableState = tester.state<EditableTextState>(find.byType(EditableText));
await tester.enterText(find.byType(CupertinoTextField), "don't lose me");
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 100),
),
child: CupertinoApp(
home: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const CupertinoTextField();
},
),
),
),
);
// The exact same state instance is still there.
expect(tester.state<EditableTextState>(find.byType(EditableText)), editableState);
expect(find.text("don't lose me"), findsOneWidget);
});
} }
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) { CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment