From 35c2267ffe7e8c82030e896fa81bd67fc1d9cfd5 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 6 Feb 2018 14:28:58 -0800 Subject: [PATCH] Add SliverSafeArea widget (#14499) A SafeArea-like widget that applies a SliverPadding instead of a Padding. --- .../flutter/lib/src/widgets/safe_area.dart | 87 +++++++ .../flutter/test/widgets/safe_area_test.dart | 217 +++++++++++++----- 2 files changed, 250 insertions(+), 54 deletions(-) diff --git a/packages/flutter/lib/src/widgets/safe_area.dart b/packages/flutter/lib/src/widgets/safe_area.dart index 3306928624..03adc9010b 100644 --- a/packages/flutter/lib/src/widgets/safe_area.dart +++ b/packages/flutter/lib/src/widgets/safe_area.dart @@ -20,6 +20,8 @@ import 'media_query.dart'; /// /// See also: /// +/// * [SliverSafeArea], for insetting slivers to avoid operating system +/// intrusions. /// * [Padding], for insetting widgets in general. /// * [MediaQuery], from which the window padding is obtained. /// * [dart:ui.Window.padding], which reports the padding from the operating @@ -93,3 +95,88 @@ class SafeArea extends StatelessWidget { description.add(new FlagProperty('bottom', value: left, ifTrue: 'avoid bottom padding')); } } + +/// A sliver that insets another sliver by sufficient padding to avoid +/// intrusions by the operating system. +/// +/// For example, this will indent the sliver by enough to avoid the status bar +/// at the top of the screen. +/// +/// It will also indent the sliver by the amount necessary to avoid The Notch +/// on the iPhone X, or other similar creative physical features of the +/// display. +/// +/// See also: +/// +/// * [SafeArea], for insetting widgets to avoid operating system intrusions. +/// * [SliverPadding], for insetting slivers in general. +/// * [MediaQuery], from which the window padding is obtained. +/// * [dart:ui.Window.padding], which reports the padding from the operating +/// system. +class SliverSafeArea extends StatelessWidget { + /// Creates a sliver that avoids operating system interfaces. + /// + /// The [left], [top], [right], and [bottom] arguments must not be null. + const SliverSafeArea({ + Key key, + this.left: true, + this.top: true, + this.right: true, + this.bottom: true, + @required this.sliver, + }) : assert(left != null), + assert(top != null), + assert(right != null), + assert(bottom != null), + super(key: key); + + /// Whether to avoid system intrusions on the left. + final bool left; + + /// Whether to avoid system intrusions at the top of the screen, typically the + /// system status bar. + final bool top; + + /// Whether to avoid system intrusions on the right. + final bool right; + + /// Whether to avoid system intrusions on the bottom side of the screen. + final bool bottom; + + /// The sliver below this sliver in the tree. + /// + /// The padding on the [MediaQuery] for the [sliver] will be suitably adjusted + /// to zero out any sides that were avoided by this sliver. + final Widget sliver; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + final EdgeInsets padding = MediaQuery.of(context).padding; + return new SliverPadding( + padding: new EdgeInsets.only( + left: left ? padding.left : 0.0, + top: top ? padding.top : 0.0, + right: right ? padding.right : 0.0, + bottom: bottom ? padding.bottom : 0.0, + ), + sliver: new MediaQuery.removePadding( + context: context, + removeLeft: left, + removeTop: top, + removeRight: right, + removeBottom: bottom, + child: sliver, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new FlagProperty('left', value: left, ifTrue: 'avoid left padding')); + description.add(new FlagProperty('top', value: left, ifTrue: 'avoid top padding')); + description.add(new FlagProperty('right', value: left, ifTrue: 'avoid right padding')); + description.add(new FlagProperty('bottom', value: left, ifTrue: 'avoid bottom padding')); + } +} diff --git a/packages/flutter/test/widgets/safe_area_test.dart b/packages/flutter/test/widgets/safe_area_test.dart index 1703535bd2..e7a99237d1 100644 --- a/packages/flutter/test/widgets/safe_area_test.dart +++ b/packages/flutter/test/widgets/safe_area_test.dart @@ -7,66 +7,175 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; void main() { - testWidgets('SafeArea - basic', (WidgetTester tester) async { - await tester.pumpWidget( - const MediaQuery( - data: const MediaQueryData(padding: const EdgeInsets.all(20.0)), - child: const SafeArea( - left: false, - child: const Placeholder(), - ), - ), - ); - expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 20.0)); - expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); - }); - - testWidgets('SafeArea - nested', (WidgetTester tester) async { - await tester.pumpWidget( - const MediaQuery( - data: const MediaQueryData(padding: const EdgeInsets.all(20.0)), - child: const SafeArea( - top: false, + group('SafeArea', () { + testWidgets('SafeArea - basic', (WidgetTester tester) async { + await tester.pumpWidget( + const MediaQuery( + data: const MediaQueryData(padding: const EdgeInsets.all(20.0)), child: const SafeArea( - right: false, + left: false, child: const Placeholder(), ), ), - ), - ); - expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0)); - expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); + ); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 20.0)); + expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); + }); + + testWidgets('SafeArea - nested', (WidgetTester tester) async { + await tester.pumpWidget( + const MediaQuery( + data: const MediaQueryData(padding: const EdgeInsets.all(20.0)), + child: const SafeArea( + top: false, + child: const SafeArea( + right: false, + child: const Placeholder(), + ), + ), + ), + ); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0)); + expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); + }); + + testWidgets('SafeArea - changing', (WidgetTester tester) async { + const Widget child = const SafeArea( + bottom: false, + child: const SafeArea( + left: false, + bottom: false, + child: const Placeholder(), + ), + ); + await tester.pumpWidget( + const MediaQuery( + data: const MediaQueryData(padding: const EdgeInsets.all(20.0)), + child: child, + ), + ); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0)); + expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 600.0)); + await tester.pumpWidget( + const MediaQuery( + data: const MediaQueryData(padding: const EdgeInsets.only( + left: 100.0, + top: 30.0, + right: 0.0, + bottom: 40.0, + )), + child: child, + ), + ); + expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(100.0, 30.0)); + expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); + }); }); - testWidgets('SafeArea - changing', (WidgetTester tester) async { - const Widget child = const SafeArea( - bottom: false, - child: const SafeArea( - left: false, + group('SliverSafeArea', () { + Widget buildWidget(EdgeInsets mediaPadding, Widget sliver) { + return new MediaQuery( + data: new MediaQueryData(padding: mediaPadding), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Viewport( + offset: new ViewportOffset.fixed(0.0), + axisDirection: AxisDirection.down, + slivers: [ + const SliverToBoxAdapter(child: const SizedBox(width: 800.0, height: 100.0, child: const Text('before'))), + sliver, + const SliverToBoxAdapter(child: const SizedBox(width: 800.0, height: 100.0, child: const Text('after'))), + ], + ), + ), + ); + } + + void verify(WidgetTester tester, List expectedRects) { + final List testAnswers = tester.renderObjectList(find.byType(SizedBox)).map( + (RenderBox target) { + final Offset topLeft = target.localToGlobal(Offset.zero); + final Offset bottomRight = target.localToGlobal(target.size.bottomRight(Offset.zero)); + return new Rect.fromPoints(topLeft, bottomRight); + } + ).toList(); + expect(testAnswers, equals(expectedRects)); + } + + testWidgets('SliverSafeArea - basic', (WidgetTester tester) async { + await tester.pumpWidget( + buildWidget( + const EdgeInsets.all(20.0), + const SliverSafeArea( + left: false, + sliver: const SliverToBoxAdapter(child: const SizedBox(width: 800.0, height: 100.0, child: const Text('padded'))), + ), + ), + ); + verify(tester, [ + new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0), + new Rect.fromLTWH(0.0, 120.0, 780.0, 100.0), + new Rect.fromLTWH(0.0, 240.0, 800.0, 100.0), + ]); + }); + + testWidgets('SliverSafeArea - nested', (WidgetTester tester) async { + await tester.pumpWidget( + buildWidget( + const EdgeInsets.all(20.0), + const SliverSafeArea( + top: false, + sliver: const SliverSafeArea( + right: false, + sliver: const SliverToBoxAdapter(child: const SizedBox(width: 800.0, height: 100.0, child: const Text('padded'))), + ), + ), + ), + ); + verify(tester, [ + new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0), + new Rect.fromLTWH(20.0, 120.0, 760.0, 100.0), + new Rect.fromLTWH(0.0, 240.0, 800.0, 100.0), + ]); + }); + + testWidgets('SliverSafeArea - changing', (WidgetTester tester) async { + const Widget sliver = const SliverSafeArea( bottom: false, - child: const Placeholder(), - ), - ); - await tester.pumpWidget( - const MediaQuery( - data: const MediaQueryData(padding: const EdgeInsets.all(20.0)), - child: child, - ), - ); - expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0)); - expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 600.0)); - await tester.pumpWidget( - const MediaQuery( - data: const MediaQueryData(padding: const EdgeInsets.only( - left: 100.0, - top: 30.0, - right: 0.0, - bottom: 40.0, - )), - child: child, - ), - ); - expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(100.0, 30.0)); - expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); + sliver: const SliverSafeArea( + left: false, + bottom: false, + sliver: const SliverToBoxAdapter(child: const SizedBox(width: 800.0, height: 100.0, child: const Text('padded'))), + ), + ); + await tester.pumpWidget( + buildWidget( + const EdgeInsets.all(20.0), + sliver, + ), + ); + verify(tester, [ + new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0), + new Rect.fromLTWH(20.0, 120.0, 760.0, 100.0), + new Rect.fromLTWH(0.0, 220.0, 800.0, 100.0), + ]); + + await tester.pumpWidget( + buildWidget( + const EdgeInsets.only( + left: 100.0, + top: 30.0, + right: 0.0, + bottom: 40.0, + ), + sliver, + ), + ); + verify(tester, [ + new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0), + new Rect.fromLTWH(100.0, 130.0, 700.0, 100.0), + new Rect.fromLTWH(0.0, 230.0, 800.0, 100.0), + ]); + }); }); }