mirror of
https://github.com/flutter/flutter
synced 2024-08-25 02:46:31 +00:00
Cache FocusNode.enclosingScope
, clean up descendantsAreFocusable
(#144207)
`FocusNode.canRequestFocus` was doing a double traversal if no ancestor disallows focus. The last for loop only has to reach as far as the enclosing scope. Also this caches the `FocusNode.enclosingScope` since the getter access happens much more frequently than node reparenting.
This commit is contained in:
parent
a23c81333f
commit
c353cb0123
|
@ -517,21 +517,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||
/// focus traversal policy for a widget subtree.
|
||||
/// * [FocusTraversalPolicy], a class that can be extended to describe a
|
||||
/// traversal policy.
|
||||
bool get canRequestFocus {
|
||||
if (!_canRequestFocus) {
|
||||
return false;
|
||||
}
|
||||
final FocusScopeNode? scope = enclosingScope;
|
||||
if (scope != null && !scope.canRequestFocus) {
|
||||
return false;
|
||||
}
|
||||
for (final FocusNode ancestor in ancestors) {
|
||||
if (!ancestor.descendantsAreFocusable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool get canRequestFocus => _canRequestFocus && ancestors.every(_allowDescendantsToBeFocused);
|
||||
static bool _allowDescendantsToBeFocused(FocusNode ancestor) => ancestor.descendantsAreFocusable;
|
||||
|
||||
bool _canRequestFocus;
|
||||
@mustCallSuper
|
||||
|
@ -791,6 +778,22 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||
/// Use [enclosingScope] to look for scopes above this node.
|
||||
FocusScopeNode? get nearestScope => enclosingScope;
|
||||
|
||||
FocusScopeNode? _enclosingScope;
|
||||
void _clearEnclosingScopeCache() {
|
||||
final FocusScopeNode? cachedScope = _enclosingScope;
|
||||
if (cachedScope == null) {
|
||||
return;
|
||||
}
|
||||
_enclosingScope = null;
|
||||
if (children.isNotEmpty) {
|
||||
for (final FocusNode child in children) {
|
||||
if (identical(cachedScope, child._enclosingScope)) {
|
||||
child._clearEnclosingScopeCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the nearest enclosing scope node above this node, or null if the
|
||||
/// node has not yet be added to the focus tree.
|
||||
///
|
||||
|
@ -799,12 +802,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||
///
|
||||
/// Use [nearestScope] to start at this node instead of above it.
|
||||
FocusScopeNode? get enclosingScope {
|
||||
for (final FocusNode node in ancestors) {
|
||||
if (node is FocusScopeNode) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
final FocusScopeNode? enclosingScope = _enclosingScope ??= parent?.nearestScope;
|
||||
assert(enclosingScope == parent?.nearestScope, '$this has invalid scope cache: $_enclosingScope != ${parent?.nearestScope}');
|
||||
return enclosingScope;
|
||||
}
|
||||
|
||||
/// Returns the size of the attached widget's [RenderObject], in logical
|
||||
|
@ -990,6 +990,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||
}
|
||||
|
||||
node._parent = null;
|
||||
node._clearEnclosingScopeCache();
|
||||
_children.remove(node);
|
||||
for (final FocusNode ancestor in ancestors) {
|
||||
ancestor._descendants = null;
|
||||
|
@ -1268,13 +1269,14 @@ class FocusScopeNode extends FocusNode {
|
|||
super.skipTraversal,
|
||||
super.canRequestFocus,
|
||||
this.traversalEdgeBehavior = TraversalEdgeBehavior.closedLoop,
|
||||
}) : super(
|
||||
descendantsAreFocusable: true,
|
||||
);
|
||||
}) : super(descendantsAreFocusable: true);
|
||||
|
||||
@override
|
||||
FocusScopeNode get nearestScope => this;
|
||||
|
||||
@override
|
||||
bool get descendantsAreFocusable => _canRequestFocus && super.descendantsAreFocusable;
|
||||
|
||||
/// Controls the transfer of focus beyond the first and the last items of a
|
||||
/// [FocusScopeNode].
|
||||
///
|
||||
|
|
|
@ -529,16 +529,16 @@ void main() {
|
|||
// child1
|
||||
// |
|
||||
// child2
|
||||
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope2');
|
||||
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
|
||||
addTearDown(scope1.dispose);
|
||||
final FocusAttachment scope2Attachment = scope1.attach(context);
|
||||
scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope);
|
||||
|
||||
final FocusNode child1 = FocusNode(debugLabel: 'child2');
|
||||
final FocusNode child1 = FocusNode(debugLabel: 'child1');
|
||||
addTearDown(child1.dispose);
|
||||
final FocusAttachment child2Attachment = child1.attach(context);
|
||||
|
||||
final FocusNode child2 = FocusNode(debugLabel: 'child3');
|
||||
final FocusNode child2 = FocusNode(debugLabel: 'child2');
|
||||
addTearDown(child2.dispose);
|
||||
final FocusAttachment child3Attachment = child2.attach(context);
|
||||
|
||||
|
@ -697,6 +697,26 @@ void main() {
|
|||
expect(parent2.children.first, equals(child1));
|
||||
});
|
||||
|
||||
test('FocusScopeNode.canRequestFocus affects descendantsAreFocusable', () {
|
||||
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
|
||||
|
||||
scope.descendantsAreFocusable = false;
|
||||
expect(scope.descendantsAreFocusable, isFalse);
|
||||
expect(scope.canRequestFocus, isTrue);
|
||||
|
||||
scope.descendantsAreFocusable = true;
|
||||
expect(scope.descendantsAreFocusable, isTrue);
|
||||
expect(scope.canRequestFocus, isTrue);
|
||||
|
||||
scope.canRequestFocus = false;
|
||||
expect(scope.descendantsAreFocusable, isFalse);
|
||||
expect(scope.canRequestFocus, isFalse);
|
||||
|
||||
scope.canRequestFocus = true;
|
||||
expect(scope.descendantsAreFocusable, isTrue);
|
||||
expect(scope.canRequestFocus, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('canRequestFocus affects children.', (WidgetTester tester) async {
|
||||
final BuildContext context = await setupWidget(tester);
|
||||
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
|
||||
|
|
Loading…
Reference in a new issue