mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
FocusNode.requestFocus should show the keyboard (#9558)
This patch introduces the notion of a keyboard token, which generalizes the logic in EditableText for distinguishing between gaining focus by default and gaining focus because of an explicit use action. Fixes #7985
This commit is contained in:
parent
1de15bbb00
commit
55f334681b
@ -225,7 +225,6 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_didAutoFocus && widget.autofocus) {
|
||||
_didRequestKeyboard = true;
|
||||
FocusScope.of(context).autofocus(widget.focusNode);
|
||||
_didAutoFocus = true;
|
||||
}
|
||||
@ -311,17 +310,16 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
return scrollOffset;
|
||||
}
|
||||
|
||||
bool _didRequestKeyboard = false;
|
||||
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
|
||||
|
||||
void _openInputConnectionIfNeeded() {
|
||||
void _openInputConnection() {
|
||||
if (!_hasInputConnection) {
|
||||
final TextEditingValue localValue = _value;
|
||||
_lastKnownRemoteTextEditingValue = localValue;
|
||||
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType))
|
||||
..setEditingState(localValue)
|
||||
..show();
|
||||
..setEditingState(localValue);
|
||||
}
|
||||
_textInputConnection.show();
|
||||
}
|
||||
|
||||
void _closeInputConnectionIfNeeded() {
|
||||
@ -333,13 +331,12 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
}
|
||||
|
||||
void _openOrCloseInputConnectionIfNeeded() {
|
||||
if (_hasFocus && _didRequestKeyboard) {
|
||||
_openInputConnectionIfNeeded();
|
||||
if (_hasFocus && widget.focusNode.consumeKeyboardToken()) {
|
||||
_openInputConnection();
|
||||
} else if (!_hasFocus) {
|
||||
_closeInputConnectionIfNeeded();
|
||||
widget.controller.clearComposing();
|
||||
}
|
||||
_didRequestKeyboard = false;
|
||||
}
|
||||
|
||||
/// Express interest in interacting with the keyboard.
|
||||
@ -350,16 +347,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
/// focus, the control will then attach to the keyboard and request that the
|
||||
/// keyboard become visible.
|
||||
void requestKeyboard() {
|
||||
if (_hasInputConnection) {
|
||||
_textInputConnection.show();
|
||||
} else {
|
||||
if (_hasFocus) {
|
||||
_openInputConnectionIfNeeded();
|
||||
} else {
|
||||
_didRequestKeyboard = true;
|
||||
FocusScope.of(context).requestFocus(widget.focusNode);
|
||||
}
|
||||
}
|
||||
if (_hasFocus)
|
||||
_openInputConnection();
|
||||
else
|
||||
FocusScope.of(context).requestFocus(widget.focusNode);
|
||||
}
|
||||
|
||||
void _hideSelectionOverlayIfNeeded() {
|
||||
|
@ -31,6 +31,7 @@ import 'package:flutter/foundation.dart';
|
||||
class FocusNode extends ChangeNotifier {
|
||||
FocusScopeNode _parent;
|
||||
FocusManager _manager;
|
||||
bool _hasKeyboardToken = false;
|
||||
|
||||
/// Whether this node has the overall focus.
|
||||
///
|
||||
@ -48,6 +49,27 @@ class FocusNode extends ChangeNotifier {
|
||||
/// This object notifies its listeners whenever this value changes.
|
||||
bool get hasFocus => _manager?._currentFocus == this;
|
||||
|
||||
/// Removes the keyboard token from this focus node if it has one.
|
||||
///
|
||||
/// This mechanism helps distinguish between an input control gaining focus by
|
||||
/// default and gaining focus as a result of an explicit user action.
|
||||
///
|
||||
/// When a focus node requests the focus (either via
|
||||
/// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus
|
||||
/// node receives a keyboard token if it does not already have one. Later,
|
||||
/// when the focus node becomes focused, the widget that manages the
|
||||
/// [TextInputConnection] should show the keyboard (i.e., call
|
||||
/// [TextInputConnection.show]) only if it successfully consumes the keyboard
|
||||
/// token from the focus node.
|
||||
///
|
||||
/// Returns whether this function successfully consumes a keyboard token.
|
||||
bool consumeKeyboardToken() {
|
||||
if (!_hasKeyboardToken)
|
||||
return false;
|
||||
_hasKeyboardToken = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Cancels any outstanding requests for focus.
|
||||
///
|
||||
/// This method is safe to call regardless of whether this node has ever
|
||||
@ -216,13 +238,9 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin {
|
||||
assert(node != null);
|
||||
if (_focus == node)
|
||||
return;
|
||||
assert(node._parent == null);
|
||||
_focus?.unfocus();
|
||||
assert(_focus == null);
|
||||
_focus = node;
|
||||
_focus._parent = this;
|
||||
_focus._manager = _manager;
|
||||
_didChangeFocusChain();
|
||||
node._hasKeyboardToken = true;
|
||||
_setFocus(node);
|
||||
}
|
||||
|
||||
/// If this scope lacks a focus, request that the given node becomes the
|
||||
@ -235,8 +253,10 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin {
|
||||
/// microtask.
|
||||
void autofocus(FocusNode node) {
|
||||
assert(node != null);
|
||||
if (_focus == null)
|
||||
requestFocus(node);
|
||||
if (_focus == null) {
|
||||
node._hasKeyboardToken = true;
|
||||
_setFocus(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adopts the given node if it is focused in another scope.
|
||||
@ -250,7 +270,19 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin {
|
||||
return;
|
||||
node.unfocus();
|
||||
assert(node._parent == null);
|
||||
autofocus(node);
|
||||
if (_focus == null)
|
||||
_setFocus(node);
|
||||
}
|
||||
|
||||
void _setFocus(FocusNode node) {
|
||||
assert(node != null);
|
||||
assert(node._parent == null);
|
||||
assert(_focus == null);
|
||||
_focus = node;
|
||||
_focus._parent = this;
|
||||
_focus._manager = _manager;
|
||||
_focus._hasKeyboardToken = true;
|
||||
_didChangeFocusChain();
|
||||
}
|
||||
|
||||
void _resignFocus(FocusNode node) {
|
||||
|
135
packages/flutter/test/material/text_field_focus_test.dart
Normal file
135
packages/flutter/test/material/text_field_focus_test.dart
Normal file
@ -0,0 +1,135 @@
|
||||
// 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/material.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Request focus shows keyboard', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = new FocusNode();
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: new Material(
|
||||
child: new Center(
|
||||
child: new TextField(
|
||||
focusNode: focusNode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
|
||||
await tester.idle();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
await tester.pumpWidget(new Container());
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Autofocus shows keyboard', (WidgetTester tester) async {
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: const Material(
|
||||
child: const Center(
|
||||
child: const TextField(
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
await tester.pumpWidget(new Container());
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Tap shows keyboard', (WidgetTester tester) async {
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: const Material(
|
||||
child: const Center(
|
||||
child: const TextField(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.idle();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
tester.testTextInput.hide();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.idle();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
await tester.pumpWidget(new Container());
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Dialog interaction', (WidgetTester tester) async {
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: const Material(
|
||||
child: const Center(
|
||||
child: const TextField(
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
final BuildContext context = tester.element(find.byType(TextField));
|
||||
|
||||
showDialog<Null>(
|
||||
context: context,
|
||||
child: const SimpleDialog(title: const Text('Dialog')),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
Navigator.of(tester.element(find.text('Dialog'))).pop();
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.idle();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
await tester.pumpWidget(new Container());
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
});
|
||||
|
||||
}
|
@ -21,24 +21,40 @@ const String _kTextInputClientChannel = 'flutter/textinputclient';
|
||||
/// * [WidgetTester.showKeyboard], which uses this class to simulate showing the
|
||||
/// popup keyboard and initializing its text.
|
||||
class TestTextInput {
|
||||
/// Installs this object as a mock handler for [SystemChannels.textInput].
|
||||
void register() {
|
||||
SystemChannels.textInput.setMockMethodCallHandler(handleTextInputCall);
|
||||
SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall);
|
||||
}
|
||||
|
||||
int _client = 0;
|
||||
Map<String, dynamic> editingState;
|
||||
|
||||
Future<dynamic> handleTextInputCall(MethodCall methodCall) async {
|
||||
Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'TextInput.setClient':
|
||||
_client = methodCall.arguments[0];
|
||||
break;
|
||||
case 'TextInput.clearClient':
|
||||
_client = 0;
|
||||
_isVisible = false;
|
||||
break;
|
||||
case 'TextInput.setEditingState':
|
||||
editingState = methodCall.arguments;
|
||||
break;
|
||||
case 'TextInput.show':
|
||||
_isVisible = true;
|
||||
break;
|
||||
case 'TextInput.hide':
|
||||
_isVisible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the onscreen keyboard is visible to the user.
|
||||
bool get isVisible => _isVisible;
|
||||
bool _isVisible = false;
|
||||
|
||||
/// Simulates the user changing the [TextEditingValue] to the given value.
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
expect(_client, isNonZero);
|
||||
BinaryMessages.handlePlatformMessage(
|
||||
@ -53,10 +69,16 @@ class TestTextInput {
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulates the user typing the given text.
|
||||
void enterText(String text) {
|
||||
updateEditingValue(new TextEditingValue(
|
||||
text: text,
|
||||
composing: new TextRange(start: 0, end: text.length),
|
||||
));
|
||||
}
|
||||
|
||||
/// Simulates the user hiding the onscreen keyboard.
|
||||
void hide() {
|
||||
_isVisible = false;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user