Unverified Commit 991da4f7 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Updating MediaQuery with viewPadding (#33999)

* Updating MediaQuery with viewPadding

* Added tests

* Fixed test

* Updating remove logic & tests

* Diagram and documentation

* Review feedback
parent ce4f15cd
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'dart:ui' show Brightness;
......@@ -32,24 +33,52 @@ enum Orientation {
/// exception, unless the `nullOk` argument is set to true, in which case it
/// returns null.
///
/// MediaQueryData includes two [EdgeInsets] values:
/// [padding] and [viewInsets]. These
/// values reflect the configuration of the device and are used by
/// many top level widgets, like [SafeArea] and the Cupertino and
/// Material scaffold widgets. The padding value defines areas that
/// might not be completely visible, like the display "notch" on the
/// iPhone X. The viewInsets value defines areas that aren't visible at
/// all, typically because they're obscured by the device's keyboard.
/// ## Insets and Padding
///
/// The viewInsets and padding values are independent, they're both
/// measured from the edges of the MediaQuery widget's bounds. The
/// bounds of the top level MediaQuery created by [WidgetsApp] are the
/// same as the window that contains the app.
/// ![A diagram of padding, viewInsets, and viewPadding in correlation with each
/// other](https://flutter.github.io/assets-for-api-docs/assets/widgets/media_query.png)
///
/// Widgets whose layouts consume space defined by [viewInsets] or
/// [padding] should enclose their children in secondary MediaQuery
/// This diagram illustrates how [padding] relates to [viewPadding] and
/// [viewInsets], shown here in its simplest configuration, as the difference
/// between the two. In cases when the viewInsets exceed the viewPadding, like
/// when a software keyboard is shown below, padding goes to zero rather than a
/// negative value. Therefore, padding is calculated by taking
/// `max(0.0, viewPadding - viewInsets)`.
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/widgets/window_padding.mp4}
///
/// In this diagram, the black areas represent system UI that the app cannot
/// draw over. The red area represents view padding that the application may not
/// be able to detect gestures in and may not want to draw in. The grey area
/// represents the system keyboard, which can cover over the bottom view padding
/// when visible.
///
/// MediaQueryData includes three [EdgeInsets] values:
/// [padding], [viewPadding], and [viewInsets]. These values reflect the
/// configuration of the device and are used and optionally consumed by widgets
/// that position content within these insets. The padding value defines areas
/// that might not be completely visible, like the display "notch" on the iPhone
/// X. The viewInsets value defines areas that aren't visible at all, typically
/// because they're obscured by the device's keyboard. Similar to viewInsets,
/// viewPadding does not differentiate padding in areas that may be obscured.
/// For example, by using the viewPadding property, padding would defer to the
/// iPhone "safe area" regardless of whether a keyboard is showing.
///
/// The viewInsets and viewPadding are independent values, they're
/// measured from the edges of the MediaQuery widget's bounds. Together they
/// inform the [padding] property. The bounds of the top level MediaQuery
/// created by [WidgetsApp] are the same as the window that contains the app.
///
/// Widgets whose layouts consume space defined by [viewInsets], [viewPadding],
/// or [padding] should enclose their children in secondary MediaQuery
/// widgets that reduce those properties by the same amount.
/// The [removePadding] and [removeInsets] methods are useful for this.
/// The [removePadding], [removeViewPadding], and [removeInsets] methods are
/// useful for this.
/// See also:
///
/// * [Scaffold], [SafeArea], [CupertinoTabScaffold], and
/// [CupertinoPageScaffold], all of which are informed by [padding],
/// [viewPadding], and [viewInsets].
@immutable
class MediaQueryData {
/// Creates data for a media query with explicit values.
......@@ -63,6 +92,7 @@ class MediaQueryData {
this.platformBrightness = Brightness.light,
this.padding = EdgeInsets.zero,
this.viewInsets = EdgeInsets.zero,
this.viewPadding = EdgeInsets.zero,
this.alwaysUse24HourFormat = false,
this.accessibleNavigation = false,
this.invertColors = false,
......@@ -82,6 +112,7 @@ class MediaQueryData {
textScaleFactor = window.textScaleFactor,
platformBrightness = window.platformBrightness,
padding = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio),
viewPadding = EdgeInsets.fromWindowPadding(window.viewPadding, window.devicePixelRatio),
viewInsets = EdgeInsets.fromWindowPadding(window.viewInsets, window.devicePixelRatio),
accessibleNavigation = window.accessibilityFeatures.accessibleNavigation,
invertColors = window.accessibilityFeatures.invertColors,
......@@ -128,15 +159,16 @@ class MediaQueryData {
/// When a mobile device's keyboard is visible `viewInsets.bottom`
/// corresponds to the top of the keyboard.
///
/// This value is independent of the [padding]: both values are
/// measured from the edges of the [MediaQuery] widget's bounds. The
/// bounds of the top level MediaQuery created by [WidgetsApp] are the
/// same as the window (often the mobile device screen) that contains the app.
/// This value is independent of the [padding] and [viewPadding]. viewPadding
/// is measured from the edges of the [MediaQuery] widget's bounds. Padding is
/// calculated based on the viewPadding and viewInsets. The bounds of the top
/// level MediaQuery created by [WidgetsApp] are the same as the window
/// (often the mobile device screen) that contains the app.
///
/// See also:
///
/// * [MediaQueryData], which provides some additional detail about this
/// property and how it differs from [padding].
/// * [ui.window], which provides some additional detail about this property
/// and how it relates to [padding] and [viewPadding].
final EdgeInsets viewInsets;
/// The parts of the display that are partially obscured by system UI,
......@@ -148,14 +180,36 @@ class MediaQueryData {
/// for subsequent descendants in the widget tree by inserting a new
/// [MediaQuery] widget using the [MediaQuery.removePadding] factory.
///
/// Padding is derived from the values of viewInsets and viewPadding.
///
/// See also:
///
/// * [MediaQueryData], which provides some additional detail about this
/// property and how it differs from [viewInsets].
/// * [ui.window], which provides some additional detail about this
/// property and how it relates to [viewInsets] and [viewPadding].
/// * [SafeArea], a widget that consumes this padding with a [Padding] widget
/// and automatically removes it from the [MediaQuery] for its child.
final EdgeInsets padding;
/// The parts of the display that are partially obscured by system UI,
/// typically by the hardware display "notches" or the system status bar.
///
/// This value remains the same regardless of whether the system is reporting
/// other obstructions in the same physical area of the screen. For example, a
/// software keyboard on the bottom of the screen that may cover and consume
/// the same area that requires bottom padding will not affect this value.
///
/// This value is independent of the [padding] and [viewInsets]: their values
/// are measured from the edges of the [MediaQuery] widget's bounds. The
/// bounds of the top level MediaQuery created by [WidgetsApp] are the
/// same as the window that contains the app. On mobile devices, this will
/// typically be the full screen.
///
/// See also:
///
/// * [ui.window], which provides some additional detail about this
/// property and how it relates to [padding] and [viewInsets].
final EdgeInsets viewPadding;
/// Whether to use 24-hour format when formatting time.
///
/// The behavior of this flag is different across platforms:
......@@ -218,6 +272,7 @@ class MediaQueryData {
double textScaleFactor,
Brightness platformBrightness,
EdgeInsets padding,
EdgeInsets viewPadding,
EdgeInsets viewInsets,
bool alwaysUse24HourFormat,
bool disableAnimations,
......@@ -231,6 +286,7 @@ class MediaQueryData {
textScaleFactor: textScaleFactor ?? this.textScaleFactor,
platformBrightness: platformBrightness ?? this.platformBrightness,
padding: padding ?? this.padding,
viewPadding: viewPadding ?? this.viewPadding,
viewInsets: viewInsets ?? this.viewInsets,
alwaysUse24HourFormat: alwaysUse24HourFormat ?? this.alwaysUse24HourFormat,
invertColors: invertColors ?? this.invertColors,
......@@ -254,6 +310,7 @@ class MediaQueryData {
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget.
/// * [removeViewInsets], the same thing but for [viewInsets].
/// * [removeViewPadding], the same thing but for [viewPadding].
MediaQueryData removePadding({
bool removeLeft = false,
bool removeTop = false,
......@@ -273,6 +330,12 @@ class MediaQueryData {
right: removeRight ? 0.0 : null,
bottom: removeBottom ? 0.0 : null,
),
viewPadding: viewPadding.copyWith(
left: math.max(0.0, viewPadding.left - padding.left),
top: math.max(0.0, viewPadding.top - padding.top),
right: math.max(0.0, viewPadding.right - padding.right),
bottom: math.max(0.0, viewPadding.bottom - padding.bottom),
),
viewInsets: viewInsets,
alwaysUse24HourFormat: alwaysUse24HourFormat,
disableAnimations: disableAnimations,
......@@ -294,6 +357,7 @@ class MediaQueryData {
/// * [new MediaQuery.removeViewInsets], which uses this method to remove
/// padding from the ambient [MediaQuery].
/// * [removePadding], the same thing but for [padding].
/// * [removeViewPadding], the same thing but for [viewPadding].
MediaQueryData removeViewInsets({
bool removeLeft = false,
bool removeTop = false,
......@@ -308,6 +372,12 @@ class MediaQueryData {
textScaleFactor: textScaleFactor,
platformBrightness: platformBrightness,
padding: padding,
viewPadding: viewPadding.copyWith(
left: math.max(0.0, viewPadding.left - viewInsets.left),
top: math.max(0.0, viewPadding.top - viewInsets.top),
right: math.max(0.0, viewPadding.right - viewInsets.right),
bottom: math.max(0.0, viewPadding.bottom - viewInsets.bottom),
),
viewInsets: viewInsets.copyWith(
left: removeLeft ? 0.0 : null,
top: removeTop ? 0.0 : null,
......@@ -322,6 +392,53 @@ class MediaQueryData {
);
}
/// Creates a copy of this media query data but with the given [viewPadding]
/// replaced with zero.
///
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
/// must not be null. If all four are false (the default) then this
/// [MediaQueryData] is returned unmodified.
///
/// See also:
///
/// * [new MediaQuery.removeViewPadding], which uses this method to remove
/// padding from the ambient [MediaQuery].
/// * [removePadding], the same thing but for [padding].
/// * [removeViewInsets], the same thing but for [viewInsets].
MediaQueryData removeViewPadding({
bool removeLeft = false,
bool removeTop = false,
bool removeRight = false,
bool removeBottom = false,
}) {
if (!(removeLeft || removeTop || removeRight || removeBottom))
return this;
return MediaQueryData(
size: size,
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
platformBrightness: platformBrightness,
padding: padding.copyWith(
left: removeLeft ? 0.0 : null,
top: removeTop ? 0.0 : null,
right: removeRight ? 0.0 : null,
bottom: removeBottom ? 0.0 : null,
),
viewInsets: viewInsets,
viewPadding: viewPadding.copyWith(
left: removeLeft ? 0.0 : null,
top: removeTop ? 0.0 : null,
right: removeRight ? 0.0 : null,
bottom: removeBottom ? 0.0 : null,
),
alwaysUse24HourFormat: alwaysUse24HourFormat,
disableAnimations: disableAnimations,
invertColors: invertColors,
accessibleNavigation: accessibleNavigation,
boldText: boldText,
);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
......@@ -332,6 +449,7 @@ class MediaQueryData {
&& typedOther.textScaleFactor == textScaleFactor
&& typedOther.platformBrightness == platformBrightness
&& typedOther.padding == padding
&& typedOther.viewPadding == viewPadding
&& typedOther.viewInsets == viewInsets
&& typedOther.alwaysUse24HourFormat == alwaysUse24HourFormat
&& typedOther.disableAnimations == disableAnimations
......@@ -348,6 +466,7 @@ class MediaQueryData {
textScaleFactor,
platformBrightness,
padding,
viewPadding,
viewInsets,
alwaysUse24HourFormat,
disableAnimations,
......@@ -365,6 +484,7 @@ class MediaQueryData {
'textScaleFactor: ${textScaleFactor.toStringAsFixed(1)}, '
'platformBrightness: $platformBrightness, '
'padding: $padding, '
'viewPadding: $viewPadding, '
'viewInsets: $viewInsets, '
'alwaysUse24HourFormat: $alwaysUse24HourFormat, '
'accessibleNavigation: $accessibleNavigation, '
......@@ -430,6 +550,7 @@ class MediaQuery extends InheritedWidget {
/// adds a [Padding] widget.
/// * [MediaQueryData.padding], the affected property of the [MediaQueryData].
/// * [new removeViewInsets], the same thing but for removing view insets.
/// * [new removeViewPadding], the same thing but for removing view insets.
factory MediaQuery.removePadding({
Key key,
@required BuildContext context,
......@@ -472,6 +593,7 @@ class MediaQuery extends InheritedWidget {
///
/// * [MediaQueryData.viewInsets], the affected property of the [MediaQueryData].
/// * [new removePadding], the same thing but for removing paddings.
/// * [new removeViewPadding], the same thing but for removing view insets.
factory MediaQuery.removeViewInsets({
Key key,
@required BuildContext context,
......@@ -493,6 +615,50 @@ class MediaQuery extends InheritedWidget {
);
}
/// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery] from
/// the given context, but removes the specified view padding.
///
/// This should be inserted into the widget tree when the [MediaQuery] view
/// padding is consumed by a widget in such a way that the view padding is no
/// longer exposed to the widget's descendants or siblings.
///
/// The [context] argument is required, must not be null, and must have a
/// [MediaQuery] in scope.
///
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
/// must not be null. If all four are false (the default) then the returned
/// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not
/// particularly useful.
///
/// The [child] argument is required and must not be null.
///
/// See also:
///
/// * [MediaQueryData.viewPadding], the affected property of the
/// [MediaQueryData].
/// * [new removePadding], the same thing but for removing paddings.
/// * [new removeViewInsets], the same thing but for removing view insets.
factory MediaQuery.removeViewPadding({
Key key,
@required BuildContext context,
bool removeLeft = false,
bool removeTop = false,
bool removeRight = false,
bool removeBottom = false,
@required Widget child,
}) {
return MediaQuery(
key: key,
data: MediaQuery.of(context).removeViewPadding(
removeLeft: removeLeft,
removeTop: removeTop,
removeRight: removeRight,
removeBottom: removeBottom,
),
child: child,
);
}
/// Contains information about the current media.
///
/// For example, the [MediaQueryData.size] property contains the width and
......
......@@ -56,6 +56,7 @@ void main() {
expect(copied.devicePixelRatio, data.devicePixelRatio);
expect(copied.textScaleFactor, data.textScaleFactor);
expect(copied.padding, data.padding);
expect(copied.viewPadding, data.viewPadding);
expect(copied.viewInsets, data.viewInsets);
expect(copied.alwaysUse24HourFormat, data.alwaysUse24HourFormat);
expect(copied.accessibleNavigation, data.accessibleNavigation);
......@@ -72,6 +73,7 @@ void main() {
devicePixelRatio: 1.41,
textScaleFactor: 1.62,
padding: const EdgeInsets.all(9.10938),
viewPadding: const EdgeInsets.all(11.24031),
viewInsets: const EdgeInsets.all(1.67262),
alwaysUse24HourFormat: true,
accessibleNavigation: true,
......@@ -84,6 +86,7 @@ void main() {
expect(copied.devicePixelRatio, 1.41);
expect(copied.textScaleFactor, 1.62);
expect(copied.padding, const EdgeInsets.all(9.10938));
expect(copied.viewPadding, const EdgeInsets.all(11.24031));
expect(copied.viewInsets, const EdgeInsets.all(1.67262));
expect(copied.alwaysUse24HourFormat, true);
expect(copied.accessibleNavigation, true);
......@@ -98,6 +101,7 @@ void main() {
const double devicePixelRatio = 2.0;
const double textScaleFactor = 1.2;
const EdgeInsets padding = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0);
const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0);
const EdgeInsets viewInsets = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0);
MediaQueryData unpadded;
......@@ -108,6 +112,7 @@ void main() {
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
padding: padding,
viewPadding: viewPadding,
viewInsets: viewInsets,
alwaysUse24HourFormat: true,
accessibleNavigation: true,
......@@ -139,6 +144,7 @@ void main() {
expect(unpadded.devicePixelRatio, devicePixelRatio);
expect(unpadded.textScaleFactor, textScaleFactor);
expect(unpadded.padding, EdgeInsets.zero);
expect(unpadded.viewPadding, viewInsets);
expect(unpadded.viewInsets, viewInsets);
expect(unpadded.alwaysUse24HourFormat, true);
expect(unpadded.accessibleNavigation, true);
......@@ -152,6 +158,7 @@ void main() {
const double devicePixelRatio = 2.0;
const double textScaleFactor = 1.2;
const EdgeInsets padding = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0);
const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0);
const EdgeInsets viewInsets = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0);
MediaQueryData unpadded;
......@@ -162,6 +169,7 @@ void main() {
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
padding: padding,
viewPadding : viewPadding,
viewInsets: viewInsets,
alwaysUse24HourFormat: true,
accessibleNavigation: true,
......@@ -193,6 +201,7 @@ void main() {
expect(unpadded.devicePixelRatio, devicePixelRatio);
expect(unpadded.textScaleFactor, textScaleFactor);
expect(unpadded.padding, padding);
expect(unpadded.viewPadding, padding);
expect(unpadded.viewInsets, EdgeInsets.zero);
expect(unpadded.alwaysUse24HourFormat, true);
expect(unpadded.accessibleNavigation, true);
......@@ -201,6 +210,63 @@ void main() {
expect(unpadded.boldText, true);
});
testWidgets('MediaQuery.removeViewPadding removes specified viewPadding', (WidgetTester tester) async {
const Size size = Size(2.0, 4.0);
const double devicePixelRatio = 2.0;
const double textScaleFactor = 1.2;
const EdgeInsets padding = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0);
const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0);
const EdgeInsets viewInsets = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0);
MediaQueryData unpadded;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(
size: size,
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
padding: padding,
viewPadding : viewPadding,
viewInsets: viewInsets,
alwaysUse24HourFormat: true,
accessibleNavigation: true,
invertColors: true,
disableAnimations: true,
boldText: true,
),
child: Builder(
builder: (BuildContext context) {
return MediaQuery.removeViewPadding(
context: context,
removeLeft: true,
removeTop: true,
removeRight: true,
removeBottom: true,
child: Builder(
builder: (BuildContext context) {
unpadded = MediaQuery.of(context);
return Container();
}
),
);
},
),
)
);
expect(unpadded.size, size);
expect(unpadded.devicePixelRatio, devicePixelRatio);
expect(unpadded.textScaleFactor, textScaleFactor);
expect(unpadded.padding, EdgeInsets.zero);
expect(unpadded.viewPadding, EdgeInsets.zero);
expect(unpadded.viewInsets, viewInsets);
expect(unpadded.alwaysUse24HourFormat, true);
expect(unpadded.accessibleNavigation, true);
expect(unpadded.invertColors, true);
expect(unpadded.disableAnimations, true);
expect(unpadded.boldText, true);
});
testWidgets('MediaQuery.textScaleFactorOf', (WidgetTester tester) async {
double outsideTextScaleFactor;
double insideTextScaleFactor;
......
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