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
This commit is contained in:
parent
d957c8f040
commit
5c4ffa13a6
@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user