Commit 55f33468 authored by Adam Barth's avatar Adam Barth Committed by GitHub

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
parent 1de15bbb
...@@ -225,7 +225,6 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -225,7 +225,6 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
if (!_didAutoFocus && widget.autofocus) { if (!_didAutoFocus && widget.autofocus) {
_didRequestKeyboard = true;
FocusScope.of(context).autofocus(widget.focusNode); FocusScope.of(context).autofocus(widget.focusNode);
_didAutoFocus = true; _didAutoFocus = true;
} }
...@@ -311,17 +310,16 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -311,17 +310,16 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
return scrollOffset; return scrollOffset;
} }
bool _didRequestKeyboard = false;
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached; bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
void _openInputConnectionIfNeeded() { void _openInputConnection() {
if (!_hasInputConnection) { if (!_hasInputConnection) {
final TextEditingValue localValue = _value; final TextEditingValue localValue = _value;
_lastKnownRemoteTextEditingValue = localValue; _lastKnownRemoteTextEditingValue = localValue;
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType)) _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType))
..setEditingState(localValue) ..setEditingState(localValue);
..show();
} }
_textInputConnection.show();
} }
void _closeInputConnectionIfNeeded() { void _closeInputConnectionIfNeeded() {
...@@ -333,13 +331,12 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -333,13 +331,12 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
} }
void _openOrCloseInputConnectionIfNeeded() { void _openOrCloseInputConnectionIfNeeded() {
if (_hasFocus && _didRequestKeyboard) { if (_hasFocus && widget.focusNode.consumeKeyboardToken()) {
_openInputConnectionIfNeeded(); _openInputConnection();
} else if (!_hasFocus) { } else if (!_hasFocus) {
_closeInputConnectionIfNeeded(); _closeInputConnectionIfNeeded();
widget.controller.clearComposing(); widget.controller.clearComposing();
} }
_didRequestKeyboard = false;
} }
/// Express interest in interacting with the keyboard. /// Express interest in interacting with the keyboard.
...@@ -350,16 +347,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -350,16 +347,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
/// focus, the control will then attach to the keyboard and request that the /// focus, the control will then attach to the keyboard and request that the
/// keyboard become visible. /// keyboard become visible.
void requestKeyboard() { void requestKeyboard() {
if (_hasInputConnection) { if (_hasFocus)
_textInputConnection.show(); _openInputConnection();
} else { else
if (_hasFocus) { FocusScope.of(context).requestFocus(widget.focusNode);
_openInputConnectionIfNeeded();
} else {
_didRequestKeyboard = true;
FocusScope.of(context).requestFocus(widget.focusNode);
}
}
} }
void _hideSelectionOverlayIfNeeded() { void _hideSelectionOverlayIfNeeded() {
......
...@@ -31,6 +31,7 @@ import 'package:flutter/foundation.dart'; ...@@ -31,6 +31,7 @@ import 'package:flutter/foundation.dart';
class FocusNode extends ChangeNotifier { class FocusNode extends ChangeNotifier {
FocusScopeNode _parent; FocusScopeNode _parent;
FocusManager _manager; FocusManager _manager;
bool _hasKeyboardToken = false;
/// Whether this node has the overall focus. /// Whether this node has the overall focus.
/// ///
...@@ -48,6 +49,27 @@ class FocusNode extends ChangeNotifier { ...@@ -48,6 +49,27 @@ class FocusNode extends ChangeNotifier {
/// This object notifies its listeners whenever this value changes. /// This object notifies its listeners whenever this value changes.
bool get hasFocus => _manager?._currentFocus == this; 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. /// Cancels any outstanding requests for focus.
/// ///
/// This method is safe to call regardless of whether this node has ever /// This method is safe to call regardless of whether this node has ever
...@@ -216,13 +238,9 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin { ...@@ -216,13 +238,9 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin {
assert(node != null); assert(node != null);
if (_focus == node) if (_focus == node)
return; return;
assert(node._parent == null);
_focus?.unfocus(); _focus?.unfocus();
assert(_focus == null); node._hasKeyboardToken = true;
_focus = node; _setFocus(node);
_focus._parent = this;
_focus._manager = _manager;
_didChangeFocusChain();
} }
/// If this scope lacks a focus, request that the given node becomes the /// If this scope lacks a focus, request that the given node becomes the
...@@ -235,8 +253,10 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin { ...@@ -235,8 +253,10 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin {
/// microtask. /// microtask.
void autofocus(FocusNode node) { void autofocus(FocusNode node) {
assert(node != null); assert(node != null);
if (_focus == null) if (_focus == null) {
requestFocus(node); node._hasKeyboardToken = true;
_setFocus(node);
}
} }
/// Adopts the given node if it is focused in another scope. /// Adopts the given node if it is focused in another scope.
...@@ -250,7 +270,19 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin { ...@@ -250,7 +270,19 @@ class FocusScopeNode extends Object with TreeDiagnosticsMixin {
return; return;
node.unfocus(); node.unfocus();
assert(node._parent == null); 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) { void _resignFocus(FocusNode node) {
......
// 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'; ...@@ -21,24 +21,40 @@ const String _kTextInputClientChannel = 'flutter/textinputclient';
/// * [WidgetTester.showKeyboard], which uses this class to simulate showing the /// * [WidgetTester.showKeyboard], which uses this class to simulate showing the
/// popup keyboard and initializing its text. /// popup keyboard and initializing its text.
class TestTextInput { class TestTextInput {
/// Installs this object as a mock handler for [SystemChannels.textInput].
void register() { void register() {
SystemChannels.textInput.setMockMethodCallHandler(handleTextInputCall); SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall);
} }
int _client = 0; int _client = 0;
Map<String, dynamic> editingState; Map<String, dynamic> editingState;
Future<dynamic> handleTextInputCall(MethodCall methodCall) async { Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
switch (methodCall.method) { switch (methodCall.method) {
case 'TextInput.setClient': case 'TextInput.setClient':
_client = methodCall.arguments[0]; _client = methodCall.arguments[0];
break; break;
case 'TextInput.clearClient':
_client = 0;
_isVisible = false;
break;
case 'TextInput.setEditingState': case 'TextInput.setEditingState':
editingState = methodCall.arguments; editingState = methodCall.arguments;
break; 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) { void updateEditingValue(TextEditingValue value) {
expect(_client, isNonZero); expect(_client, isNonZero);
BinaryMessages.handlePlatformMessage( BinaryMessages.handlePlatformMessage(
...@@ -53,10 +69,16 @@ class TestTextInput { ...@@ -53,10 +69,16 @@ class TestTextInput {
); );
} }
/// Simulates the user typing the given text.
void enterText(String text) { void enterText(String text) {
updateEditingValue(new TextEditingValue( updateEditingValue(new TextEditingValue(
text: text, text: text,
composing: new TextRange(start: 0, end: text.length), composing: new TextRange(start: 0, end: text.length),
)); ));
} }
/// Simulates the user hiding the onscreen keyboard.
void hide() {
_isVisible = false;
}
} }
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