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,7 +131,10 @@ class CupertinoDemoTab1 extends StatelessWidget { ...@@ -131,7 +131,10 @@ class CupertinoDemoTab1 extends StatelessWidget {
largeTitle: const Text('Colors'), largeTitle: const Text('Colors'),
trailing: const ExitButton(), trailing: const ExitButton(),
), ),
new SliverList( new SliverPadding(
// Top media query padding already consumed by CupertinoSliverNavigationBar.
padding: MediaQuery.of(context).removePadding(removeTop: true).padding,
sliver: new SliverList(
delegate: new SliverChildBuilderDelegate( delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
return new Tab1RowItem( return new Tab1RowItem(
...@@ -144,6 +147,7 @@ class CupertinoDemoTab1 extends StatelessWidget { ...@@ -144,6 +147,7 @@ class CupertinoDemoTab1 extends StatelessWidget {
childCount: 50, childCount: 50,
), ),
), ),
),
], ],
), ),
); );
...@@ -271,7 +275,7 @@ class Tab1ItemPageState extends State<Tab1ItemPage> { ...@@ -271,7 +275,7 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
), ),
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 80.0)), const Padding(padding: const EdgeInsets.only(top: 16.0)),
new Padding( new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new Row( child: new Row(
...@@ -396,7 +400,6 @@ class CupertinoDemoTab2 extends StatelessWidget { ...@@ -396,7 +400,6 @@ class CupertinoDemoTab2 extends StatelessWidget {
), ),
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 60.0)),
new Tab2Header(), new Tab2Header(),
]..addAll(buildTab2Conversation()), ]..addAll(buildTab2Conversation()),
), ),
...@@ -686,7 +689,6 @@ List<Widget> 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 { ...@@ -702,7 +704,7 @@ class CupertinoDemoTab3 extends StatelessWidget {
decoration: const BoxDecoration(color: const Color(0xFFEFEFF4)), decoration: const BoxDecoration(color: const Color(0xFFEFEFF4)),
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 100.0)), const Padding(padding: const EdgeInsets.only(top: 32.0)),
new GestureDetector( new GestureDetector(
onTap: () { onTap: () {
Navigator.of(context, rootNavigator: true).push( Navigator.of(context, rootNavigator: true).push(
......
...@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'button.dart'; import 'button.dart';
import 'colors.dart'; import 'colors.dart';
import 'icons.dart'; import 'icons.dart';
import 'page_scaffold.dart';
/// Standard iOS navigation bar height without the status bar. /// Standard iOS navigation bar height without the status bar.
const double _kNavBarPersistentHeight = 44.0; const double _kNavBarPersistentHeight = 44.0;
...@@ -68,7 +69,7 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle( ...@@ -68,7 +69,7 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// [CupertinoNavigationBar]. /// [CupertinoNavigationBar].
/// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a /// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a
/// scrolling list and that supports iOS-11-style large titles. /// 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. /// Creates a navigation bar in the iOS style.
const CupertinoNavigationBar({ const CupertinoNavigationBar({
Key key, Key key,
...@@ -116,11 +117,12 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid ...@@ -116,11 +117,12 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
final Color actionsForegroundColor; final Color actionsForegroundColor;
/// True if the navigation bar's background color has no transparency. /// 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 @override
Size get preferredSize { Size get preferredSize {
return opaque ? const Size.fromHeight(_kNavBarPersistentHeight) : Size.zero; return const Size.fromHeight(_kNavBarPersistentHeight);
} }
@override @override
......
...@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; ...@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'nav_bar.dart';
/// Implements a single iOS application page's layout. /// Implements a single iOS application page's layout.
/// ///
...@@ -32,41 +31,52 @@ class CupertinoPageScaffold extends StatelessWidget { ...@@ -32,41 +31,52 @@ class CupertinoPageScaffold extends StatelessWidget {
/// ///
/// If translucent, the main content may slide behind it. /// If translucent, the main content may slide behind it.
/// Otherwise, the main content's top margin will be offset by its height. /// 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 // TODO(xster): document its page transition animation when ready
final PreferredSizeWidget navigationBar; final ObstructingPreferredSizeWidget navigationBar;
/// Widget to show in the main content area. /// 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; final Widget child;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[]; final List<Widget> stacked = <Widget>[];
Widget childWithMediaQuery = child;
double topPadding = 0.0; Widget paddedContent = child;
if (navigationBar != null) { if (navigationBar != null) {
topPadding += navigationBar.preferredSize.height; final MediaQueryData existingMediaQuery = MediaQuery.of(context);
// 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 // TODO(https://github.com/flutter/flutter/issues/12912):
// of the page. // Use real size after partial layout instead of preferred size.
if (topPadding > 0.0) { final double topPadding = navigationBar.preferredSize.height
final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding; + existingMediaQuery.padding.top;
topPadding += mediaQueryPadding.top;
childWithMediaQuery = new MediaQuery.removePadding( // If nav bar is opaquely obstructing, directly shift the main content
context: context, // down. If translucent, let main content draw behind nav bar but hint the
removeTop: true, // 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, child: child,
); );
} }
} }
// 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.
stacked.add(new Padding( stacked.add(paddedContent);
padding: new EdgeInsets.only(top: topPadding),
child: childWithMediaQuery,
));
if (navigationBar != null) { if (navigationBar != null) {
stacked.add(new Positioned( stacked.add(new Positioned(
...@@ -85,3 +95,13 @@ class CupertinoPageScaffold extends StatelessWidget { ...@@ -85,3 +95,13 @@ class CupertinoPageScaffold extends StatelessWidget {
); );
} }
} }
/// 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 { ...@@ -117,7 +117,8 @@ class CupertinoTabScaffold extends StatefulWidget {
/// When the tab becomes inactive, its content is still cached in the widget /// When the tab becomes inactive, its content is still cached in the widget
/// tree [Offstage] and its animations disabled. /// 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; final IndexedWidgetBuilder tabBuilder;
@override @override
...@@ -131,17 +132,42 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> { ...@@ -131,17 +132,42 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[]; final List<Widget> stacked = <Widget>[];
// The main content being at the bottom is added to the stack first. Widget content = new _TabView(
stacked.add(
new Padding(
padding: new EdgeInsets.only(bottom: widget.tabBar.opaque ? widget.tabBar.preferredSize.height : 0.0),
child: new _TabView(
currentTabIndex: _currentPage, currentTabIndex: _currentPage,
tabNumber: widget.tabBar.items.length, tabNumber: widget.tabBar.items.length,
tabBuilder: widget.tabBuilder, 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) { if (widget.tabBar != null) {
stacked.add(new Align( stacked.add(new Align(
......
...@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart';
import 'primary_scroll_controller.dart'; import 'primary_scroll_controller.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
...@@ -372,8 +373,33 @@ abstract class BoxScrollView extends ScrollView { ...@@ -372,8 +373,33 @@ abstract class BoxScrollView extends ScrollView {
@override @override
List<Widget> buildSlivers(BuildContext context) { List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context); Widget sliver = buildChildLayout(context);
if (padding != null) EdgeInsetsGeometry effectivePadding = padding;
sliver = new SliverPadding(padding: padding, sliver: sliver); 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 ]; return <Widget>[ sliver ];
} }
......
...@@ -78,6 +78,66 @@ void main() { ...@@ -78,6 +78,66 @@ void main() {
expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0); 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 { testWidgets('iOS independent tab navigation', (WidgetTester tester) async {
// A full on iOS information architecture app with 2 tabs, and 2 pages // A full on iOS information architecture app with 2 tabs, and 2 pages
// in each with independent navigation states. // in each with independent navigation states.
......
...@@ -265,4 +265,32 @@ void main() { ...@@ -265,4 +265,32 @@ void main() {
expect(delegate.log, equals(<String>['didFinishLayout firstIndex=2 lastIndex=5'])); expect(delegate.log, equals(<String>['didFinishLayout firstIndex=2 lastIndex=5']));
delegate.log.clear(); 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