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 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -38,6 +39,8 @@ class Dialog extends StatelessWidget { ...@@ -38,6 +39,8 @@ class Dialog extends StatelessWidget {
const Dialog({ const Dialog({
Key key, Key key,
this.child, this.child,
this.insetAnimationDuration: const Duration(milliseconds: 100),
this.insetAnimationCurve: Curves.decelerate,
}) : super(key: key); }) : super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -45,25 +48,46 @@ class Dialog extends StatelessWidget { ...@@ -45,25 +48,46 @@ class Dialog extends StatelessWidget {
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
final Widget 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) { Color _getColor(BuildContext context) {
return Theme.of(context).dialogBackgroundColor; return Theme.of(context).dialogBackgroundColor;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Center( return new AnimatedPadding(
child: new Container( padding: MediaQuery.of(context).viewInsets + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
margin: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0), duration: insetAnimationDuration,
child: new ConstrainedBox( curve: insetAnimationCurve,
constraints: const BoxConstraints(minWidth: 280.0), child: new MediaQuery.removeViewInsets(
child: new Material( removeLeft: true,
elevation: 24.0, removeTop: true,
color: _getColor(context), removeRight: true,
type: MaterialType.card, removeBottom: true,
child: child 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,
),
),
),
),
); );
} }
} }
......
...@@ -140,7 +140,7 @@ class MediaQueryData { ...@@ -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. /// replaced with zero.
/// ///
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
...@@ -153,6 +153,7 @@ class MediaQueryData { ...@@ -153,6 +153,7 @@ class MediaQueryData {
/// from the ambient [MediaQuery]. /// from the ambient [MediaQuery].
/// * [SafeArea], which both removes the padding from the [MediaQuery] and /// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget. /// adds a [Padding] widget.
/// * [removeViewInsets], the same thing but for [viewInsets].
MediaQueryData removePadding({ MediaQueryData removePadding({
bool removeLeft: false, bool removeLeft: false,
bool removeTop: false, bool removeTop: false,
...@@ -176,6 +177,41 @@ class MediaQueryData { ...@@ -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 @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
...@@ -194,9 +230,14 @@ class MediaQueryData { ...@@ -194,9 +230,14 @@ class MediaQueryData {
@override @override
String toString() { String toString() {
return '$runtimeType(size: $size, devicePixelRatio: $devicePixelRatio, ' return '$runtimeType('
'textScaleFactor: $textScaleFactor, padding: $padding, ' 'size: $size, '
'viewInsets: $viewInsets, alwaysUse24HourFormat: $alwaysUse24HourFormat)'; 'devicePixelRatio: $devicePixelRatio, '
'textScaleFactor: $textScaleFactor, '
'padding: $padding, '
'viewInsets: $viewInsets, '
'alwaysUse24HourFormat: $alwaysUse24HourFormat'
')';
} }
} }
...@@ -236,8 +277,8 @@ class MediaQuery extends InheritedWidget { ...@@ -236,8 +277,8 @@ class MediaQuery extends InheritedWidget {
/// the given context, but removes the specified paddings. /// the given context, but removes the specified paddings.
/// ///
/// This should be inserted into the widget tree when the [MediaQuery] padding /// 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 /// is consumed by a widget in such a way that the padding is no longer
/// descendents or siblings. /// exposed to the widget's descendents or siblings.
/// ///
/// The [context] argument is required, must not be null, and must have a /// The [context] argument is required, must not be null, and must have a
/// [MediaQuery] in scope. /// [MediaQuery] in scope.
...@@ -253,6 +294,8 @@ class MediaQuery extends InheritedWidget { ...@@ -253,6 +294,8 @@ class MediaQuery extends InheritedWidget {
/// ///
/// * [SafeArea], which both removes the padding from the [MediaQuery] and /// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget. /// 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({ factory MediaQuery.removePadding({
Key key, Key key,
@required BuildContext context, @required BuildContext context,
...@@ -274,6 +317,48 @@ class MediaQuery extends InheritedWidget { ...@@ -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. /// Contains information about the current media.
/// ///
/// For example, the [MediaQueryData.size] property contains the width and /// For example, the [MediaQueryData.size] property contains the width and
......
...@@ -242,8 +242,9 @@ void main() { ...@@ -242,8 +242,9 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Dialogs removes MediaQuery padding', (WidgetTester tester) async { testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async {
BuildContext outerContext; BuildContext outerContext;
BuildContext routeContext;
BuildContext dialogContext; BuildContext dialogContext;
await tester.pumpWidget(new Localizations( await tester.pumpWidget(new Localizations(
...@@ -255,6 +256,7 @@ void main() { ...@@ -255,6 +256,7 @@ void main() {
child: new MediaQuery( child: new MediaQuery(
data: const MediaQueryData( data: const MediaQueryData(
padding: const EdgeInsets.all(50.0), padding: const EdgeInsets.all(50.0),
viewInsets: const EdgeInsets.only(left: 25.0, bottom: 75.0),
), ),
child: new Navigator( child: new Navigator(
onGenerateRoute: (_) { onGenerateRoute: (_) {
...@@ -273,14 +275,61 @@ void main() { ...@@ -273,14 +275,61 @@ void main() {
context: outerContext, context: outerContext,
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) { builder: (BuildContext context) {
dialogContext = context; routeContext = context;
return new Container(); return new Dialog(
child: new Builder(
builder: (BuildContext context) {
dialogContext = context;
return const Placeholder();
},
),
);
}, },
); );
await tester.pump(); await tester.pump();
expect(MediaQuery.of(outerContext).padding, const EdgeInsets.all(50.0)); 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(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() { ...@@ -96,9 +96,9 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return new MediaQuery.removePadding( return new MediaQuery.removePadding(
context: context, context: context,
removeLeft: true,
removeTop: true, removeTop: true,
removeRight: true, removeRight: true,
removeLeft: true,
removeBottom: true, removeBottom: true,
child: new Builder( child: new Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
...@@ -107,7 +107,8 @@ void main() { ...@@ -107,7 +107,8 @@ void main() {
} }
), ),
); );
}), },
),
) )
); );
...@@ -118,4 +119,50 @@ void main() { ...@@ -118,4 +119,50 @@ void main() {
expect(unpadded.viewInsets, viewInsets); expect(unpadded.viewInsets, viewInsets);
expect(unpadded.alwaysUse24HourFormat, true); 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