Skip to content

Commit

Permalink
Factor out RawView, make View listen to engine generated view foc…
Browse files Browse the repository at this point in the history
…us events (flutter#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.
  • Loading branch information
gspencergoog authored May 20, 2024
1 parent 72f06d2 commit 333c076
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 175 deletions.
14 changes: 14 additions & 0 deletions packages/flutter/lib/src/services/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
SystemChannels.accessibility.setMessageHandler((dynamic message) => _handleAccessibilityMessage(message as Object));
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage);
platformDispatcher.onViewFocusChange = handleViewFocusChanged;
TextInput.ensureInitialized();
readInitialLifecycleStateFromNativeWindow();
initializationComplete();
Expand Down Expand Up @@ -355,6 +356,19 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
return;
}

/// Called whenever the [PlatformDispatcher] receives a notification that the
/// focus state on a view has changed.
///
/// The [event] contains the view ID for the view that changed its focus
/// state.
///
/// See also:
///
/// * [PlatformDispatcher.onViewFocusChange], which calls this method.
@protected
@mustCallSuper
void handleViewFocusChanged(ui.ViewFocusEvent event) {}

Future<dynamic> _handlePlatformMessage(MethodCall methodCall) async {
final String method = methodCall.method;
switch (method) {
Expand Down
23 changes: 22 additions & 1 deletion packages/flutter/lib/src/widgets/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import 'dart:async';
import 'dart:developer' as developer;
import 'dart:ui' show AccessibilityFeatures, AppExitResponse, AppLifecycleState, FrameTiming, Locale, PlatformDispatcher, TimingsCallback;
import 'dart:ui' show AccessibilityFeatures, AppExitResponse, AppLifecycleState,
FrameTiming, Locale, PlatformDispatcher, TimingsCallback, ViewFocusEvent;

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

/// Called whenever the [PlatformDispatcher] receives a notification that the
/// focus state on a view has changed.
///
/// The [event] contains the view ID for the view that changed its focus
/// state.
///
/// The view ID of the [FlutterView] in which a particular [BuildContext]
/// resides can be retrieved with `View.of(context).viewId`, so that it may be
/// compared with the view ID in the `event` to see if the event pertains to
/// the given context.
void didChangeViewFocus(ViewFocusEvent event) { }

/// Called when a request is received from the system to exit the application.
///
/// If any observer responds with [AppExitResponse.cancel], it will cancel the
Expand Down Expand Up @@ -951,6 +964,14 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
}
}

@override
void handleViewFocusChanged(ViewFocusEvent event) {
super.handleViewFocusChanged(event);
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeViewFocus(event);
}
}

@override
void handleMemoryPressure() {
super.handleMemoryPressure();
Expand Down
25 changes: 18 additions & 7 deletions packages/flutter/lib/src/widgets/focus_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ class FocusAttachment {
_node._manager?._markDetached(_node);
_node._parent?._removeChild(_node);
_node._attachment = null;
assert(!_node.hasPrimaryFocus);
assert(_node._manager?._markedForFocus != _node);
assert(!_node.hasPrimaryFocus, 'Node ${_node.debugLabel ?? _node} still has primary focus while being detached.');
assert(_node._manager?._markedForFocus != _node, 'Node ${_node.debugLabel ?? _node} still marked for focus while being detached.');
}
assert(!isAttached);
}
Expand Down Expand Up @@ -1296,8 +1296,10 @@ class FocusScopeNode extends FocusNode {
///
/// Returns null if there is no currently focused child.
FocusNode? get focusedChild {
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this, 'Focused child does not have the same idea of its enclosing scope as the scope does.');
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this,
'$debugLabel: Focused child does not have the same idea of its enclosing scope '
'(${_focusedChildren.lastOrNull?.enclosingScope}) as the scope does.');
return _focusedChildren.lastOrNull;
}

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

/// Requests that the scope itself receive focus, without trying to find
/// a descendant that should receive focus.
///
/// This is used only if you want to park the focus on a scope itself.
void requestScopeFocus() {
_doRequestFocus(findFirstFocus: false);
}

@override
void _doRequestFocus({required bool findFirstFocus}) {

// It is possible that a previously focused child is no longer focusable.
while (this.focusedChild != null && !this.focusedChild!.canRequestFocus) {
// It is possible that a previously focused child is no longer focusable, so
// clean out the list if so.
while (_focusedChildren.isNotEmpty &&
(!_focusedChildren.last.canRequestFocus || _focusedChildren.last.enclosingScope == null)) {
_focusedChildren.removeLast();
}

Expand Down
21 changes: 15 additions & 6 deletions packages/flutter/lib/src/widgets/focus_scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,9 @@ class FocusScope extends Focus {
super.onKeyEvent,
super.onKey,
super.debugLabel,
super.includeSemantics,
super.descendantsAreFocusable,
super.descendantsAreTraversable,
}) : super(
focusNode: node,
);
Expand All @@ -770,6 +773,7 @@ class FocusScope extends Focus {
required FocusScopeNode focusScopeNode,
FocusNode? parentNode,
bool autofocus,
bool includeSemantics,
ValueChanged<bool>? onFocusChange,
}) = _FocusScopeWithExternalFocusNode;

Expand Down Expand Up @@ -798,6 +802,7 @@ class _FocusScopeWithExternalFocusNode extends FocusScope {
required FocusScopeNode focusScopeNode,
super.parentNode,
super.autofocus,
super.includeSemantics,
super.onFocusChange,
}) : super(
node: focusScopeNode,
Expand Down Expand Up @@ -834,13 +839,17 @@ class _FocusScopeState extends _FocusState {
@override
Widget build(BuildContext context) {
_focusAttachment!.reparent(parent: widget.parentNode);
return Semantics(
explicitChildNodes: true,
child: _FocusInheritedScope(
node: focusNode,
child: widget.child,
),
Widget result = _FocusInheritedScope(
node: focusNode,
child: widget.child,
);
if (widget.includeSemantics) {
result = Semantics(
explicitChildNodes: true,
child: result,
);
}
return result;
}
}

Expand Down
Loading

0 comments on commit 333c076

Please sign in to comment.