Unverified Commit 5c4ffa13 authored by xster's avatar xster Committed by GitHub

Let translucent Cupertino bars have its scaffold children automatically pad their heights (#13194)

* Let lists automatically add sliver padding from media query. Translucent nav and tab bars leave behind media query paddings in scaffolds.

* tests

* const lint

* Rename base abstract class to generalized ObstructingPreferredSizeWidget
parent d957c8f0
......@@ -131,17 +131,21 @@ class CupertinoDemoTab1 extends StatelessWidget {
largeTitle: const Text('Colors'),
trailing: const ExitButton(),
),
new SliverList(
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Tab1RowItem(
index: index,
lastItem: index == 49,
color: colorItems[index],
colorName: colorNameItems[index],
);
},
childCount: 50,
new SliverPadding(
// Top media query padding already consumed by CupertinoSliverNavigationBar.
padding: MediaQuery.of(context).removePadding(removeTop: true).padding,
sliver: new SliverList(
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Tab1RowItem(
index: index,
lastItem: index == 49,
color: colorItems[index],
colorName: colorNameItems[index],
);
},
childCount: 50,
),
),
),
],
......@@ -271,7 +275,7 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
),
child: new ListView(
children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 80.0)),
const Padding(padding: const EdgeInsets.only(top: 16.0)),
new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new Row(
......@@ -396,7 +400,6 @@ class CupertinoDemoTab2 extends StatelessWidget {
),
child: new ListView(
children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 60.0)),
new Tab2Header(),
]..addAll(buildTab2Conversation()),
),
......@@ -686,7 +689,6 @@ List<Widget> buildTab2Conversation() {
),
],
),
const Padding(padding: const EdgeInsets.only(bottom: 80.0)),
];
}
......@@ -702,7 +704,7 @@ class CupertinoDemoTab3 extends StatelessWidget {
decoration: const BoxDecoration(color: const Color(0xFFEFEFF4)),
child: new ListView(
children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 100.0)),
const Padding(padding: const EdgeInsets.only(top: 32.0)),
new GestureDetector(
onTap: () {
Navigator.of(context, rootNavigator: true).push(
......
......@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'button.dart';
import 'colors.dart';
import 'icons.dart';
import 'page_scaffold.dart';
/// Standard iOS navigation bar height without the status bar.
const double _kNavBarPersistentHeight = 44.0;
......@@ -68,7 +69,7 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// [CupertinoNavigationBar].
/// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a
/// scrolling list and that supports iOS-11-style large titles.
class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget {
class CupertinoNavigationBar extends StatelessWidget implements ObstructingPreferredSizeWidget {
/// Creates a navigation bar in the iOS style.
const CupertinoNavigationBar({
Key key,
......@@ -116,11 +117,12 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
final Color actionsForegroundColor;
/// True if the navigation bar's background color has no transparency.
bool get opaque => backgroundColor.alpha == 0xFF;
@override
bool get fullObstruction => backgroundColor.alpha == 0xFF;
@override
Size get preferredSize {
return opaque ? const Size.fromHeight(_kNavBarPersistentHeight) : Size.zero;
return const Size.fromHeight(_kNavBarPersistentHeight);
}
@override
......
......@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'nav_bar.dart';
/// Implements a single iOS application page's layout.
///
......@@ -32,41 +31,52 @@ class CupertinoPageScaffold extends StatelessWidget {
///
/// If translucent, the main content may slide behind it.
/// Otherwise, the main content's top margin will be offset by its height.
///
/// The scaffold assumes the nav bar will consume the [MediaQuery] top padding.
// TODO(xster): document its page transition animation when ready
final PreferredSizeWidget navigationBar;
final ObstructingPreferredSizeWidget navigationBar;
/// Widget to show in the main content area.
///
/// Content can slide under the [navigationBar] when they're translucent.
/// Content can slide under the [navigationBar] when they're translucent with
/// a [MediaQuery] padding hinting the top obstructed area.
final Widget child;
@override
Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[];
Widget childWithMediaQuery = child;
double topPadding = 0.0;
Widget paddedContent = child;
if (navigationBar != null) {
topPadding += navigationBar.preferredSize.height;
// If the navigation bar has a preferred size, pad it and the OS status
// bar as well. Otherwise, let the content extend to the complete top
// of the page.
if (topPadding > 0.0) {
final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding;
topPadding += mediaQueryPadding.top;
childWithMediaQuery = new MediaQuery.removePadding(
context: context,
removeTop: true,
final MediaQueryData existingMediaQuery = MediaQuery.of(context);
// TODO(https://github.com/flutter/flutter/issues/12912):
// Use real size after partial layout instead of preferred size.
final double topPadding = navigationBar.preferredSize.height
+ existingMediaQuery.padding.top;
// If nav bar is opaquely obstructing, directly shift the main content
// down. If translucent, let main content draw behind nav bar but hint the
// obstructed area.
if (navigationBar.fullObstruction) {
paddedContent = new Padding(
padding: new EdgeInsets.only(top: topPadding),
child: child,
);
} else {
paddedContent = new MediaQuery(
data: existingMediaQuery.copyWith(
padding: existingMediaQuery.padding.copyWith(
top: topPadding,
),
),
child: child,
);
}
}
// The main content being at the bottom is added to the stack first.
stacked.add(new Padding(
padding: new EdgeInsets.only(top: topPadding),
child: childWithMediaQuery,
));
stacked.add(paddedContent);
if (navigationBar != null) {
stacked.add(new Positioned(
......@@ -84,4 +94,14 @@ class CupertinoPageScaffold extends StatelessWidget {
),
);
}
}
\ No newline at end of file
}
/// Widget that has a preferred size and reports whether it fully obstructs
/// widgets behind it.
abstract class ObstructingPreferredSizeWidget extends PreferredSizeWidget {
/// If true, this widget fully obstructs widgets behind it by the specified
/// size.
///
/// If false, this widget partially obstructs.
bool get fullObstruction;
}
......@@ -117,7 +117,8 @@ class CupertinoTabScaffold extends StatefulWidget {
/// When the tab becomes inactive, its content is still cached in the widget
/// tree [Offstage] and its animations disabled.
///
/// Content can slide under the [tabBar] when it's translucent.
/// Content can slide under the [tabBar] when it's translucent with a
/// [MediaQuery] padding hinting the bottom obstructed area.
final IndexedWidgetBuilder tabBuilder;
@override
......@@ -131,18 +132,43 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[];
// The main content being at the bottom is added to the stack first.
stacked.add(
new Padding(
padding: new EdgeInsets.only(bottom: widget.tabBar.opaque ? widget.tabBar.preferredSize.height : 0.0),
child: new _TabView(
currentTabIndex: _currentPage,
tabNumber: widget.tabBar.items.length,
tabBuilder: widget.tabBuilder,
)
),
Widget content = new _TabView(
currentTabIndex: _currentPage,
tabNumber: widget.tabBar.items.length,
tabBuilder: widget.tabBuilder,
);
if (widget.tabBar != null) {
final MediaQueryData existingMediaQuery = MediaQuery.of(context);
// TODO(https://github.com/flutter/flutter/issues/12912):
// Use real size after partial layout instead of preferred size.
final double bottomPadding = widget.tabBar.preferredSize.height
+ existingMediaQuery.padding.bottom;
// If tab bar opaque, directly stop the main content higher. If
// translucent, let main content draw behind the tab bar but hint the
// obstructed area.
if (widget.tabBar.opaque) {
content = new Padding(
padding: new EdgeInsets.only(bottom: bottomPadding),
child: content,
);
} else {
content = new MediaQuery(
data: existingMediaQuery.copyWith(
padding: existingMediaQuery.padding.copyWith(
bottom: bottomPadding,
),
),
child: content,
);
}
}
// The main content being at the bottom is added to the stack first.
stacked.add(content);
if (widget.tabBar != null) {
stacked.add(new Align(
alignment: Alignment.bottomCenter,
......
......@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'media_query.dart';
import 'primary_scroll_controller.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
......@@ -372,8 +373,33 @@ abstract class BoxScrollView extends ScrollView {
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
if (padding != null)
sliver = new SliverPadding(padding: padding, sliver: sliver);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = new MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver = new SliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[ sliver ];
}
......
......@@ -78,6 +78,66 @@ void main() {
expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0);
});
testWidgets('Contents have automatic sliver padding between translucent bars', (WidgetTester tester) async {
final Container content = new Container(height: 600.0, width: 600.0);
await tester.pumpWidget(
new WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new MediaQuery(
data: const MediaQueryData(
padding: const EdgeInsets.symmetric(vertical: 20.0),
),
child: new CupertinoTabScaffold(
tabBar: new CupertinoTabBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 1'),
),
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 2'),
),
],
),
tabBuilder: (BuildContext context, int index) {
return index == 0
? new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: const Text('Title'),
),
child: new ListView(
children: <Widget>[
content,
],
),
)
: new Stack();
}
),
);
},
);
},
),
);
// List content automatically padded by nav bar and top media query padding.
expect(tester.getTopLeft(find.byWidget(content)).dy, 20.0 + 44.0);
// Overscroll to the bottom.
await tester.drag(find.byWidget(content), const Offset(0.0, -400.0));
await tester.pump(const Duration(seconds: 1));
// List content automatically padded by tab bar and bottom media query padding.
expect(tester.getBottomLeft(find.byWidget(content)).dy, 600 - 20.0 - 50.0);
});
testWidgets('iOS independent tab navigation', (WidgetTester tester) async {
// A full on iOS information architecture app with 2 tabs, and 2 pages
// in each with independent navigation states.
......
......@@ -265,4 +265,32 @@ void main() {
expect(delegate.log, equals(<String>['didFinishLayout firstIndex=2 lastIndex=5']));
delegate.log.clear();
});
testWidgets('ListView automatically pad MediaQuery on axis', (WidgetTester tester) async {
EdgeInsets innerMediaQueryPadding;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(
padding: const EdgeInsets.all(30.0),
),
child: new ListView(
children: <Widget>[
const Text('top', textDirection: TextDirection.ltr),
new Builder(builder: (BuildContext context) {
innerMediaQueryPadding = MediaQuery.of(context).padding;
return new Container();
}),
],
),
),
),
);
// Automatically apply the top/bottom padding into sliver.
expect(tester.getTopLeft(find.text('top')).dy, 30.0);
// Leave left/right padding as is for children.
expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.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