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 {
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,17 +347,11 @@ 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;
if (_hasFocus)
_openInputConnection();
else
FocusScope.of(context).requestFocus(widget.focusNode);
}
}
}
void _hideSelectionOverlayIfNeeded() {
_selectionOverlay?.hide();
......
......@@ -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) {
......
// 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;
}
}
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