Skip to content

Commit 333c076

Browse files
authored
Factor out RawView, make View listen to engine generated view focus events (#143259)
## Description This factors out a separate `RawView` that doesn't add a `MediaQuery` or a `FocusScope`. This PR also adds a new method `WidgetsBindingObserver.didChangeViewFocus` which allows the observer to know when the `FlutterView` that has focus has changed. It also makes the `View` widget a stateful widget that contains a `FocusScope` and ` FocusTraversalGroup` so that it can respond to changes in the focus of the view. I've also added a new function to `FocusScopeNode` that will allow the scope node itself to be focused, without looking for descendants that could take the focus. This lets the focus be "parked" at the `FocusManager.instance.rootScope` so that nothing else appears to have focus. ## Tests - Added tests for the new functionality.
1 parent 72f06d2 commit 333c076

17 files changed

+588
-175
lines changed

packages/flutter/lib/src/services/binding.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
4747
SystemChannels.accessibility.setMessageHandler((dynamic message) => _handleAccessibilityMessage(message as Object));
4848
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
4949
SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage);
50+
platformDispatcher.onViewFocusChange = handleViewFocusChanged;
5051
TextInput.ensureInitialized();
5152
readInitialLifecycleStateFromNativeWindow();
5253
initializationComplete();
@@ -355,6 +356,19 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
355356
return;
356357
}
357358

359+
/// Called whenever the [PlatformDispatcher] receives a notification that the
360+
/// focus state on a view has changed.
361+
///
362+
/// The [event] contains the view ID for the view that changed its focus
363+
/// state.
364+
///
365+
/// See also:
366+
///
367+
/// * [PlatformDispatcher.onViewFocusChange], which calls this method.
368+
@protected
369+
@mustCallSuper
370+
void handleViewFocusChanged(ui.ViewFocusEvent event) {}
371+
358372
Future<dynamic> _handlePlatformMessage(MethodCall methodCall) async {
359373
final String method = methodCall.method;
360374
switch (method) {

packages/flutter/lib/src/widgets/binding.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import 'dart:async';
66
import 'dart:developer' as developer;
7-
import 'dart:ui' show AccessibilityFeatures, AppExitResponse, AppLifecycleState, FrameTiming, Locale, PlatformDispatcher, TimingsCallback;
7+
import 'dart:ui' show AccessibilityFeatures, AppExitResponse, AppLifecycleState,
8+
FrameTiming, Locale, PlatformDispatcher, TimingsCallback, ViewFocusEvent;
89

910
import 'package:flutter/foundation.dart';
1011
import 'package:flutter/gestures.dart';
@@ -321,6 +322,18 @@ abstract mixin class WidgetsBindingObserver {
321322
/// application lifecycle changes.
322323
void didChangeAppLifecycleState(AppLifecycleState state) { }
323324

325+
/// Called whenever the [PlatformDispatcher] receives a notification that the
326+
/// focus state on a view has changed.
327+
///
328+
/// The [event] contains the view ID for the view that changed its focus
329+
/// state.
330+
///
331+
/// The view ID of the [FlutterView] in which a particular [BuildContext]
332+
/// resides can be retrieved with `View.of(context).viewId`, so that it may be
333+
/// compared with the view ID in the `event` to see if the event pertains to
334+
/// the given context.
335+
void didChangeViewFocus(ViewFocusEvent event) { }
336+
324337
/// Called when a request is received from the system to exit the application.
325338
///
326339
/// If any observer responds with [AppExitResponse.cancel], it will cancel the
@@ -951,6 +964,14 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
951964
}
952965
}
953966

967+
@override
968+
void handleViewFocusChanged(ViewFocusEvent event) {
969+
super.handleViewFocusChanged(event);
970+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
971+
observer.didChangeViewFocus(event);
972+
}
973+
}
974+
954975
@override
955976
void handleMemoryPressure() {
956977
super.handleMemoryPressure();

packages/flutter/lib/src/widgets/focus_manager.dart

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ class FocusAttachment {
219219
_node._manager?._markDetached(_node);
220220
_node._parent?._removeChild(_node);
221221
_node._attachment = null;
222-
assert(!_node.hasPrimaryFocus);
223-
assert(_node._manager?._markedForFocus != _node);
222+
assert(!_node.hasPrimaryFocus, 'Node ${_node.debugLabel ?? _node} still has primary focus while being detached.');
223+
assert(_node._manager?._markedForFocus != _node, 'Node ${_node.debugLabel ?? _node} still marked for focus while being detached.');
224224
}
225225
assert(!isAttached);
226226
}
@@ -1296,8 +1296,10 @@ class FocusScopeNode extends FocusNode {
12961296
///
12971297
/// Returns null if there is no currently focused child.
12981298
FocusNode? get focusedChild {
1299-
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this, 'Focused child does not have the same idea of its enclosing scope as the scope does.');
1300-
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
1299+
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this,
1300+
'$debugLabel: Focused child does not have the same idea of its enclosing scope '
1301+
'(${_focusedChildren.lastOrNull?.enclosingScope}) as the scope does.');
1302+
return _focusedChildren.lastOrNull;
13011303
}
13021304

13031305
// A stack of the children that have been set as the focusedChild, most recent
@@ -1377,11 +1379,20 @@ class FocusScopeNode extends FocusNode {
13771379
_manager?._markNeedsUpdate();
13781380
}
13791381

1382+
/// Requests that the scope itself receive focus, without trying to find
1383+
/// a descendant that should receive focus.
1384+
///
1385+
/// This is used only if you want to park the focus on a scope itself.
1386+
void requestScopeFocus() {
1387+
_doRequestFocus(findFirstFocus: false);
1388+
}
1389+
13801390
@override
13811391
void _doRequestFocus({required bool findFirstFocus}) {
1382-
1383-
// It is possible that a previously focused child is no longer focusable.
1384-
while (this.focusedChild != null && !this.focusedChild!.canRequestFocus) {
1392+
// It is possible that a previously focused child is no longer focusable, so
1393+
// clean out the list if so.
1394+
while (_focusedChildren.isNotEmpty &&
1395+
(!_focusedChildren.last.canRequestFocus || _focusedChildren.last.enclosingScope == null)) {
13851396
_focusedChildren.removeLast();
13861397
}
13871398

packages/flutter/lib/src/widgets/focus_scope.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,9 @@ class FocusScope extends Focus {
757757
super.onKeyEvent,
758758
super.onKey,
759759
super.debugLabel,
760+
super.includeSemantics,
761+
super.descendantsAreFocusable,
762+
super.descendantsAreTraversable,
760763
}) : super(
761764
focusNode: node,
762765
);
@@ -770,6 +773,7 @@ class FocusScope extends Focus {
770773
required FocusScopeNode focusScopeNode,
771774
FocusNode? parentNode,
772775
bool autofocus,
776+
bool includeSemantics,
773777
ValueChanged<bool>? onFocusChange,
774778
}) = _FocusScopeWithExternalFocusNode;
775779

@@ -798,6 +802,7 @@ class _FocusScopeWithExternalFocusNode extends FocusScope {
798802
required FocusScopeNode focusScopeNode,
799803
super.parentNode,
800804
super.autofocus,
805+
super.includeSemantics,
801806
super.onFocusChange,
802807
}) : super(
803808
node: focusScopeNode,
@@ -834,13 +839,17 @@ class _FocusScopeState extends _FocusState {
834839
@override
835840
Widget build(BuildContext context) {
836841
_focusAttachment!.reparent(parent: widget.parentNode);
837-
return Semantics(
838-
explicitChildNodes: true,
839-
child: _FocusInheritedScope(
840-
node: focusNode,
841-
child: widget.child,
842-
),
842+
Widget result = _FocusInheritedScope(
843+
node: focusNode,
844+
child: widget.child,
843845
);
846+
if (widget.includeSemantics) {
847+
result = Semantics(
848+
explicitChildNodes: true,
849+
child: result,
850+
);
851+
}
852+
return result;
844853
}
845854
}
846855

0 commit comments

Comments
 (0)