Improve focus management (#9074)

We now have an explicit focus tree that we manage. Instead of using
GlobalKeys to manage focus, we use FocusNode and FocusScopeNode objects.
The FocusNode is Listenable and notifies when its focus state changes.

Focus notifications trigger by tree mutations are now delayed by one
frame, which is necessary to handle certain complex tree mutations. In
the common case of focus changes being triggered by user input, the
focus notificiation still arives in the same frame.
This commit is contained in:
Adam Barth 2017-03-31 13:10:37 -07:00 committed by GitHub
parent 60e05e9a0e
commit 89aaaa9c32
23 changed files with 1009 additions and 731 deletions

View file

@ -5,34 +5,37 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
GlobalKey _key = new GlobalKey();
void main() {
runApp(new MaterialApp(
title: "Hardware Key Demo",
title: 'Hardware Key Demo',
home: new Scaffold(
appBar: new AppBar(
title: new Text("Hardware Key Demo")
title: new Text('Hardware Key Demo'),
),
body: new Center(
child: new RawKeyboardDemo(
key: _key
)
)
)
child: new RawKeyboardDemo(),
),
),
));
}
class RawKeyboardDemo extends StatefulWidget {
RawKeyboardDemo({ GlobalKey key }) : super(key: key);
RawKeyboardDemo({ Key key }) : super(key: key);
@override
_HardwareKeyDemoState createState() => new _HardwareKeyDemoState();
}
class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
final FocusNode _focusNode = new FocusNode();
RawKeyEvent _event;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _handleKeyEvent(RawKeyEvent event) {
setState(() {
_event = event;
@ -42,44 +45,46 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final bool focused = Focus.at(context);
Widget child;
if (!focused) {
child = new GestureDetector(
onTap: () {
Focus.moveTo(config.key);
},
child: new Text('Tap to focus', style: textTheme.display1),
);
} else if (_event == null) {
child = new Text('Press a key', style: textTheme.display1);
} else {
int codePoint;
int keyCode;
int hidUsage;
final RawKeyEventData data = _event.data;
if (data is RawKeyEventDataAndroid) {
codePoint = data.codePoint;
keyCode = data.keyCode;
} else if (data is RawKeyEventDataFuchsia) {
codePoint = data.codePoint;
hidUsage = data.hidUsage;
}
child = new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('${_event.runtimeType}', style: textTheme.body2),
new Text('codePoint: $codePoint', style: textTheme.display4),
new Text('keyCode: $keyCode', style: textTheme.display4),
new Text('hidUsage: $hidUsage', style: textTheme.display4),
],
);
}
return new RawKeyboardListener(
focused: focused,
focusNode: _focusNode,
onKey: _handleKeyEvent,
child: child,
child: new AnimatedBuilder(
animation: _focusNode,
builder: (BuildContext context, Widget child) {
if (!_focusNode.hasFocus) {
return new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(_focusNode);
},
child: new Text('Tap to focus', style: textTheme.display1),
);
}
if (_event == null)
return new Text('Press a key', style: textTheme.display1);
int codePoint;
int keyCode;
int hidUsage;
final RawKeyEventData data = _event.data;
if (data is RawKeyEventDataAndroid) {
codePoint = data.codePoint;
keyCode = data.keyCode;
} else if (data is RawKeyEventDataFuchsia) {
codePoint = data.codePoint;
hidUsage = data.hidUsage;
}
return new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('${_event.runtimeType}', style: textTheme.body2),
new Text('codePoint: $codePoint', style: textTheme.display4),
new Text('keyCode: $keyCode', style: textTheme.display4),
new Text('hidUsage: $hidUsage', style: textTheme.display4),
],
);
},
),
);
}
}

View file

@ -26,3 +26,4 @@ export 'src/foundation/platform.dart';
export 'src/foundation/print.dart';
export 'src/foundation/serialization.dart';
export 'src/foundation/synchronous_future.dart';
export 'src/foundation/tree_diagnostics_mixin.dart';

View file

@ -0,0 +1,38 @@
// 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:meta/meta.dart';
/// A mixin that helps dump string representations of trees.
abstract class TreeDiagnosticsMixin {
@override
String toString() => '$runtimeType#$hashCode';
/// Returns a string representation of this node and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
final String childrenDescription = debugDescribeChildren(prefixOtherLines);
final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines ';
final List<String> description = <String>[];
debugFillDescription(description);
result += description.map((String description) => '$descriptionPrefix$description\n').join();
if (childrenDescription == '') {
final String prefix = prefixOtherLines.trimRight();
if (prefix != '')
result += '$prefix\n';
} else {
result += childrenDescription;
}
return result;
}
/// Add additional information to the given description for use by [toStringDeep].
@protected
@mustCallSuper
void debugFillDescription(List<String> description) { }
/// Returns a description of this node's children for use by [toStringDeep].
@protected
String debugDescribeChildren(String prefix) => '';
}

View file

@ -158,8 +158,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
}
LocalHistoryEntry _historyEntry;
// TODO(abarth): This should be a GlobalValueKey when those exist.
GlobalKey get _drawerKey => new GlobalObjectKey(config.key);
final FocusScopeNode _focusScopeNode = new FocusScopeNode();
void _ensureHistoryEntry() {
if (_historyEntry == null) {
@ -167,7 +166,7 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
if (route != null) {
_historyEntry = new LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
route.addLocalHistoryEntry(_historyEntry);
Focus.moveScopeTo(_drawerKey, context: context);
FocusScope.of(context).setFirstFocus(_focusScopeNode);
}
}
}
@ -210,10 +209,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
}
}
final GlobalKey _drawerKey = new GlobalKey();
double get _width {
final RenderBox drawerBox = _drawerKey.currentContext?.findRenderObject();
if (drawerBox != null)
return drawerBox.size.width;
final RenderBox box = _drawerKey.currentContext?.findRenderObject();
if (box != null)
return box.size.width;
return _kWidth; // drawer not being shown currently
}
@ -286,8 +287,9 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
alignment: FractionalOffset.centerRight,
widthFactor: _controller.value,
child: new RepaintBoundary(
child: new Focus(
child: new FocusScope(
key: _drawerKey,
node: _focusScopeNode,
child: config.child
),
),

View file

@ -43,7 +43,7 @@ const Curve _kTransitionCurve = Curves.fastOutSlowIn;
class InputField extends StatefulWidget {
InputField({
Key key,
this.focusKey,
this.focusNode,
this.value,
this.keyboardType: TextInputType.text,
this.hintText,
@ -56,7 +56,10 @@ class InputField extends StatefulWidget {
this.onSubmitted,
}) : super(key: key);
final GlobalKey focusKey;
/// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The current state of text of the input field. This includes the selected
/// text, if any, among other things.
@ -109,9 +112,15 @@ class InputField extends StatefulWidget {
class _InputFieldState extends State<InputField> {
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
final GlobalKey<EditableTextState> _focusKey = new GlobalKey(debugLabel: "_InputFieldState _focusKey");
GlobalKey get focusKey => config.focusKey ?? (config.key is GlobalKey ? config.key : _focusKey);
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
void requestKeyboard() {
_editableTextKey.currentState?.requestKeyboard();
@ -126,31 +135,22 @@ class _InputFieldState extends State<InputField> {
final List<Widget> stackChildren = <Widget>[
new GestureDetector(
key: focusKey == _focusKey ? _focusKey : null,
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
// Since the focusKey may have been created here, defer building the
// EditableText until the focusKey's context has been set. This is
// necessary because the EditableText will check the focus, like
// Focus.at(focusContext), when it builds.
child: new Builder(
builder: (BuildContext context) {
return new EditableText(
key: _editableTextKey,
value: value,
focusKey: focusKey,
style: textStyle,
obscureText: config.obscureText,
maxLines: config.maxLines,
autofocus: config.autofocus,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
);
}
child: new EditableText(
key: _editableTextKey,
value: value,
focusNode: _effectiveFocusNode,
style: textStyle,
obscureText: config.obscureText,
maxLines: config.maxLines,
autofocus: config.autofocus,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
),
];
@ -432,6 +432,7 @@ class Input extends StatefulWidget {
Input({
Key key,
this.value,
this.focusNode,
this.keyboardType: TextInputType.text,
this.icon,
this.labelText,
@ -451,6 +452,11 @@ class Input extends StatefulWidget {
/// text, if any, among other things.
final InputValue value;
/// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
@ -522,26 +528,30 @@ class Input extends StatefulWidget {
class _InputState extends State<Input> {
final GlobalKey<_InputFieldState> _inputFieldKey = new GlobalKey<_InputFieldState>();
final GlobalKey _focusKey = new GlobalKey();
GlobalKey get focusKey => config.key is GlobalKey ? config.key : _focusKey;
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
final FocusNode focusNode = _effectiveFocusNode;
return new GestureDetector(
key: focusKey == _focusKey ? _focusKey : null,
onTap: () {
_inputFieldKey.currentState?.requestKeyboard();
},
// Since the focusKey may have been created here, defer building the
// InputContainer until the focusKey's context has been set. This is
// necessary because we're passing the value of Focus.at() along.
child: new Builder(
builder: (BuildContext context) {
final bool focused = Focus.at(focusKey.currentContext, autofocus: config.autofocus);
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
child: new AnimatedBuilder(
animation: focusNode,
builder: (BuildContext context, Widget child) {
return new InputContainer(
focused: focused,
focused: focusNode.hasFocus,
isEmpty: isEmpty,
icon: config.icon,
labelText: config.labelText,
@ -550,20 +560,21 @@ class _InputState extends State<Input> {
style: config.style,
isDense: config.isDense,
showDivider: config.showDivider,
child: new InputField(
key: _inputFieldKey,
focusKey: focusKey,
value: config.value,
style: config.style,
obscureText: config.obscureText,
maxLines: config.maxLines,
autofocus: config.autofocus,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
child: child,
);
},
child: new InputField(
key: _inputFieldKey,
focusNode: focusNode,
value: config.value,
style: config.style,
obscureText: config.obscureText,
maxLines: config.maxLines,
autofocus: config.autofocus,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
),
);
}
@ -643,7 +654,7 @@ class _InputState extends State<Input> {
class TextField extends FormField<InputValue> {
TextField({
Key key,
GlobalKey focusKey,
FocusNode focusNode,
TextInputType keyboardType: TextInputType.text,
Icon icon,
String labelText,
@ -664,7 +675,7 @@ class TextField extends FormField<InputValue> {
validator: validator,
builder: (FormFieldState<InputValue> field) {
return new Input(
key: focusKey,
focusNode: focusNode,
keyboardType: keyboardType,
icon: icon,
labelText: labelText,

View file

@ -16,7 +16,7 @@ import 'debug.dart';
/// During painting, the render tree generates a tree of composited layers that
/// are uploaded into the engine and displayed by the compositor. This class is
/// the base class for all composited layers.
abstract class Layer {
abstract class Layer extends Object with TreeDiagnosticsMixin {
/// This layer's parent in the layer tree
ContainerLayer get parent => _parent;
ContainerLayer _parent;
@ -70,44 +70,18 @@ abstract class Layer {
/// origin of the builder's coordinate system.
void addToScene(ui.SceneBuilder builder, Offset layerOffset);
@override
String toString() => '$runtimeType';
/// The object responsible for creating this layer.
///
/// Defaults to the value of [RenderObject.debugCreator] for the render object
/// that created this layer. Used in debug messages.
dynamic debugCreator;
/// Returns a string representation of this layer and its descendants.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n';
final String childrenDescription = debugDescribeChildren(prefixOtherLines);
final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines ';
final List<String> description = <String>[];
debugFillDescription(description);
result += description.map((String description) => "$descriptionPrefix$description\n").join();
if (childrenDescription == '') {
final String prefix = prefixOtherLines.trimRight();
if (prefix != '')
result += '$prefix\n';
} else {
result += childrenDescription;
}
return result;
}
/// Add additional information to the given description for use by [toStringDeep].
@protected
@mustCallSuper
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (debugCreator != null)
description.add('creator: $debugCreator');
}
/// Returns a description of this layer's children for use by [toStringDeep].
@protected
String debugDescribeChildren(String prefix) => '';
}
/// A composited layer containing a [Picture]

View file

@ -14,6 +14,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'app.dart';
import 'focus_manager.dart';
import 'framework.dart';
export 'dart:ui' show AppLifecycleState, Locale;
@ -119,6 +120,14 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
BuildOwner get buildOwner => _buildOwner;
final BuildOwner _buildOwner = new BuildOwner();
/// The object in charge of the focus tree.
///
/// Rarely used directly. Instead, consider using [FocusScope.of] to obtain
/// the [FocusScopeNode] for a given [BuildContext].
///
/// See [FocusManager] for more details.
final FocusManager focusManager = new FocusManager();
final List<WidgetsBindingObserver> _observers = <WidgetsBindingObserver>[];
/// Registers the given object as a binding observer. Binding

View file

@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'focus.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'media_query.dart';
import 'scroll_controller.dart';
@ -125,11 +126,12 @@ class InputValue {
class EditableText extends StatefulWidget {
/// Creates a basic text input control.
///
/// The [value] argument must not be null.
/// The [value], [focusNode], [style], and [cursorColor] arguments must not
/// be null.
EditableText({
Key key,
@required this.value,
@required this.focusKey,
@required this.focusNode,
this.obscureText: false,
@required this.style,
@required this.cursorColor,
@ -143,7 +145,7 @@ class EditableText extends StatefulWidget {
this.onSubmitted,
}) : super(key: key) {
assert(value != null);
assert(focusKey != null);
assert(focusNode != null);
assert(obscureText != null);
assert(style != null);
assert(cursorColor != null);
@ -154,8 +156,8 @@ class EditableText extends StatefulWidget {
/// The string being displayed in this widget.
final InputValue value;
/// Key of the enclosing widget that holds the focus.
final GlobalKey focusKey;
/// Controls whether this widget has keyboard focus.
final FocusNode focusNode;
/// Whether to hide the text being edited (e.g., for passwords).
///
@ -217,11 +219,23 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = new ScrollController();
bool _didAutoFocus = false;
@override
void initState() {
super.initState();
_currentValue = config.value;
config.focusNode.addListener(_handleFocusChanged);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutoFocus && config.autofocus) {
_didRequestKeyboard = true;
FocusScope.of(context).autofocus(config.focusNode);
_didAutoFocus = true;
}
}
@override
@ -231,6 +245,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (_isAttachedToKeyboard)
_textInputConnection.setEditingState(_getTextEditingValueFromInputValue(_currentValue));
}
if (config.focusNode != oldConfig.focusNode) {
oldConfig.focusNode.removeListener(_handleFocusChanged);
config.focusNode.addListener(_handleFocusChanged);
}
}
bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached;
@ -250,12 +268,10 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
return scrollOffset;
}
// True if the focus was explicitly requested last frame. This ensures we
// don't show the keyboard when focus defaults back to the EditableText.
bool _requestingFocus = false;
bool _didRequestKeyboard = false;
void _attachOrDetachKeyboard(bool focused) {
if (focused && !_isAttachedToKeyboard && (_requestingFocus || config.autofocus)) {
if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) {
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
..setEditingState(_getTextEditingValueFromInputValue(_currentValue))
..show();
@ -266,7 +282,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
}
_clearComposing();
}
_requestingFocus = false;
_didRequestKeyboard = false;
}
void _clearComposing() {
@ -286,10 +302,11 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (_isAttachedToKeyboard) {
_textInputConnection.show();
} else {
Focus.moveTo(config.focusKey);
setState(() {
_requestingFocus = true;
});
_didRequestKeyboard = true;
if (config.focusNode.hasFocus)
_attachOrDetachKeyboard(true);
else
FocusScope.of(context).requestFocus(config.focusNode);
}
}
@ -307,7 +324,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
@override
void performAction(TextInputAction action) {
_clearComposing();
Focus.clear(context);
config.focusNode.unfocus();
if (config.onSubmitted != null)
config.onSubmitted(_currentValue);
}
@ -369,30 +386,8 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
_cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
@override
void dispose() {
if (_isAttachedToKeyboard) {
_textInputConnection.close();
_textInputConnection = null;
}
assert(!_isAttachedToKeyboard);
if (_cursorTimer != null)
_stopCursorTimer();
assert(_cursorTimer == null);
_selectionOverlay?.dispose();
_selectionOverlay = null;
super.dispose();
}
void _stopCursorTimer() {
_cursorTimer.cancel();
_cursorTimer = null;
_showCursor = false;
}
@override
Widget build(BuildContext context) {
final bool focused = Focus.at(config.focusKey.currentContext);
void _handleFocusChanged() {
final bool focused = config.focusNode.hasFocus;
_attachOrDetachKeyboard(focused);
if (_cursorTimer == null && focused && config.value.selection.isCollapsed)
@ -408,7 +403,33 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
_selectionOverlay = null;
}
}
}
@override
void dispose() {
if (_isAttachedToKeyboard) {
_textInputConnection.close();
_textInputConnection = null;
}
assert(!_isAttachedToKeyboard);
if (_cursorTimer != null)
_stopCursorTimer();
assert(_cursorTimer == null);
_selectionOverlay?.dispose();
_selectionOverlay = null;
config.focusNode.removeListener(_handleFocusChanged);
super.dispose();
}
void _stopCursorTimer() {
_cursorTimer.cancel();
_cursorTimer = null;
_showCursor = false;
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(config.focusNode);
return new Scrollable(
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController,

View file

@ -1,386 +0,0 @@
// Copyright 2015 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 'dart:async';
import 'package:flutter/foundation.dart';
import 'basic.dart';
import 'framework.dart';
import 'media_query.dart';
import 'scrollable.dart';
// _noFocusedScope is used by Focus to track the case where none of the Focus
// widget's subscopes (e.g. dialogs) are focused. This is distinct from the
// focused scope being null, which means that we haven't yet decided which scope
// is focused and whichever is the first scope to ask for focus will get it.
final GlobalKey _noFocusedScope = new GlobalKey();
class _FocusScope extends InheritedWidget {
_FocusScope({
Key key,
this.focusState,
@required this.scopeFocused,
this.focusedScope,
this.focusedWidget,
@required Widget child,
}) : super(key: key, child: child) {
assert(scopeFocused != null);
}
/// The state for this focus scope.
///
/// This widget is always our direct parent widget.
final _FocusState focusState;
/// Whether this scope is focused in our ancestor focus scope.
final bool scopeFocused;
// These are mutable because we implicitly change them when they're null in
// certain cases, basically pretending retroactively that we were constructed
// with the right keys.
/// Which of our descendant scopes is focused, if any.
GlobalKey focusedScope;
/// Which of our descendant widgets is focused, if any.
GlobalKey focusedWidget;
// The _setFocusedWidgetIfUnset() methodsdon't need to notify descendants
// because by definition they are only going to make a change the very first
// time that our state is checked.
void _setFocusedWidgetIfUnset(GlobalKey key) {
focusState._setFocusedWidgetIfUnset(key);
focusedWidget = focusState._focusedWidget;
focusedScope = focusState._focusedScope == _noFocusedScope ? null : focusState._focusedScope;
}
@override
bool updateShouldNotify(_FocusScope oldWidget) {
if (scopeFocused != oldWidget.scopeFocused)
return true;
if (!scopeFocused)
return false;
if (focusedScope != oldWidget.focusedScope)
return true;
if (focusedScope != null)
return false;
if (focusedWidget != oldWidget.focusedWidget)
return true;
return false;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (scopeFocused)
description.add('this scope has focus');
if (focusedScope != null)
description.add('focused subscope: $focusedScope');
if (focusedWidget != null)
description.add('focused widget: $focusedWidget');
}
}
/// A scope for managing the focus state of descendant widgets.
///
/// The focus represents where the user's attention is directed. If the use
/// interacts with the system in a way that isn't visually directed at a
/// particular widget (e.g., by typing on a keyboard), the interaction is
/// directed to the currently focused widget.
///
/// The focus system consists of a tree of Focus widgets, which is embedded in
/// the widget tree. Focus widgets themselves can be focused in their enclosing
/// Focus widget, which means that their subtree is the one that has the current
/// focus. For example, a dialog creates a Focus widget to maintain focus
/// within the dialog. When the dialog closes, its Focus widget is removed from
/// the tree and focus is restored to whichever other part of the Focus tree
/// previously had focus.
///
/// In addition to tracking which enclosed Focus widget has focus, each Focus
/// widget also tracks a GlobalKey, which represents the currently focused
/// widget in this part of the focus tree. If this Focus widget is the currently
/// focused subtree of the focus system (i.e., the path from it to the root is
/// focused at each level and it hasn't focused any of its enclosed Focus
/// widgets), then the widget with this global key actually has the focus in the
/// entire system.
class Focus extends StatefulWidget {
/// Creates a scope for managing focus.
///
/// The [key] argument must not be null.
Focus({
@required GlobalKey key,
this.initiallyFocusedScope,
@required this.child,
}) : super(key: key) {
assert(key != null);
}
/// The global key of the [Focus] widget below this widget in the tree that
/// will be focused initially.
///
/// If non-null, a [Focus] widget with this key must be added to the tree
/// before the end of the current microtask in which the [Focus] widget was
/// initially constructed.
final GlobalKey initiallyFocusedScope;
/// The widget below this widget in the tree.
final Widget child;
/// The key that currently has focus globally in the entire focus tree.
///
/// This field is always null except in checked mode.
static GlobalKey debugOnlyFocusedKey;
/// Whether the focus is current at the given context.
///
/// If autofocus is true, the given context will become focused if no other
/// widget is already focused.
static bool at(BuildContext context, { bool autofocus: false }) {
assert(context != null);
assert(context.widget != null);
assert(context.widget.key != null);
assert(context.widget.key is GlobalKey);
final _FocusScope focusScope = context.inheritFromWidgetOfExactType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedWidgetIfUnset(context.widget.key);
return focusScope.scopeFocused &&
focusScope.focusedScope == null &&
focusScope.focusedWidget == context.widget.key;
}
assert(() {
if (debugOnlyFocusedKey?.currentContext == null)
debugOnlyFocusedKey = context.widget.key;
if (debugOnlyFocusedKey != context.widget.key) {
throw new FlutterError(
'Missing Focus scope.\n'
'Two focusable widgets with different keys, $debugOnlyFocusedKey and ${context.widget.key}, '
'exist in the widget tree simultaneously, but they have no Focus widget ancestor.\n'
'If you have more than one focusable widget, then you should put them inside a Focus. '
'Normally, this is done for you using a Route, via Navigator, WidgetsApp, or MaterialApp.'
);
}
return true;
});
return true;
}
static bool _atScope(BuildContext context) {
assert(context != null);
assert(context.widget != null);
assert(context.widget is Focus);
assert(context.widget.key != null);
final _FocusScope focusScope = context.inheritFromWidgetOfExactType(_FocusScope);
if (focusScope != null) {
return focusScope.scopeFocused &&
focusScope.focusedScope == context.widget.key;
}
return true;
}
/// Focuses a particular widget, identified by its GlobalKey.
/// The widget must be in the widget tree.
///
/// Don't call moveTo() from your build() functions, it's intended to be
/// called from event listeners, e.g. in response to a finger tap or tab key.
static void moveTo(GlobalKey key) {
final BuildContext focusedContext = key.currentContext;
assert(focusedContext != null);
final _FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope);
if (focusScope != null) {
focusScope.focusState._setFocusedWidget(key);
Scrollable.ensureVisible(focusedContext);
}
}
/// Unfocuses the currently focused widget (if any) in the Focus that most
/// tightly encloses the given context.
static void clear(BuildContext context) {
final _FocusScope focusScope = context.ancestorWidgetOfExactType(_FocusScope);
if (focusScope != null)
focusScope.focusState._clearFocusedWidget();
}
/// Focuses a particular focus scope, identified by its GlobalKey.
///
/// Don't call moveScopeTo() from your build() functions, it's intended to be
/// called from event listeners, e.g. in response to a finger tap or tab key.
static void moveScopeTo(GlobalKey key, { BuildContext context }) {
_FocusScope focusScope;
final BuildContext searchContext = key.currentContext;
if (searchContext != null) {
assert(key.currentWidget is Focus);
focusScope = searchContext.ancestorWidgetOfExactType(_FocusScope);
assert(context == null || focusScope == context.ancestorWidgetOfExactType(_FocusScope));
} else {
focusScope = context.ancestorWidgetOfExactType(_FocusScope);
}
if (focusScope != null)
focusScope.focusState._setFocusedScope(key);
}
@override
_FocusState createState() => new _FocusState();
}
class _FocusState extends State<Focus> {
@override
void initState() {
super.initState();
_focusedScope = config.initiallyFocusedScope;
_updateWidgetRemovalListener(_focusedWidget);
_updateScopeRemovalListener(_focusedScope);
assert(() {
if (_focusedScope != null)
scheduleMicrotask(_debugCheckInitiallyFocusedScope);
return true;
});
}
@override
void dispose() {
_updateWidgetRemovalListener(null);
_updateScopeRemovalListener(null);
super.dispose();
}
void _debugCheckInitiallyFocusedScope() {
assert(config.initiallyFocusedScope != null);
assert(() {
if (!mounted)
return true;
final Widget widget = config.initiallyFocusedScope.currentWidget;
if (widget == null) {
throw new FlutterError(
'The initially focused scope is not in the tree.\n'
'When a Focus widget is given an initially focused scope, that focus '
'scope must be added to the tree before the end of the microtask in '
'which the Focus widget was first built. However, it is the end of '
'the microtask and ${config.initiallyFocusedScope} is not in the '
'tree.'
);
}
if (widget is! Focus) {
throw new FlutterError(
'The initially focused scope was not a Focus widget.\n'
'The initially focused scope for a Focus widget must be another '
'Focus widget. Instead, the initially focused scope was a '
'${widget.runtimeType} widget.'
);
}
return true;
});
}
GlobalKey _focusedWidget; // when null, the first widget to ask if it's focused will get the focus
GlobalKey _currentlyRegisteredWidgetRemovalListenerKey;
void _setFocusedWidget(GlobalKey key) {
setState(() {
_focusedWidget = key;
if (_focusedScope == null)
_focusedScope = _noFocusedScope;
});
_updateWidgetRemovalListener(key);
}
void _setFocusedWidgetIfUnset(GlobalKey key) {
if (_focusedWidget == null && (_focusedScope == null || _focusedScope == _noFocusedScope)) {
_focusedWidget = key;
_focusedScope = _noFocusedScope;
_updateWidgetRemovalListener(key);
}
}
void _clearFocusedWidget() {
if (_focusedWidget != null) {
_updateWidgetRemovalListener(null);
setState(() {
_focusedWidget = null;
});
}
}
void _handleWidgetRemoved(GlobalKey key) {
assert(key != null);
assert(_focusedWidget == key);
_clearFocusedWidget();
}
void _updateWidgetRemovalListener(GlobalKey key) {
if (_currentlyRegisteredWidgetRemovalListenerKey != key) {
if (_currentlyRegisteredWidgetRemovalListenerKey != null)
GlobalKey.unregisterRemoveListener(_currentlyRegisteredWidgetRemovalListenerKey, _handleWidgetRemoved);
if (key != null)
GlobalKey.registerRemoveListener(key, _handleWidgetRemoved);
_currentlyRegisteredWidgetRemovalListenerKey = key;
}
}
GlobalKey _focusedScope; // when null, the first scope to ask if it's focused will get the focus
GlobalKey _currentlyRegisteredScopeRemovalListenerKey;
void _setFocusedScope(GlobalKey key) {
setState(() {
_focusedScope = key;
});
_updateScopeRemovalListener(key);
}
void _scopeRemoved(GlobalKey key) {
assert(_focusedScope == key);
GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
_currentlyRegisteredScopeRemovalListenerKey = null;
setState(() {
_focusedScope = null;
});
}
void _updateScopeRemovalListener(GlobalKey key) {
if (_currentlyRegisteredScopeRemovalListenerKey != key) {
if (_currentlyRegisteredScopeRemovalListenerKey != null)
GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
if (key != null)
GlobalKey.registerRemoveListener(key, _scopeRemoved);
_currentlyRegisteredScopeRemovalListenerKey = key;
}
}
Size _mediaSize;
EdgeInsets _mediaPadding;
void _ensureVisibleIfFocused() {
if (!Focus._atScope(context))
return;
final BuildContext focusedContext = _focusedWidget?.currentContext;
if (focusedContext == null)
return;
Scrollable.ensureVisible(focusedContext);
}
@override
Widget build(BuildContext context) {
final MediaQueryData data = MediaQuery.of(context);
final Size newMediaSize = data.size;
final EdgeInsets newMediaPadding = data.padding;
if (newMediaSize != _mediaSize || newMediaPadding != _mediaPadding) {
_mediaSize = newMediaSize;
_mediaPadding = newMediaPadding;
scheduleMicrotask(_ensureVisibleIfFocused);
}
return new Semantics(
container: true,
child: new _FocusScope(
focusState: this,
scopeFocused: Focus._atScope(context),
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
focusedWidget: _focusedWidget,
child: config.child
)
);
}
}

View file

@ -0,0 +1,422 @@
// 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 'dart:async';
import 'package:flutter/foundation.dart';
/// A leaf node in the focus tree that can receive focus.
///
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
/// To request focus, find the [FocusScopeNode] for the current [BuildContext]
/// and call the [FocusScopeNode.requestFocus] method:
///
/// ```dart
/// FocusScope.of(context).requestFocus(focusNode);
/// ```
///
/// If your widget requests focus, be sure to call
/// `FocusScope.of(context).reparentIfNeeded(focusNode);` in your `build`
/// method to reparent your [FocusNode] if your widget moves from one
/// location in the tree to another.
///
/// See also:
///
/// * [FocusScopeNode], which is an interior node in the focus tree.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// [BuildContext].
class FocusNode extends ChangeNotifier {
FocusScopeNode _parent;
FocusManager _manager;
/// Whether this node has the overall focus.
///
/// A [FocusNode] has the overall focus when the node is focused in its
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
/// that scope and all its ancestor scopes.
///
/// To request focus, find the [FocusScopeNode] for the current [BuildContext]
/// and call the [FocusScopeNode.requestFocus] method:
///
/// ```dart
/// FocusScope.of(context).requestFocus(focusNode);
/// ```
///
/// This object notifies its listeners whenever this value changes.
bool get hasFocus => _manager?._currentFocus == this;
/// Cancels any oustanding requests for focus.
///
/// This method is safe to call regardless of whether this node has ever
/// requested focus.
void unfocus() {
_parent?._resignFocus(this);
assert(_parent == null);
assert(_manager == null);
}
@override
void dispose() {
_manager?._willDisposeFocusNode(this);
_parent?._resignFocus(this);
assert(_parent == null);
assert(_manager == null);
super.dispose();
}
void _notify() {
notifyListeners();
}
@override
String toString() => '$runtimeType#$hashCode${hasFocus ? '(FOCUSED)' : ''}';
}
/// An interior node in the focus tree.
///
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
/// The interior nodes in the focus tree cannot themselves be focused but
/// instead remember previous focus states. A scope is currently active in its
/// parent whenever [isFirstFocus] is true. If that scope is detached from its
/// parent, its previous sibling becomes the parent's first focus.
///
/// A [FocusNode] has the overall focus when the node is focused in its
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
/// that scope and all its ancestor scopes.
///
/// See also:
///
/// * [FocusNode], which is a leaf node in the focus tree that can receive
/// focus.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// [BuildContext].
/// * [FocusScope], which is a widget that associates a [FocusScopeNode] with
/// its location in the tree.
class FocusScopeNode extends Object with TreeDiagnosticsMixin {
FocusManager _manager;
FocusScopeNode _parent;
FocusScopeNode _nextSibling;
FocusScopeNode _previousSibling;
FocusScopeNode _firstChild;
FocusScopeNode _lastChild;
FocusNode _focus;
/// Whether this scope is currently active in its parent scope.
bool get isFirstFocus => _parent == null || _parent._firstChild == this;
void _prepend(FocusScopeNode child) {
assert(child != this);
assert(child != _firstChild);
assert(child != _lastChild);
assert(child._parent == null);
assert(child._manager == null);
assert(child._nextSibling == null);
assert(child._previousSibling == null);
assert(() {
FocusScopeNode node = this;
while (node._parent != null)
node = node._parent;
assert(node != child); // indicates we are about to create a cycle
return true;
});
child._parent = this;
child._nextSibling = _firstChild;
if (_firstChild != null)
_firstChild._previousSibling = child;
_firstChild = child;
if (_lastChild == null)
_lastChild = child;
child._updateManager(_manager);
}
void _updateManager(FocusManager manager) {
void update(FocusScopeNode child) {
if (child._manager == manager)
return;
child._manager = manager;
// We don't proactively null out the manager for FocusNodes because the
// manager holds the currently active focus node until the end of the
// microtask, even if that node is detached from the focus tree.
if (manager != null)
child._focus?._manager = manager;
child._visitChildren(update);
}
update(this);
}
void _visitChildren(void visitor(FocusScopeNode child)) {
FocusScopeNode child = _firstChild;
while (child != null) {
visitor(child);
child = child._nextSibling;
}
}
bool _debugUltimatePreviousSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) {
while (child._previousSibling != null) {
assert(child._previousSibling != child);
child = child._previousSibling;
}
return child == equals;
}
bool _debugUltimateNextSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) {
while (child._nextSibling != null) {
assert(child._nextSibling != child);
child = child._nextSibling;
}
return child == equals;
}
void _remove(FocusScopeNode child) {
assert(child._parent == this);
assert(child._manager == _manager);
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild));
if (child._previousSibling == null) {
assert(_firstChild == child);
_firstChild = child._nextSibling;
} else {
child._previousSibling._nextSibling = child._nextSibling;
}
if (child._nextSibling == null) {
assert(_lastChild == child);
_lastChild = child._previousSibling;
} else {
child._nextSibling._previousSibling = child._previousSibling;
}
child._previousSibling = null;
child._nextSibling = null;
child._parent = null;
child._updateManager(null);
}
void _didChangeFocusChain() {
if (isFirstFocus)
_manager?._markNeedsUpdate();
}
/// Requests that the given node becomes the focus for this scope.
///
/// If the given node is currently focused in another scope, the node will
/// first be unfocused in that scope.
///
/// The node will receive the overall focus if this [isFirstFocus] is true
/// in this scope and all its ancestor scopes. The node is notified that it
/// has received the overall focus in a microtask.
void requestFocus(FocusNode node) {
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();
}
/// If this scope lacks a focus, request that the given node becomes the
/// focus.
///
/// Useful for widgets that wish to grab the focus if no other widget already
/// has the focus.
///
/// The node is notified that it has received the overall focus in a
/// microtask.
void autofocus(FocusNode node) {
assert(node != null);
if (_focus == null)
requestFocus(node);
}
/// Adopts the given node if it is focused in another scope.
///
/// A widget that requests that a node is focused should call this method
/// during its `build` method in case the widget is moved from one location
/// in the tree to another location that has a different focus scope.
void reparentIfNeeded(FocusNode node) {
assert(node != null);
if (node._parent == null || node._parent == this)
return;
node.unfocus();
assert(node._parent == null);
autofocus(node);
}
void _resignFocus(FocusNode node) {
assert(node != null);
if (_focus != node)
return;
_focus._parent = null;
_focus._manager = null;
_focus = null;
_didChangeFocusChain();
}
/// Makes the given child the first focus of this scope.
///
/// If the child has another parent scope, the child is first removed from
/// that scope. After this method returns [isFirstFocus] will be true for
/// the child.
void setFirstFocus(FocusScopeNode child) {
assert(child != null);
assert(child._parent == null || child._parent == this);
if (_firstChild == child)
return;
child.detach();
_prepend(child);
assert(child._parent == this);
_didChangeFocusChain();
}
/// Adopts the given scope if it is the first focus of another scope.
///
/// A widget that sets a scope as the first focus of another scope should
/// call this method during its `build` method in case the widget is moved
/// from one location in the tree to another location that has a different
/// focus scope.
///
/// If the given scope is not the first focus of its old parent, the scope
/// is simply detached from its old parent.
void reparentScopeIfNeeded(FocusScopeNode child) {
assert(child != null);
if (child._parent == null || child._parent == this)
return;
if (child.isFirstFocus)
setFirstFocus(child);
else
child.detach();
}
/// Remove this scope from its parent child list.
///
/// This method is safe to call even if this scope does not have a parent.
///
/// A widget that sets a scope as the first focus of another scope should
/// call this method during [State.dispose] to avoid leaving dangling
/// children in their parent scope.
void detach() {
_didChangeFocusChain();
_parent?._remove(this);
assert(_parent == null);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (_focus != null)
description.add('focus: $_focus');
}
@override
String debugDescribeChildren(String prefix) {
final StringBuffer buffer = new StringBuffer();
if (_firstChild != null) {
FocusScopeNode child = _firstChild;
int count = 1;
while (child != _lastChild) {
buffer.write(child.toStringDeep("$prefix \u251C\u2500child $count: ", "$prefix \u2502"));
count += 1;
child = child._nextSibling;
}
if (child != null) {
assert(child == _lastChild);
buffer.write(child.toStringDeep("$prefix \u2514\u2500child $count: ", "$prefix "));
}
}
return buffer.toString();
}
}
/// Manages the focus tree.
///
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
/// The focus manager is responsible for holding the [FocusScopeNode] that is
/// the root of the focus tree and tracking which [FocusNode] has the overall
/// focus.
///
/// The [FocusManager] is held by the [WidgetBinding] as
/// [WidgetBinding.focusManager]. The [FocusManager] is rarely accessed
/// directly. Instead, to find the [FocusScopeNode] for a given [BuildContext],
/// use [FocusScope.of].
///
/// See also:
///
/// * [FocusNode], which is a leaf node in the focus tree that can receive
/// focus.
/// * [FocusScopeNode], which is an interior node in the focus tree.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// [BuildContext].
class FocusManager {
/// Creates an object that manages the focus tree.
///
/// This constructor is rarely called directly. To access the [FocusManager],
/// consider using [WidgetBinding.focusManager] instead.
FocusManager() {
rootScope._manager = this;
assert(rootScope._firstChild == null);
assert(rootScope._lastChild == null);
}
/// The root [FocusScopeNode] in the focus tree.
///
/// This field is rarely used direction. Instead, to find the
/// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of].
final FocusScopeNode rootScope = new FocusScopeNode();
FocusNode _currentFocus;
void _willDisposeFocusNode(FocusNode node) {
assert(node != null);
if (_currentFocus == node)
_currentFocus = null;
}
bool _haveScheduledUpdate = false;
void _markNeedsUpdate() {
if (_haveScheduledUpdate)
return;
_haveScheduledUpdate = true;
scheduleMicrotask(_update);
}
FocusNode _findNextFocus() {
FocusScopeNode scope = rootScope;
while (scope._firstChild != null)
scope = scope._firstChild;
return scope._focus;
}
void _update() {
_haveScheduledUpdate = false;
final FocusNode nextFocus = _findNextFocus();
if (_currentFocus == nextFocus)
return;
final FocusNode previousFocus = _currentFocus;
_currentFocus = nextFocus;
previousFocus?._notify();
_currentFocus?._notify();
}
@override
String toString() {
final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : '';
final String indent = ' ';
return '$runtimeType#$hashCode$status\n'
'${indent}currentFocus: $_currentFocus\n'
'${rootScope.toStringDeep(indent, indent)}';
}
}

View file

@ -0,0 +1,114 @@
// Copyright 2015 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/foundation.dart';
import 'basic.dart';
import 'binding.dart';
import 'focus_manager.dart';
import 'framework.dart';
class _FocusScopeMarker extends InheritedWidget {
_FocusScopeMarker({
Key key,
@required this.node,
Widget child,
}) : super(key: key, child: child) {
assert(node != null);
}
final FocusScopeNode node;
@override
bool updateShouldNotify(_FocusScopeMarker oldWidget) {
return node != oldWidget.node;
}
}
/// Establishes a scope in which widgets can receive focus.
///
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
/// The a focus scope does not itself receive focus but instead helps remember
/// previous focus states. A scope is currently active when its [node] is the
/// first focus of its parent scope. To activate a [FocusScope], either use the
/// [autofocus] property or explicitly make the [node] the first focus in the
/// parent scope:
///
/// ```dart
/// FocusScope.of(context).setFirstFocus(node);
/// ```
///
/// When a [FocusScope] is removed from the tree, the previously active
/// [FocusScope] becomes active again.
///
/// See also:
///
/// * [FocusScopeNode], which is the associated node in the focus tree.
/// * [FocusNode], which is a leaf node in the focus tree that can receive
/// focus.
class FocusScope extends StatefulWidget {
/// Creates a scope in which widgets can receive focus.
///
/// The [node] argument must not be null.
FocusScope({
Key key,
@required this.node,
this.autofocus: false,
this.child,
}) : super(key: key) {
assert(node != null);
assert(autofocus != null);
}
/// Controls whether this scope is currently active.
final FocusScopeNode node;
/// Whether this scope should attempt to become active when first added to
/// the tree.
final bool autofocus;
/// The widget below this widget in the tree.
final Widget child;
static FocusScopeNode of(BuildContext context) {
final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker);
return scope?.node ?? WidgetsBinding.instance.focusManager.rootScope;
}
@override
_FocusScopeState createState() => new _FocusScopeState();
}
class _FocusScopeState extends State<FocusScope> {
bool _didAutofocus = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutofocus && config.autofocus) {
FocusScope.of(context).setFirstFocus(config.node);
_didAutofocus = true;
}
}
@override
void dispose() {
config.node.detach();
super.dispose();
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentScopeIfNeeded(config.node);
return new Semantics(
container: true,
child: new _FocusScopeMarker(
node: config.node,
child: config.child,
),
);
}
}

View file

@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'binding.dart';
import 'focus.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'overlay.dart';
import 'ticker_provider.dart';
@ -28,12 +29,6 @@ abstract class Route<T> {
/// The overlay entries for this route.
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
/// The key this route will use for its root [Focus] widget, if any.
///
/// If this route is the first route shown by the navigator, the navigator
/// will initialize its [Focus] to this key.
GlobalKey get focusKey => null;
/// A future that completes when this route is popped off the navigator.
///
/// The future completes with the value given to [Navigator.pop], if any.
@ -700,6 +695,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final List<Route<dynamic>> _history = <Route<dynamic>>[];
final Set<Route<dynamic>> _poppedRoutes = new Set<Route<dynamic>>();
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = new FocusScopeNode();
@override
void initState() {
super.initState();
@ -736,6 +734,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.dispose();
_poppedRoutes.clear();
_history.clear();
focusScopeNode.detach();
super.dispose();
assert(() { _debugLocked = false; return true; });
}
@ -1112,10 +1111,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
WidgetsBinding.instance.cancelPointer(pointer);
}
// TODO(abarth): We should be able to take a focusScopeKey as configuration
// information in case our parent wants to control whether we are focused.
final GlobalKey _focusScopeKey = new GlobalKey();
@override
Widget build(BuildContext context) {
assert(!_debugLocked);
@ -1127,9 +1122,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
onPointerCancel: _handlePointerUpOrCancel,
child: new AbsorbPointer(
absorbing: false,
child: new Focus(
key: _focusScopeKey,
initiallyFocusedScope: initialRoute.focusKey,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new Overlay(
key: _overlayKey,
initialEntries: initialRoute.overlayEntries,

View file

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'focus_manager.dart';
import 'framework.dart';
/// A widget that calls a callback whenever the user presses or releases a key
@ -29,18 +30,16 @@ class RawKeyboardListener extends StatefulWidget {
/// on-screen keyboards and input method editors (IMEs).
RawKeyboardListener({
Key key,
this.focused: false,
this.onKey,
@required this.focusNode,
@required this.onKey,
@required this.child,
}) : super(key: key) {
assert(focusNode != null);
assert(child != null);
}
/// Whether this widget should actually listen for raw keyboard events.
///
/// Typically set to the value returned by [Focus.at] for the [GlobalKey] of
/// the widget that builds the raw keyboard listener.
final bool focused;
/// Controls whether this widget has keyboard focus.
final FocusNode focusNode;
/// Called whenever this widget receives a raw keyboard event.
final ValueChanged<RawKeyEvent> onKey;
@ -56,22 +55,26 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
@override
void initState() {
super.initState();
_attachOrDetachKeyboard();
config.focusNode.addListener(_handleFocusChanged);
}
@override
void didUpdateConfig(RawKeyboardListener oldConfig) {
_attachOrDetachKeyboard();
if (config.focusNode != oldConfig.focusNode) {
oldConfig.focusNode.removeListener(_handleFocusChanged);
config.focusNode.addListener(_handleFocusChanged);
}
}
@override
void dispose() {
config.focusNode.removeListener(_handleFocusChanged);
_detachKeyboardIfAttached();
super.dispose();
}
void _attachOrDetachKeyboard() {
if (config.focused)
void _handleFocusChanged() {
if (config.focusNode.hasFocus)
_attachKeyboardIfDetached();
else
_detachKeyboardIfAttached();

View file

@ -7,7 +7,8 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'basic.dart';
import 'focus.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'modal_barrier.dart';
import 'navigator.dart';
@ -449,8 +450,8 @@ class _ModalScopeState extends State<_ModalScope> {
@override
Widget build(BuildContext context) {
return new Focus(
key: config.route.focusKey,
return new FocusScope(
node: config.route.focusScopeNode,
child: new Offstage(
offstage: config.route.offstage,
child: new IgnorePointer(
@ -575,8 +576,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child;
}
@override
GlobalKey get focusKey => new GlobalObjectKey(this);
/// The node this route will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = new FocusScopeNode();
@override
void install(OverlayEntry insertionPoint) {
@ -587,27 +588,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override
Future<Null> didPush() {
if (!settings.isInitialRoute) {
final BuildContext overlayContext = navigator.overlay?.context;
assert(() {
if (overlayContext == null) {
throw new FlutterError(
'Unable to find the BuildContext for the Navigator\'s overlay.\n'
'Did you remember to pass the settings object to the route\'s '
'constructor in your onGenerateRoute callback?'
);
}
return true;
});
Focus.moveScopeTo(focusKey, context: overlayContext);
}
navigator.focusScopeNode.setFirstFocus(focusScopeNode);
return super.didPush();
}
@override
void didPopNext(Route<dynamic> nextRoute) {
Focus.moveScopeTo(focusKey, context: navigator.overlay.context);
super.didPopNext(nextRoute);
void dispose() {
focusScopeNode.detach();
super.dispose();
}
// The API for subclasses to override - used by this class

View file

@ -21,7 +21,8 @@ export 'src/widgets/debug.dart';
export 'src/widgets/dismissible.dart';
export 'src/widgets/drag_target.dart';
export 'src/widgets/editable_text.dart';
export 'src/widgets/focus.dart';
export 'src/widgets/focus_manager.dart';
export 'src/widgets/focus_scope.dart';
export 'src/widgets/form.dart';
export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart';

View file

@ -0,0 +1,58 @@
// 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/foundation.dart';
import 'package:test/test.dart';
class TestTree extends Object with TreeDiagnosticsMixin {
TestTree({
this.name,
this.children: const <TestTree>[],
});
final String name;
final List<TestTree> children;
@override
String debugDescribeChildren(String prefix) {
final StringBuffer buffer = new StringBuffer();
for (TestTree child in children)
buffer.write(child.toStringDeep('$prefix \u251C\u2500child ${child.name}: ', '$prefix \u2502'));
return buffer.toString();
}
}
void main() {
test('TreeDiagnosticsMixin control test', () async {
final TestTree tree = new TestTree(
children: <TestTree>[
new TestTree(name: 'node A'),
new TestTree(
name: 'node B',
children: <TestTree>[
new TestTree(name: 'node B1'),
new TestTree(name: 'node B2'),
new TestTree(name: 'node B3'),
],
),
new TestTree(name: 'node C'),
],
);
final String dump = tree.toStringDeep().replaceAll(new RegExp(r'#\d+'), '');
expect(dump, equals('''TestTree
child node A: TestTree
child node B: TestTree
child node B1: TestTree
child node B2: TestTree
child node B3: TestTree
child node C: TestTree
'''));
});
}

View file

@ -39,16 +39,16 @@ void main() {
});
testWidgets('Focus handling', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey();
final FocusNode focusNode = new FocusNode();
await tester.pumpWidget(new MaterialApp(
home: new Material(
child: new Center(
child: new Input(key: inputKey, autofocus: true)
child: new Input(focusNode: focusNode, autofocus: true)
)
)
));
expect(Focus.at(inputKey.currentContext), isTrue);
expect(focusNode.hasFocus, isTrue);
});
testWidgets('Can show grid without losing sync', (WidgetTester tester) async {

View file

@ -137,7 +137,7 @@ void main() {
return new SampleForm(
callback: () => new Future<bool>.value(willPopValue),
);
}
},
));
},
),

View file

@ -5,75 +5,103 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class TestFocusable extends StatelessWidget {
class TestFocusable extends StatefulWidget {
TestFocusable({
GlobalKey key,
Key key,
this.no,
this.yes,
this.autofocus: true
this.autofocus: true,
}) : super(key: key);
final String no;
final String yes;
final bool autofocus;
@override
TestFocusableState createState() => new TestFocusableState();
}
class TestFocusableState extends State<TestFocusable> {
final FocusNode focusNode = new FocusNode();
bool _didAutofocus = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutofocus && config.autofocus) {
_didAutofocus = true;
FocusScope.of(context).autofocus(focusNode);
}
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool focused = Focus.at(context, autofocus: autofocus);
return new GestureDetector(
onTap: () { Focus.moveTo(key); },
child: new Text(focused ? yes : no)
onTap: () { FocusScope.of(context).requestFocus(focusNode); },
child: new AnimatedBuilder(
animation: focusNode,
builder: (BuildContext context, Widget child) {
// print('focusNode.hasFocus = ${focusNode.hasFocus} ${focusNode.hashCode} ${focusNode.hasFocus ? config.yes : config.no}');
return new Text(focusNode.hasFocus ? config.yes : config.no);
},
),
);
}
}
void main() {
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
final GlobalKey keyFocus = new GlobalKey();
final GlobalKey keyA = new GlobalKey();
final GlobalKey keyB = new GlobalKey();
await tester.pumpWidget(
new Focus(
key: keyFocus,
child: new Column(
children: <Widget>[
new TestFocusable(
key: keyA,
no: 'a',
yes: 'A FOCUSED'
),
new TestFocusable(
key: keyB,
no: 'b',
yes: 'B FOCUSED'
),
]
)
)
new Column(
children: <Widget>[
new TestFocusable(
no: 'a',
yes: 'A FOCUSED',
),
new TestFocusable(
no: 'b',
yes: 'B FOCUSED',
),
],
),
);
// Autofocus is delayed one frame.
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('A FOCUSED'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('A FOCUSED'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
expect(find.text('b'), findsOneWidget);
expect(find.text('B FOCUSED'), findsNothing);
await tester.tap(find.text('b'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
expect(find.text('b'), findsNothing);
expect(find.text('B FOCUSED'), findsOneWidget);
await tester.tap(find.text('a'));
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
@ -82,30 +110,27 @@ void main() {
});
testWidgets('Can blur', (WidgetTester tester) async {
final GlobalKey keyFocus = new GlobalKey();
final GlobalKey keyA = new GlobalKey();
await tester.pumpWidget(
new Focus(
key: keyFocus,
child: new TestFocusable(
key: keyA,
no: 'a',
yes: 'A FOCUSED',
autofocus: false
)
)
new TestFocusable(
no: 'a',
yes: 'A FOCUSED',
autofocus: false,
),
);
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
Focus.moveTo(keyA);
final TestFocusableState state = tester.state(find.byType(TestFocusable));
FocusScope.of(state.context).requestFocus(state.focusNode);
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
Focus.clear(keyA.currentContext);
state.focusNode.unfocus();
await tester.idle();
await tester.pump();
expect(find.text('a'), findsOneWidget);
@ -113,76 +138,77 @@ void main() {
});
testWidgets('Can move focus to scope', (WidgetTester tester) async {
final GlobalKey keyParentFocus = new GlobalKey();
final GlobalKey keyChildFocus = new GlobalKey();
final GlobalKey keyA = new GlobalKey();
final FocusScopeNode parentFocusScope = new FocusScopeNode();
final FocusScopeNode childFocusScope = new FocusScopeNode();
await tester.pumpWidget(
new Focus(
key: keyParentFocus,
new FocusScope(
node: parentFocusScope,
autofocus: true,
child: new Row(
children: <Widget>[
new TestFocusable(
key: keyA,
no: 'a',
yes: 'A FOCUSED',
autofocus: false
)
]
)
)
autofocus: false,
),
],
),
),
);
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
Focus.moveTo(keyA);
final TestFocusableState state = tester.state(find.byType(TestFocusable));
FocusScope.of(state.context).requestFocus(state.focusNode);
await tester.idle();
await tester.pump();
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
Focus.moveScopeTo(keyChildFocus, context: keyA.currentContext);
parentFocusScope.setFirstFocus(childFocusScope);
await tester.idle();
await tester.pumpWidget(
new Focus(
key: keyParentFocus,
new FocusScope(
node: parentFocusScope,
child: new Row(
children: <Widget>[
new TestFocusable(
key: keyA,
no: 'a',
yes: 'A FOCUSED',
autofocus: false
autofocus: false,
),
new Focus(
key: keyChildFocus,
new FocusScope(
node: childFocusScope,
child: new Container(
width: 50.0,
height: 50.0
)
)
]
)
)
height: 50.0,
),
),
],
),
),
);
expect(find.text('a'), findsOneWidget);
expect(find.text('A FOCUSED'), findsNothing);
await tester.pumpWidget(
new Focus(
key: keyParentFocus,
new FocusScope(
node: parentFocusScope,
child: new Row(
children: <Widget>[
new TestFocusable(
key: keyA,
no: 'a',
yes: 'A FOCUSED',
autofocus: false
)
]
)
)
autofocus: false,
),
],
),
),
);
// Focus has received the removal notification but we haven't rebuilt yet.
@ -193,5 +219,7 @@ void main() {
expect(find.text('a'), findsNothing);
expect(find.text('A FOCUSED'), findsOneWidget);
parentFocusScope.detach();
});
}

View file

@ -120,7 +120,6 @@ void main() {
testWidgets('Multiple Inputs communicate', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
final GlobalKey<FormFieldState<InputValue>> fieldKey = new GlobalKey<FormFieldState<InputValue>>();
final GlobalKey focusKey = new GlobalKey();
// Input 2's validator depends on a input 1's value.
String errorText(InputValue input) => fieldKey.currentState.value?.text.toString() + '/error';
@ -130,21 +129,18 @@ void main() {
child: new Form(
key: formKey,
autovalidate: true,
child: new Focus(
key: focusKey,
child: new ListView(
children: <Widget>[
new TextField(
key: fieldKey
),
new TextField(
validator: errorText,
),
]
)
child: new ListView(
children: <Widget>[
new TextField(
key: fieldKey,
),
new TextField(
validator: errorText,
),
],
),
)
)
),
),
);
}

View file

@ -795,26 +795,22 @@ void main() {
testWidgets('Input label text animates', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey();
final GlobalKey focusKey = new GlobalKey();
Widget innerBuilder() {
return new Center(
child: new Material(
child: new Focus(
key: focusKey,
child: new Column(
children: <Widget>[
new Input(
labelText: 'First'
),
new Input(
key: inputKey,
labelText: 'Second'
),
]
)
)
)
child: new Column(
children: <Widget>[
new Input(
labelText: 'First',
),
new Input(
key: inputKey,
labelText: 'Second',
),
],
),
),
);
}
Widget builder() => overlay(innerBuilder());
@ -825,6 +821,7 @@ void main() {
// Focus the Input. The label should start animating upwards.
await tester.tap(find.byKey(inputKey));
await tester.idle();
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));

View file

@ -177,18 +177,6 @@ void main() {
expect('$exception', startsWith('Navigator operation requested with a context'));
});
testWidgets('Missing settings in onGenerateRoute throws exception', (WidgetTester tester) async {
await tester.pumpWidget(new Navigator(
onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>(
builder: (BuildContext context) => new Container()
);
}
));
final Object exception = tester.takeException();
expect(exception is FlutterError, isTrue);
});
testWidgets('Gestures between push and build are ignored', (WidgetTester tester) async {
final List<String> log = <String>[];
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{

View file

@ -15,16 +15,20 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
void main() {
testWidgets('Can dispose without keyboard', (WidgetTester tester) async {
await tester.pumpWidget(new RawKeyboardListener(child: new Container()));
await tester.pumpWidget(new RawKeyboardListener(child: new Container()));
final FocusNode focusNode = new FocusNode();
await tester.pumpWidget(new RawKeyboardListener(focusNode: focusNode, onKey: null, child: new Container()));
await tester.pumpWidget(new RawKeyboardListener(focusNode: focusNode, onKey: null, child: new Container()));
await tester.pumpWidget(new Container());
});
testWidgets('Fuchsia key event', (WidgetTester tester) async {
final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = new FocusNode();
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.pumpWidget(new RawKeyboardListener(
focused: true,
focusNode: focusNode,
onKey: events.add,
child: new Container(),
));
@ -46,14 +50,20 @@ void main() {
expect(typedData.hidUsage, 0x04);
expect(typedData.codePoint, 0x64);
expect(typedData.modifiers, 0x08);
await tester.pumpWidget(new Container());
focusNode.dispose();
});
testWidgets('Defunct listeners do not receive events',
(WidgetTester tester) async {
final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = new FocusNode();
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.pumpWidget(new RawKeyboardListener(
focused: true,
focusNode: focusNode,
onKey: events.add,
child: new Container(),
));
@ -84,5 +94,8 @@ void main() {
await tester.idle();
expect(events.length, 0);
await tester.pumpWidget(new Container());
focusNode.dispose();
});
}