Unverified Commit ecf50418 authored by xster's avatar xster Committed by GitHub

Let translucent Cupertino bars have its scaffold children automatically pad...

Let translucent Cupertino bars have its scaffold children automatically pad their heights - second try (#13440)

* 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

* review

* More docs and comments from #13317
parent 80c69199
...@@ -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()),
), ),
...@@ -664,7 +667,6 @@ List<Widget> buildTab2Conversation() { ...@@ -664,7 +667,6 @@ List<Widget> buildTab2Conversation() {
const Tab2ConversationRow( const Tab2ConversationRow(
text: "What's that?", text: "What's that?",
), ),
const Padding(padding: const EdgeInsets.only(bottom: 80.0)),
]; ];
} }
...@@ -680,7 +682,7 @@ class CupertinoDemoTab3 extends StatelessWidget { ...@@ -680,7 +682,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,54 @@ class CupertinoPageScaffold extends StatelessWidget { ...@@ -32,41 +31,54 @@ 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.
/// In that case, the child's [BuildContext]'s [MediaQuery] will have a
/// top padding indicating the area of obstructing overlap from the
/// [navigationBar].
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(xster): Use real size after partial layout instead of preferred size.
// of the page. // https://github.com/flutter/flutter/issues/12912
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 +97,16 @@ class CupertinoPageScaffold extends StatelessWidget { ...@@ -85,3 +97,16 @@ class CupertinoPageScaffold extends StatelessWidget {
); );
} }
} }
/// Widget that has a preferred size and reports whether it fully obstructs
/// widgets behind it.
///
/// Used by [CupertinoPageScaffold] to either shift away fully obstructed content
/// or provide a padding guide to partially obstructed content.
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,10 @@ class CupertinoTabScaffold extends StatefulWidget { ...@@ -117,7 +117,10 @@ 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 they're translucent.
/// In that case, the child's [BuildContext]'s [MediaQuery] will have a
/// bottom padding indicating the area of obstructing overlap from the
/// [tabBar].
final IndexedWidgetBuilder tabBuilder; final IndexedWidgetBuilder tabBuilder;
@override @override
...@@ -131,17 +134,42 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> { ...@@ -131,17 +134,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(xster): Use real size after partial layout instead of preferred size.
// https://github.com/flutter/flutter/issues/12912
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 ];
} }
...@@ -465,6 +491,10 @@ abstract class BoxScrollView extends ScrollView { ...@@ -465,6 +491,10 @@ abstract class BoxScrollView extends ScrollView {
/// [CustomScrollView.slivers] property instead of the list itself, and having /// [CustomScrollView.slivers] property instead of the list itself, and having
/// the [SliverList] instead be a child of the [SliverPadding]. /// the [SliverList] instead be a child of the [SliverPadding].
/// ///
/// By default, [ListView] will automatically pad the list's scrollable
/// extremities to avoid partial obstructions indicated by [MediaQuery]'s
/// padding. To avoid this behavior, override with a zero [padding] property.
///
/// Once code has been ported to use [CustomScrollView], other slivers, such as /// Once code has been ported to use [CustomScrollView], other slivers, such as
/// [SliverGrid] or [SliverAppBar], can be put in the [CustomScrollView.slivers] /// [SliverGrid] or [SliverAppBar], can be put in the [CustomScrollView.slivers]
/// list. /// list.
...@@ -742,6 +772,10 @@ class ListView extends BoxScrollView { ...@@ -742,6 +772,10 @@ class ListView extends BoxScrollView {
/// [CustomScrollView.slivers] property instead of the grid itself, and having /// [CustomScrollView.slivers] property instead of the grid itself, and having
/// the [SliverGrid] instead be a child of the [SliverPadding]. /// the [SliverGrid] instead be a child of the [SliverPadding].
/// ///
/// By default, [ListView] will automatically pad the list's scrollable
/// extremities to avoid partial obstructions indicated by [MediaQuery]'s
/// padding. To avoid this behavior, override with a zero [padding] property.
///
/// Once code has been ported to use [CustomScrollView], other slivers, such as /// Once code has been ported to use [CustomScrollView], other slivers, such as
/// [SliverList] or [SliverAppBar], can be put in the [CustomScrollView.slivers] /// [SliverList] or [SliverAppBar], can be put in the [CustomScrollView.slivers]
/// list. /// list.
......
...@@ -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