This adds multiline text widget support. (#12120)

Add multiline keyboard support to editable text widget.  Fixes #8028.
This commit is contained in:
gspencergoog 2017-09-27 14:23:34 -07:00 committed by GitHub
parent 2670786210
commit 4389f07024
5 changed files with 220 additions and 15 deletions

View file

@ -66,8 +66,10 @@ class TextField extends StatefulWidget {
/// null.
///
/// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is 1, meaning this is a single-line
/// text field. If it is not null, it must be greater than zero.
/// the number of lines. By default, it is one, meaning this is a single-line
/// text field. [maxLines] must not be zero. If [maxLines] is not one, then
/// [keyboardType] is ignored, and the [TextInputType.multiline] keyboard type
/// is used.
///
/// The [keyboardType], [textAlign], [autofocus], [obscureText], and
/// [autocorrect] arguments must not be null.
@ -76,7 +78,7 @@ class TextField extends StatefulWidget {
this.controller,
this.focusNode,
this.decoration: const InputDecoration(),
this.keyboardType: TextInputType.text,
TextInputType keyboardType: TextInputType.text,
this.style,
this.textAlign: TextAlign.start,
this.autofocus: false,
@ -92,6 +94,7 @@ class TextField extends StatefulWidget {
assert(obscureText != null),
assert(autocorrect != null),
assert(maxLines == null || maxLines > 0),
keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline,
super(key: key);
/// Controls the text being edited.
@ -115,7 +118,9 @@ class TextField extends StatefulWidget {
/// The type of keyboard to use for editing the text.
///
/// Defaults to [TextInputType.text]. Cannot be null.
/// Defaults to [TextInputType.text]. Must not be null. If
/// [maxLines] is not one, then [keyboardType] is ignored, and the
/// [TextInputType.multiline] keyboard type is used.
final TextInputType keyboardType;
/// The style to use for the text being edited.

View file

@ -22,6 +22,13 @@ enum TextInputType {
/// Requests the default platform keyboard.
text,
/// Optimize for multi-line textual information.
///
/// Requests the default platform keyboard, but accepts newlines when the
/// enter key is pressed. This is the input type used for all multi-line text
/// fields.
multiline,
/// Optimize for numerical information.
///
/// Requests a keyboard with ready access to the decimal point and number
@ -56,6 +63,10 @@ enum TextInputType {
enum TextInputAction {
/// Complete the text input operation.
done,
/// The action to take when the enter button is pressed in a multi-line
/// text field (which is typically to do nothing).
newline,
}
/// Controls the visual appearance of the text input control.
@ -67,15 +78,18 @@ enum TextInputAction {
class TextInputConfiguration {
/// Creates configuration information for a text input control.
///
/// The [inputType], [obscureText], and [autocorrect] arguments must not be null.
/// All arguments have default values, except [actionLabel]. Only
/// [actionLabel] may be null.
const TextInputConfiguration({
this.inputType: TextInputType.text,
this.obscureText: false,
this.autocorrect: true,
this.actionLabel,
this.inputAction: TextInputAction.done,
}) : assert(inputType != null),
assert(obscureText != null),
assert(autocorrect != null);
assert(autocorrect != null),
assert(inputAction != null);
/// The type of information for which to optimize the text input control.
final TextInputType inputType;
@ -93,6 +107,9 @@ class TextInputConfiguration {
/// What text to display in the text input control's action button.
final String actionLabel;
/// What kind of action to request for the action button on the IME.
final TextInputAction inputAction;
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
return <String, dynamic>{
@ -100,6 +117,7 @@ class TextInputConfiguration {
'obscureText': obscureText,
'autocorrect': autocorrect,
'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
};
}
}
@ -278,6 +296,8 @@ TextInputAction _toTextInputAction(String action) {
switch (action) {
case 'TextInputAction.done':
return TextInputAction.done;
case 'TextInputAction.newline':
return TextInputAction.newline;
}
throw new FlutterError('Unknown text input action: $action');
}

View file

@ -141,8 +141,12 @@ class EditableText extends StatefulWidget {
/// Creates a basic text input control.
///
/// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is 1, meaning this is a single-line
/// text field. If it is not null, it must be greater than zero.
/// the number of lines. By default, it is one, meaning this is a single-line
/// text field. [maxLines] must be null or greater than zero.
///
/// If [keyboardType] is not set or is null, it will default to
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
/// default to [TextInputType.multiline].
///
/// The [controller], [focusNode], [style], [cursorColor], and [textAlign]
/// arguments must not be null.
@ -161,7 +165,7 @@ class EditableText extends StatefulWidget {
this.autofocus: false,
this.selectionColor,
this.selectionControls,
this.keyboardType,
TextInputType keyboardType,
this.onChanged,
this.onSubmitted,
this.onSelectionChanged,
@ -175,6 +179,7 @@ class EditableText extends StatefulWidget {
assert(textAlign != null),
assert(maxLines == null || maxLines > 0),
assert(autofocus != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
inputFormatters = maxLines == 1
? (
<TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter]
@ -376,10 +381,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void performAction(TextInputAction action) {
widget.controller.clearComposing();
widget.focusNode.unfocus();
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
switch (action) {
case TextInputAction.done:
widget.controller.clearComposing();
widget.focusNode.unfocus();
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
break;
case TextInputAction.newline:
// Do nothing for a "newline" action: the newline is already inserted.
break;
}
}
void _updateRemoteEditingValueIfNeeded() {
@ -419,8 +431,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (!_hasInputConnection) {
final TextEditingValue localValue = _value;
_lastKnownRemoteTextEditingValue = localValue;
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType, obscureText: widget.obscureText, autocorrect: widget.autocorrect))
..setEditingState(localValue);
_textInputConnection = TextInput.attach(this,
new TextInputConfiguration(
inputType: widget.keyboardType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
inputAction: widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
)
)..setEditingState(localValue);
}
_textInputConnection.show();
}

View file

@ -0,0 +1,156 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
void main() {
final TextEditingController controller = new TextEditingController();
final FocusNode focusNode = new FocusNode();
final FocusScopeNode focusScopeNode = new FocusScopeNode();
final TextStyle textStyle = const TextStyle();
final Color cursorColor = const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
testWidgets('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
)));
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1));
expect(editableText.obscureText, isFalse);
expect(editableText.autocorrect, isTrue);
});
testWidgets('text keyboard is requested when maxLines is default',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
))));
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1));
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('multiline keyboard is requested when set explicitly',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
))));
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
});
testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.phone,
maxLines: 3,
style: textStyle,
cursorColor: cursorColor,
))));
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.phone'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
});
testWidgets('multiline keyboard is requested when set implicitly',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 3, // Sets multiline keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
))));
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
});
testWidgets('single line inputs have correct default keyboard',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
))));
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
});
}

View file

@ -28,6 +28,9 @@ class TestTextInput {
int _client = 0;
/// Arguments supplied to the TextInput.setClient method call.
Map<String, dynamic> setClientArgs;
/// The last set of arguments that [TextInputConnection.setEditingState] sent
/// to the embedder.
///
@ -40,6 +43,7 @@ class TestTextInput {
switch (methodCall.method) {
case 'TextInput.setClient':
_client = methodCall.arguments[0];
setClientArgs = methodCall.arguments[1];
break;
case 'TextInput.clearClient':
_client = 0;