Unverified Commit da27d674 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Move dialogs when keyboard pops up (#15426)

Fixes #7032
parent 076594b3
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
......@@ -38,6 +39,8 @@ class Dialog extends StatelessWidget {
const Dialog({
Key key,
this.child,
this.insetAnimationDuration: const Duration(milliseconds: 100),
this.insetAnimationCurve: Curves.decelerate,
}) : super(key: key);
/// The widget below this widget in the tree.
......@@ -45,25 +48,46 @@ class Dialog extends StatelessWidget {
/// {@macro flutter.widgets.child}
final Widget child;
/// The duration of the animation to show when the system keyboard intrudes
/// into the space that the dialog is placed in.
///
/// Defaults to 100 milliseconds.
final Duration insetAnimationDuration;
/// The curve to use for the animation shown when the system keyboard intrudes
/// into the space that the dialog is placed in.
///
/// Defaults to [Curves.fastOutSlowIn].
final Curve insetAnimationCurve;
Color _getColor(BuildContext context) {
return Theme.of(context).dialogBackgroundColor;
}
@override
Widget build(BuildContext context) {
return new Center(
child: new Container(
margin: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
return new AnimatedPadding(
padding: MediaQuery.of(context).viewInsets + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
duration: insetAnimationDuration,
curve: insetAnimationCurve,
child: new MediaQuery.removeViewInsets(
removeLeft: true,
removeTop: true,
removeRight: true,
removeBottom: true,
context: context,
child: new Center(
child: new ConstrainedBox(
constraints: const BoxConstraints(minWidth: 280.0),
child: new Material(
elevation: 24.0,
color: _getColor(context),
type: MaterialType.card,
child: child
)
)
)
child: child,
),
),
),
),
);
}
}
......
......@@ -140,7 +140,7 @@ class MediaQueryData {
);
}
/// Creates a copy of this media query data but with the given paddings
/// Creates a copy of this media query data but with the given [padding]s
/// replaced with zero.
///
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
......@@ -153,6 +153,7 @@ class MediaQueryData {
/// from the ambient [MediaQuery].
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget.
/// * [removeViewInsets], the same thing but for [viewInsets].
MediaQueryData removePadding({
bool removeLeft: false,
bool removeTop: false,
......@@ -176,6 +177,41 @@ class MediaQueryData {
);
}
/// Creates a copy of this media query data but with the given [viewInsets]
/// 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.removeViewInsets], which uses this method to remove
/// padding from the ambient [MediaQuery].
/// * [removePadding], the same thing but for [padding].
MediaQueryData removeViewInsets({
bool removeLeft: false,
bool removeTop: false,
bool removeRight: false,
bool removeBottom: false,
}) {
if (!(removeLeft || removeTop || removeRight || removeBottom))
return this;
return new MediaQueryData(
size: size,
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
padding: padding,
viewInsets: viewInsets.copyWith(
left: removeLeft ? 0.0 : null,
top: removeTop ? 0.0 : null,
right: removeRight ? 0.0 : null,
bottom: removeBottom ? 0.0 : null,
),
alwaysUse24HourFormat: alwaysUse24HourFormat,
);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
......@@ -194,9 +230,14 @@ class MediaQueryData {
@override
String toString() {
return '$runtimeType(size: $size, devicePixelRatio: $devicePixelRatio, '
'textScaleFactor: $textScaleFactor, padding: $padding, '
'viewInsets: $viewInsets, alwaysUse24HourFormat: $alwaysUse24HourFormat)';
return '$runtimeType('
'size: $size, '
'devicePixelRatio: $devicePixelRatio, '
'textScaleFactor: $textScaleFactor, '
'padding: $padding, '
'viewInsets: $viewInsets, '
'alwaysUse24HourFormat: $alwaysUse24HourFormat'
')';
}
}
......@@ -236,8 +277,8 @@ class MediaQuery extends InheritedWidget {
/// the given context, but removes the specified paddings.
///
/// This should be inserted into the widget tree when the [MediaQuery] padding
/// is consumed in such a way that the padding is no longer exposed to its
/// descendents or siblings.
/// is consumed by a widget in such a way that the padding is no longer
/// exposed to the widget's descendents or siblings.
///
/// The [context] argument is required, must not be null, and must have a
/// [MediaQuery] in scope.
......@@ -253,6 +294,8 @@ class MediaQuery extends InheritedWidget {
///
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget.
/// * [MediaQueryData.padding], the affected property of the [MediaQueryData].
/// * [new removeViewInsets], the same thing but for removing view insets.
factory MediaQuery.removePadding({
Key key,
@required BuildContext context,
......@@ -274,6 +317,48 @@ class MediaQuery extends InheritedWidget {
);
}
/// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery] from
/// the given context, but removes the specified view insets.
///
/// This should be inserted into the widget tree when the [MediaQuery] view
/// insets are consumed by a widget in such a way that the view insets are no
/// longer exposed to the widget's descendents 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.viewInsets], the affected property of the [MediaQueryData].
/// * [new removePadding], the same thing but for removing paddings.
factory MediaQuery.removeViewInsets({
Key key,
@required BuildContext context,
bool removeLeft: false,
bool removeTop: false,
bool removeRight: false,
bool removeBottom: false,
@required Widget child,
}) {
return new MediaQuery(
key: key,
data: MediaQuery.of(context).removeViewInsets(
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
......
......@@ -242,8 +242,9 @@ void main() {
semantics.dispose();
});
testWidgets('Dialogs removes MediaQuery padding', (WidgetTester tester) async {
testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async {
BuildContext outerContext;
BuildContext routeContext;
BuildContext dialogContext;
await tester.pumpWidget(new Localizations(
......@@ -255,6 +256,7 @@ void main() {
child: new MediaQuery(
data: const MediaQueryData(
padding: const EdgeInsets.all(50.0),
viewInsets: const EdgeInsets.only(left: 25.0, bottom: 75.0),
),
child: new Navigator(
onGenerateRoute: (_) {
......@@ -272,15 +274,62 @@ void main() {
showDialog<Null>(
context: outerContext,
barrierDismissible: false,
builder: (BuildContext context) {
routeContext = context;
return new Dialog(
child: new Builder(
builder: (BuildContext context) {
dialogContext = context;
return new Container();
return const Placeholder();
},
),
);
},
);
await tester.pump();
expect(MediaQuery.of(outerContext).padding, const EdgeInsets.all(50.0));
expect(MediaQuery.of(routeContext).padding, EdgeInsets.zero);
expect(MediaQuery.of(dialogContext).padding, EdgeInsets.zero);
expect(MediaQuery.of(outerContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0));
expect(MediaQuery.of(routeContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0));
expect(MediaQuery.of(dialogContext).viewInsets, EdgeInsets.zero);
});
testWidgets('Dialog widget insets by viewInsets', (WidgetTester tester) async {
await tester.pumpWidget(
const MediaQuery(
data: const MediaQueryData(
viewInsets: const EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
),
child: const Dialog(
child: const Placeholder(),
),
),
);
expect(
tester.getRect(find.byType(Placeholder)),
new Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)),
);
await tester.pumpWidget(
const MediaQuery(
data: const MediaQueryData(
viewInsets: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
),
child: const Dialog(
child: const Placeholder(),
),
),
);
expect( // no change because this is an animation
tester.getRect(find.byType(Placeholder)),
new Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)),
);
await tester.pump(const Duration(seconds: 1));
expect( // animation finished
tester.getRect(find.byType(Placeholder)),
new Rect.fromLTRB(40.0, 24.0, 800.0 - 40.0, 600.0 - 24.0),
);
});
}
......@@ -96,9 +96,9 @@ void main() {
builder: (BuildContext context) {
return new MediaQuery.removePadding(
context: context,
removeLeft: true,
removeTop: true,
removeRight: true,
removeLeft: true,
removeBottom: true,
child: new Builder(
builder: (BuildContext context) {
......@@ -107,7 +107,8 @@ void main() {
}
),
);
}),
},
),
)
);
......@@ -118,4 +119,50 @@ void main() {
expect(unpadded.viewInsets, viewInsets);
expect(unpadded.alwaysUse24HourFormat, true);
});
testWidgets('MediaQuery.removeViewInsets removes specified viewInsets', (WidgetTester tester) async {
const Size size = const Size(2.0, 4.0);
const double devicePixelRatio = 2.0;
const double textScaleFactor = 1.2;
const EdgeInsets padding = const EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0);
const EdgeInsets viewInsets = const EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0);
MediaQueryData unpadded;
await tester.pumpWidget(
new MediaQuery(
data: const MediaQueryData(
size: size,
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
padding: padding,
viewInsets: viewInsets,
alwaysUse24HourFormat: true,
),
child: new Builder(
builder: (BuildContext context) {
return new MediaQuery.removeViewInsets(
context: context,
removeLeft: true,
removeTop: true,
removeRight: true,
removeBottom: true,
child: new Builder(
builder: (BuildContext context) {
unpadded = MediaQuery.of(context);
return new Container();
}
),
);
},
),
)
);
expect(unpadded.size, size);
expect(unpadded.devicePixelRatio, devicePixelRatio);
expect(unpadded.textScaleFactor, textScaleFactor);
expect(unpadded.padding, padding);
expect(unpadded.viewInsets, EdgeInsets.zero);
expect(unpadded.alwaysUse24HourFormat, true);
});
}
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