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,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()),
),
......@@ -664,7 +667,6 @@ List<Widget> buildTab2Conversation() {
const Tab2ConversationRow(
text: "What's that?",
),
const Padding(padding: const EdgeInsets.only(bottom: 80.0)),
];
}
......@@ -680,7 +682,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,54 @@ 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.
/// 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;
@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(xster): Use real size after partial layout instead of preferred size.
// https://github.com/flutter/flutter/issues/12912
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 +96,17 @@ 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.
///
/// 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 {
/// 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 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;
@override
......@@ -131,18 +134,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(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) {
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 ];
}
......@@ -465,6 +491,10 @@ abstract class BoxScrollView extends ScrollView {
/// [CustomScrollView.slivers] property instead of the list itself, and having
/// 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
/// [SliverGrid] or [SliverAppBar], can be put in the [CustomScrollView.slivers]
/// list.
......@@ -742,6 +772,10 @@ class ListView extends BoxScrollView {
/// [CustomScrollView.slivers] property instead of the grid itself, and having
/// 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
/// [SliverList] or [SliverAppBar], can be put in the [CustomScrollView.slivers]
/// list.
......
......@@ -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