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.
This commit is contained in:
parent
72f06d2bf3
commit
333c076e53
@ -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();
|
||||
@ -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) {
|
||||
|
@ -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';
|
||||
@ -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
|
||||
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
@ -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
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
@ -757,6 +757,9 @@ class FocusScope extends Focus {
|
||||
super.onKeyEvent,
|
||||
super.onKey,
|
||||
super.debugLabel,
|
||||
super.includeSemantics,
|
||||
super.descendantsAreFocusable,
|
||||
super.descendantsAreTraversable,
|
||||
}) : super(
|
||||
focusNode: node,
|
||||
);
|
||||
@ -770,6 +773,7 @@ class FocusScope extends Focus {
|
||||
required FocusScopeNode focusScopeNode,
|
||||
FocusNode? parentNode,
|
||||
bool autofocus,
|
||||
bool includeSemantics,
|
||||
ValueChanged<bool>? onFocusChange,
|
||||
}) = _FocusScopeWithExternalFocusNode;
|
||||
|
||||
@ -798,6 +802,7 @@ class _FocusScopeWithExternalFocusNode extends FocusScope {
|
||||
required FocusScopeNode focusScopeNode,
|
||||
super.parentNode,
|
||||
super.autofocus,
|
||||
super.includeSemantics,
|
||||
super.onFocusChange,
|
||||
}) : super(
|
||||
node: focusScopeNode,
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,15 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:ui' show FlutterView, SemanticsUpdate;
|
||||
import 'dart:ui' show FlutterView, SemanticsUpdate, ViewFocusDirection, ViewFocusEvent, ViewFocusState;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'binding.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'focus_scope.dart';
|
||||
import 'focus_traversal.dart';
|
||||
import 'framework.dart';
|
||||
import 'lookup_boundary.dart';
|
||||
import 'media_query.dart';
|
||||
@ -19,7 +23,7 @@ import 'media_query.dart';
|
||||
/// rendered into via [View.of] and [View.maybeOf].
|
||||
///
|
||||
/// The provided [child] is wrapped in a [MediaQuery] constructed from the given
|
||||
/// [view].
|
||||
/// [view], a [FocusScope], and a [RawView] widget.
|
||||
///
|
||||
/// For most use cases, using [MediaQuery.of], or its associated "...Of" methods
|
||||
/// are a more appropriate way of obtaining the information that a [FlutterView]
|
||||
@ -31,33 +35,38 @@ import 'media_query.dart';
|
||||
/// information to be aware of the context of the widget; e.g. the [Scaffold]
|
||||
/// widget adjusts the values for its various children.
|
||||
///
|
||||
/// Each [FlutterView] can be associated with at most one [View] widget in the
|
||||
/// widget tree. Two or more [View] widgets configured with the same
|
||||
/// [FlutterView] must never exist within the same widget tree at the same time.
|
||||
/// This limitation is enforced by a [GlobalObjectKey] that derives its identity
|
||||
/// from the [view] provided to this widget.
|
||||
/// Each [FlutterView] can be associated with at most one [View] or [RawView]
|
||||
/// widget in the widget tree. Two or more [View] or [RawView] widgets
|
||||
/// configured with the same [FlutterView] must never exist within the same
|
||||
/// widget tree at the same time. This limitation is enforced by a
|
||||
/// [GlobalObjectKey] that derives its identity from the [view] provided to this
|
||||
/// widget.
|
||||
///
|
||||
/// Since the [View] widget bootstraps its own independent render tree, neither
|
||||
/// it nor any of its descendants will insert a [RenderObject] into an existing
|
||||
/// render tree. Therefore, the [View] widget can only be used in those parts of
|
||||
/// the widget tree where it is not required to participate in the construction
|
||||
/// of the surrounding render tree. In other words, the widget may only be used
|
||||
/// in a non-rendering zone of the widget tree (see [WidgetsBinding] for a
|
||||
/// definition of rendering and non-rendering zones).
|
||||
/// Since the [View] widget bootstraps its own independent render tree using its
|
||||
/// embedded [RawView], neither it nor any of its descendants will insert a
|
||||
/// [RenderObject] into an existing render tree. Therefore, the [View] widget
|
||||
/// can only be used in those parts of the widget tree where it is not required
|
||||
/// to participate in the construction of the surrounding render tree. In other
|
||||
/// words, the widget may only be used in a non-rendering zone of the widget
|
||||
/// tree (see [WidgetsBinding] for a definition of rendering and non-rendering
|
||||
/// zones).
|
||||
///
|
||||
/// In practical terms, the widget is typically used at the root of the widget
|
||||
/// tree outside of any other [View] widget, as a child of a [ViewCollection]
|
||||
/// widget, or in the [ViewAnchor.view] slot of a [ViewAnchor] widget. It is not
|
||||
/// required to be a direct child, though, since other non-[RenderObjectWidget]s
|
||||
/// (e.g. [InheritedWidget]s, [Builder]s, or [StatefulWidget]s/[StatelessWidget]
|
||||
/// that only produce non-[RenderObjectWidget]s) are allowed to be present
|
||||
/// between those widgets and the [View] widget.
|
||||
/// tree outside of any other [View] or [RawView] widget, as a child of a
|
||||
/// [ViewCollection] widget, or in the [ViewAnchor.view] slot of a [ViewAnchor]
|
||||
/// widget. It is not required to be a direct child, though, since other
|
||||
/// non-[RenderObjectWidget]s (e.g. [InheritedWidget]s, [Builder]s, or
|
||||
/// [StatefulWidget]s/[StatelessWidget]s that only produce
|
||||
/// non-[RenderObjectWidget]s) are allowed to be present between those widgets
|
||||
/// and the [View] widget.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Element.debugExpectsRenderObjectForSlot], which defines whether a [View]
|
||||
/// widget is allowed in a given child slot.
|
||||
class View extends StatelessWidget {
|
||||
/// * [RawView], the workhorse that [View] uses to create the render tree, but
|
||||
/// without the [MediaQuery] and [FocusScope] that [View] adds.
|
||||
/// * [Element.debugExpectsRenderObjectForSlot], which defines whether a [View]
|
||||
/// widget is allowed in a given child slot.
|
||||
class View extends StatefulWidget {
|
||||
/// Create a [View] widget to bootstrap a render tree that is rendered into
|
||||
/// the provided [FlutterView].
|
||||
///
|
||||
@ -105,7 +114,7 @@ class View extends StatelessWidget {
|
||||
/// moved to render into a different [FlutterView] then before). The context
|
||||
/// will not be informed when the _properties_ on the [FlutterView] itself
|
||||
/// change their values. To access the property values of a [FlutterView] it
|
||||
/// is best practise to use [MediaQuery.maybeOf] instead, which will ensure
|
||||
/// is best practice to use [MediaQuery.maybeOf] instead, which will ensure
|
||||
/// that the `context` is informed when the view properties change.
|
||||
///
|
||||
/// See also:
|
||||
@ -170,9 +179,153 @@ class View extends StatelessWidget {
|
||||
?? RendererBinding.instance.rootPipelineOwner;
|
||||
}
|
||||
|
||||
@override
|
||||
State<View> createState() => _ViewState();
|
||||
}
|
||||
|
||||
class _ViewState extends State<View> with WidgetsBindingObserver {
|
||||
final FocusScopeNode _scopeNode = FocusScopeNode(
|
||||
debugLabel: kReleaseMode ? null : 'View Scope',
|
||||
);
|
||||
final FocusTraversalPolicy _policy = ReadingOrderTraversalPolicy();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_scopeNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeViewFocus(ViewFocusEvent event) {
|
||||
if (event.viewId != widget.view.viewId) {
|
||||
// The event is not pertinent to this view.
|
||||
return;
|
||||
}
|
||||
FocusNode nextFocus;
|
||||
switch (event.state) {
|
||||
case ViewFocusState.focused:
|
||||
switch (event.direction) {
|
||||
case ViewFocusDirection.forward:
|
||||
nextFocus = _policy.findFirstFocus(_scopeNode, ignoreCurrentFocus: true) ?? _scopeNode;
|
||||
case ViewFocusDirection.backward:
|
||||
nextFocus = _policy.findLastFocus(_scopeNode, ignoreCurrentFocus: true);
|
||||
case ViewFocusDirection.undefined:
|
||||
nextFocus = _scopeNode;
|
||||
}
|
||||
nextFocus.requestFocus();
|
||||
case ViewFocusState.unfocused:
|
||||
// Focusing on the root scope node will "park" the focus, so that no
|
||||
// descendant node will be given focus, and there's no widget that can
|
||||
// receive keyboard events.
|
||||
FocusManager.instance.rootScope.requestScopeFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _RawView(
|
||||
return RawView(
|
||||
view: widget.view,
|
||||
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: widget._deprecatedPipelineOwner,
|
||||
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: widget._deprecatedRenderView,
|
||||
child: MediaQuery.fromView(
|
||||
view: widget.view,
|
||||
child: FocusTraversalGroup(
|
||||
policy: _policy,
|
||||
child: FocusScope.withExternalFocusNode(
|
||||
includeSemantics: false,
|
||||
focusScopeNode: _scopeNode,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The lower level workhorse widget for [View] that bootstraps a render tree
|
||||
/// for a view.
|
||||
///
|
||||
/// Typically, the [View] widget is used instead of a [RawView] widget to create
|
||||
/// a view, since, in addition to creating a view, it also adds some useful
|
||||
/// widgets, such as a [MediaQuery] and [FocusScope]. The [RawView] widget is
|
||||
/// only used directly if it is not desirable to have these additional widgets
|
||||
/// around the resulting widget tree. The [View] widget uses the [RawView]
|
||||
/// widget internally to manage its [FlutterView].
|
||||
///
|
||||
/// This widget can be used at the root of the widget tree outside of any other
|
||||
/// [View] or [RawView] widget, as a child to a [ViewCollection], or in the
|
||||
/// [ViewAnchor.view] slot of a [ViewAnchor] widget. It is not required to be a
|
||||
/// direct child of those widgets; other non-[RenderObjectWidget]s may appear in
|
||||
/// between the two (such as an [InheritedWidget]).
|
||||
///
|
||||
/// Each [FlutterView] can be associated with at most one [View] or [RawView]
|
||||
/// widget in the widget tree. Two or more [View] or [RawView] widgets
|
||||
/// configured with the same [FlutterView] must never exist within the same
|
||||
/// widget tree at the same time. This limitation is enforced by a
|
||||
/// [GlobalObjectKey] that derives its identity from the [view] provided to this
|
||||
/// widget.
|
||||
///
|
||||
/// Since the [RawView] widget bootstraps its own independent render tree,
|
||||
/// neither it nor any of its descendants will insert a [RenderObject] into an
|
||||
/// existing render tree. Therefore, the [RawView] widget can only be used in
|
||||
/// those parts of the widget tree where it is not required to participate in
|
||||
/// the construction of the surrounding render tree. In other words, the widget
|
||||
/// may only be used in a non-rendering zone of the widget tree (see
|
||||
/// [WidgetsBinding] for a definition of rendering and non-rendering zones).
|
||||
///
|
||||
/// To find the [FlutterView] associated with a [BuildContext], use [View.of] or
|
||||
/// [View.maybeOf], even if the view was created using [RawView] instead of
|
||||
/// [View].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [View] for a higher level interface that also sets up a [MediaQuery] and
|
||||
/// [FocusScope] for the view's widget tree.
|
||||
class RawView extends StatelessWidget {
|
||||
/// Creates a [RawView] widget.
|
||||
RawView({
|
||||
super.key,
|
||||
required this.view,
|
||||
@Deprecated(
|
||||
'Do not use. '
|
||||
'This parameter only exists to implement the deprecated RendererBinding.pipelineOwner property until it is removed. '
|
||||
'This feature was deprecated after v3.10.0-12.0.pre.'
|
||||
)
|
||||
PipelineOwner? deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner,
|
||||
@Deprecated(
|
||||
'Do not use. '
|
||||
'This parameter only exists to implement the deprecated RendererBinding.renderView property until it is removed. '
|
||||
'This feature was deprecated after v3.10.0-12.0.pre.'
|
||||
)
|
||||
RenderView? deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView,
|
||||
required this.child,
|
||||
}) : _deprecatedPipelineOwner = deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner,
|
||||
_deprecatedRenderView = deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView,
|
||||
assert((deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner == null) == (deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null)),
|
||||
assert(deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null || deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView.flutterView == view);
|
||||
|
||||
/// The [FlutterView] into which [child] is drawn.
|
||||
final FlutterView view;
|
||||
|
||||
/// The widget below this widget in the tree, which will be drawn into the
|
||||
/// [view].
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
final PipelineOwner? _deprecatedPipelineOwner;
|
||||
final RenderView? _deprecatedRenderView;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _RawViewInternal(
|
||||
view: view,
|
||||
deprecatedPipelineOwner: _deprecatedPipelineOwner,
|
||||
deprecatedRenderView: _deprecatedRenderView,
|
||||
@ -181,30 +334,27 @@ class View extends StatelessWidget {
|
||||
view: view,
|
||||
child: _PipelineOwnerScope(
|
||||
pipelineOwner: owner,
|
||||
child: MediaQuery.fromView(
|
||||
view: view,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for the content [Widget] of a [_RawView].
|
||||
/// A builder for the content [Widget] of a [_RawViewInternal].
|
||||
///
|
||||
/// The widget returned by the builder defines the content that is drawn into
|
||||
/// the [FlutterView] configured on the [_RawView].
|
||||
/// the [FlutterView] configured on the [_RawViewInternal].
|
||||
///
|
||||
/// The builder is given the [PipelineOwner] that the [_RawView] uses to manage
|
||||
/// its render tree. Typical builder implementations make that pipeline owner
|
||||
/// available as an attachment point for potential child views.
|
||||
/// The builder is given the [PipelineOwner] that the [_RawViewInternal] uses to
|
||||
/// manage its render tree. Typical builder implementations make that pipeline
|
||||
/// owner available as an attachment point for potential child views.
|
||||
///
|
||||
/// Used by [_RawView.builder].
|
||||
/// Used by [_RawViewInternal.builder].
|
||||
typedef _RawViewContentBuilder = Widget Function(BuildContext context, PipelineOwner owner);
|
||||
|
||||
/// The workhorse behind the [View] widget that actually bootstraps a render
|
||||
/// The workhorse behind the [RawView] widget that actually bootstraps a render
|
||||
/// tree.
|
||||
///
|
||||
/// It instantiates the [RenderView] as the root of that render tree and adds it
|
||||
@ -213,13 +363,13 @@ typedef _RawViewContentBuilder = Widget Function(BuildContext context, PipelineO
|
||||
/// the surrounding parent [PipelineOwner] obtained with [View.pipelineOwnerOf].
|
||||
/// This ensures that the render tree bootstrapped by this widget participates
|
||||
/// properly in frame production and hit testing.
|
||||
class _RawView extends RenderObjectWidget {
|
||||
/// Create a [RawView] widget to bootstrap a render tree that is rendered into
|
||||
/// the provided [FlutterView].
|
||||
class _RawViewInternal extends RenderObjectWidget {
|
||||
/// Create a [_RawViewInternal] widget to bootstrap a render tree that is
|
||||
/// rendered into the provided [FlutterView].
|
||||
///
|
||||
/// The content rendered into that [view] is determined by the [Widget]
|
||||
/// returned by [builder].
|
||||
_RawView({
|
||||
_RawViewInternal({
|
||||
required this.view,
|
||||
required PipelineOwner? deprecatedPipelineOwner,
|
||||
required RenderView? deprecatedRenderView,
|
||||
@ -266,7 +416,7 @@ class _RawViewElement extends RenderTreeRootElement {
|
||||
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
|
||||
);
|
||||
|
||||
PipelineOwner get _effectivePipelineOwner => (widget as _RawView)._deprecatedPipelineOwner ?? _pipelineOwner;
|
||||
PipelineOwner get _effectivePipelineOwner => (widget as _RawViewInternal)._deprecatedPipelineOwner ?? _pipelineOwner;
|
||||
|
||||
void _handleSemanticsOwnerCreated() {
|
||||
(_effectivePipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics();
|
||||
@ -277,7 +427,7 @@ class _RawViewElement extends RenderTreeRootElement {
|
||||
}
|
||||
|
||||
void _handleSemanticsUpdate(SemanticsUpdate update) {
|
||||
(widget as _RawView).view.updateSemantics(update);
|
||||
(widget as _RawViewInternal).view.updateSemantics(update);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -287,7 +437,7 @@ class _RawViewElement extends RenderTreeRootElement {
|
||||
|
||||
void _updateChild() {
|
||||
try {
|
||||
final Widget child = (widget as _RawView).builder(this, _effectivePipelineOwner);
|
||||
final Widget child = (widget as _RawViewInternal).builder(this, _effectivePipelineOwner);
|
||||
_child = updateChild(_child, child, null);
|
||||
} catch (e, stack) {
|
||||
final FlutterErrorDetails details = FlutterErrorDetails(
|
||||
@ -373,7 +523,7 @@ class _RawViewElement extends RenderTreeRootElement {
|
||||
}
|
||||
|
||||
@override
|
||||
void update(_RawView newWidget) {
|
||||
void update(_RawViewInternal newWidget) {
|
||||
super.update(newWidget);
|
||||
_updateChild();
|
||||
}
|
||||
@ -413,7 +563,7 @@ class _RawViewElement extends RenderTreeRootElement {
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
if (_effectivePipelineOwner != (widget as _RawView)._deprecatedPipelineOwner) {
|
||||
if (_effectivePipelineOwner != (widget as _RawViewInternal)._deprecatedPipelineOwner) {
|
||||
_effectivePipelineOwner.dispose();
|
||||
}
|
||||
super.unmount();
|
||||
|
@ -73,7 +73,7 @@ void main() {
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
@ -91,7 +91,7 @@ void main() {
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
@ -205,7 +205,7 @@ void main() {
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
label: 'checkbox',
|
||||
textDirection: TextDirection.ltr,
|
||||
hasCheckedState: true,
|
||||
|
@ -41,6 +41,8 @@ $license2
|
||||
''';
|
||||
|
||||
class TestBinding extends BindingBase with SchedulerBinding, ServicesBinding {
|
||||
ViewFocusEvent? lastFocusEvent;
|
||||
|
||||
@override
|
||||
TestDefaultBinaryMessenger get defaultBinaryMessenger => super.defaultBinaryMessenger as TestDefaultBinaryMessenger;
|
||||
|
||||
@ -54,6 +56,12 @@ class TestBinding extends BindingBase with SchedulerBinding, ServicesBinding {
|
||||
outboundHandlers: <String, MessageHandler>{'flutter/keyboard': keyboardHandler},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleViewFocusChanged(ViewFocusEvent event) {
|
||||
super.handleViewFocusChanged(event);
|
||||
lastFocusEvent = event;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
@ -161,4 +169,13 @@ void main() {
|
||||
expect(physicalKeys.first, const PhysicalKeyboardKey(1));
|
||||
expect(logicalKeys.first, const LogicalKeyboardKey(1));
|
||||
});
|
||||
|
||||
test('Default handleViewFocusChanged propagates event', () async {
|
||||
const ViewFocusEvent event = ViewFocusEvent(
|
||||
viewId: 0,
|
||||
direction: ViewFocusDirection.forward,
|
||||
state: ViewFocusState.focused);
|
||||
PlatformDispatcher.instance.onViewFocusChange?.call(event);
|
||||
expect(binding.lastFocusEvent, equals(event));
|
||||
});
|
||||
}
|
||||
|
@ -27,6 +27,15 @@ class AppLifecycleStateObserver with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
class ViewFocusObserver with WidgetsBindingObserver {
|
||||
List<ViewFocusEvent> accumulatedEvents = <ViewFocusEvent>[];
|
||||
|
||||
@override
|
||||
void didChangeViewFocus(ViewFocusEvent state) {
|
||||
accumulatedEvents.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
class PushRouteObserver with WidgetsBindingObserver {
|
||||
late String pushedRoute;
|
||||
|
||||
@ -76,6 +85,12 @@ class RentrantObserver implements WidgetsBindingObserver {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeViewFocus(ViewFocusEvent event) {
|
||||
assert(active);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeLocales(List<Locale>? locales) {
|
||||
assert(active);
|
||||
@ -183,6 +198,11 @@ void main() {
|
||||
WidgetsBinding.instance.handlePopRoute();
|
||||
WidgetsBinding.instance.handlePushRoute('/');
|
||||
WidgetsBinding.instance.handleRequestAppExit();
|
||||
WidgetsBinding.instance.handleViewFocusChanged(
|
||||
const ViewFocusEvent(viewId: 0,
|
||||
state: ViewFocusState.focused,
|
||||
direction: ViewFocusDirection.forward),
|
||||
);
|
||||
await tester.idle();
|
||||
expect(observer.removeSelf(), greaterThan(1));
|
||||
expect(observer.removeSelf(), 0);
|
||||
@ -262,6 +282,22 @@ void main() {
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
});
|
||||
|
||||
testWidgets('handleViewFocusChanged callback', (WidgetTester tester) async {
|
||||
final ViewFocusObserver observer = ViewFocusObserver();
|
||||
WidgetsBinding.instance.addObserver(observer);
|
||||
|
||||
const ViewFocusEvent expectedEvent = ViewFocusEvent(
|
||||
viewId: 0,
|
||||
state: ViewFocusState.focused,
|
||||
direction: ViewFocusDirection.forward,
|
||||
);
|
||||
|
||||
PlatformDispatcher.instance.onViewFocusChange!.call(expectedEvent);
|
||||
expect(observer.accumulatedEvents, <ViewFocusEvent>[expectedEvent]);
|
||||
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
});
|
||||
|
||||
testWidgets('didPushRoute callback', (WidgetTester tester) async {
|
||||
final PushRouteObserver observer = PushRouteObserver();
|
||||
WidgetsBinding.instance.addObserver(observer);
|
||||
|
@ -153,10 +153,10 @@ void main() {
|
||||
box.toStringDeep(minLevel: DiagnosticLevel.debug),
|
||||
equalsIgnoringHashCodes(
|
||||
'RenderPadding#00000 relayoutBoundary=up1\n'
|
||||
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ [root]\n'
|
||||
' │ creator: Padding ← Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||
' │ _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||
' │ size: Size(63.0, 88.0)\n'
|
||||
@ -164,9 +164,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' │ ← _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
||||
' │ size: Size(53.0, 78.0)\n'
|
||||
@ -174,9 +174,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderDecoratedBox#00000\n'
|
||||
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
||||
' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
|
||||
' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ Align ← _FocusInheritedScope ← _FocusScopeWithExternalFocusNode\n'
|
||||
' │ ← _FocusInheritedScope ← Focus ← FocusTraversalGroup ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ size: Size(53.0, 78.0)\n'
|
||||
@ -188,10 +188,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: _RenderColoredBox#00000\n'
|
||||
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
||||
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||
' │ _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ ⋯\n'
|
||||
' │ Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← MediaQuery ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ size: Size(53.0, 78.0)\n'
|
||||
@ -199,9 +198,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderPadding#00000\n'
|
||||
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
||||
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
||||
' │ ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
|
||||
' │ Padding ← Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ size: Size(53.0, 78.0)\n'
|
||||
@ -209,8 +208,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderPositionedBox#00000\n'
|
||||
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← ⋯\n'
|
||||
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
||||
' │ size: Size(39.0, 64.0)\n'
|
||||
@ -220,8 +220,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
||||
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
||||
' │ size: Size(25.0, 33.0)\n'
|
||||
@ -230,7 +231,7 @@ void main() {
|
||||
' └─child: RenderDecoratedBox#00000\n'
|
||||
' creator: DecoratedBox ← SizedBox ← Align ← Padding ← ColoredBox ←\n'
|
||||
' DecoratedBox ← ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ← ⋯\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=25.0, h=33.0)\n'
|
||||
' size: Size(25.0, 33.0)\n'
|
||||
@ -238,7 +239,7 @@ void main() {
|
||||
' color: Color(0xffffff00)\n'
|
||||
' configuration: ImageConfiguration(bundle:\n'
|
||||
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
|
||||
' android)\n',
|
||||
' android)\n'
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -255,10 +256,10 @@ void main() {
|
||||
box.toStringDeep(minLevel: DiagnosticLevel.fine),
|
||||
equalsIgnoringHashCodes(
|
||||
'RenderPadding#00000 relayoutBoundary=up1\n'
|
||||
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ [root]\n'
|
||||
' │ creator: Padding ← Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||
' │ _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -269,9 +270,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' │ ← _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -281,9 +282,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderDecoratedBox#00000\n'
|
||||
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
||||
' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
|
||||
' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ Align ← _FocusInheritedScope ← _FocusScopeWithExternalFocusNode\n'
|
||||
' │ ← _FocusInheritedScope ← Focus ← FocusTraversalGroup ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -303,10 +304,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: _RenderColoredBox#00000\n'
|
||||
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
||||
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||
' │ _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ ⋯\n'
|
||||
' │ Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← MediaQuery ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -316,9 +316,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderPadding#00000\n'
|
||||
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
||||
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
||||
' │ ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
|
||||
' │ Padding ← Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -329,8 +329,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderPositionedBox#00000\n'
|
||||
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← ⋯\n'
|
||||
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -343,8 +344,9 @@ void main() {
|
||||
' │\n'
|
||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
||||
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -355,7 +357,7 @@ void main() {
|
||||
' └─child: RenderDecoratedBox#00000\n'
|
||||
' creator: DecoratedBox ← SizedBox ← Align ← Padding ← ColoredBox ←\n'
|
||||
' DecoratedBox ← ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ← ⋯\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=25.0, h=33.0)\n'
|
||||
' layer: null\n'
|
||||
@ -389,10 +391,10 @@ void main() {
|
||||
equalsIgnoringHashCodes(
|
||||
'RenderPadding#00000 relayoutBoundary=up1\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ [root]\n'
|
||||
' │ creator: Padding ← Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||
' │ _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -406,9 +408,9 @@ void main() {
|
||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' │ ← _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -421,9 +423,9 @@ void main() {
|
||||
' └─child: RenderDecoratedBox#00000\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
||||
' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
|
||||
' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ Align ← _FocusInheritedScope ← _FocusScopeWithExternalFocusNode\n'
|
||||
' │ ← _FocusInheritedScope ← Focus ← FocusTraversalGroup ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -446,10 +448,9 @@ void main() {
|
||||
' └─child: _RenderColoredBox#00000\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
||||
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||
' │ _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ ⋯\n'
|
||||
' │ Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← MediaQuery ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -462,9 +463,9 @@ void main() {
|
||||
' └─child: RenderPadding#00000\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
||||
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
||||
' │ ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
|
||||
' │ Padding ← Container ← Align ← _FocusInheritedScope ←\n'
|
||||
' │ _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' │ ← FocusTraversalGroup ← ⋯\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -478,8 +479,9 @@ void main() {
|
||||
' └─child: RenderPositionedBox#00000\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← ⋯\n'
|
||||
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -495,8 +497,9 @@ void main() {
|
||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
||||
' │ needsCompositing: false\n'
|
||||
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
||||
' │ layer: null\n'
|
||||
@ -510,7 +513,7 @@ void main() {
|
||||
' needsCompositing: false\n'
|
||||
' creator: DecoratedBox ← SizedBox ← Align ← Padding ← ColoredBox ←\n'
|
||||
' DecoratedBox ← ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||
' MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ← ⋯\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=25.0, h=33.0)\n'
|
||||
' layer: null\n'
|
||||
|
@ -373,14 +373,13 @@ void main() {
|
||||
' in its parent data.\n'
|
||||
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
|
||||
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n'
|
||||
' CustomMultiChildLayout ← Center ← MediaQuery ←\n'
|
||||
' _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' [root]\n'
|
||||
' CustomMultiChildLayout ← Center ← _FocusInheritedScope ←\n'
|
||||
' _FocusScopeWithExternalFocusNode ← _FocusInheritedScope ← Focus\n'
|
||||
' ← FocusTraversalGroup ← MediaQuery ← _MediaQueryFromView ← ⋯\n'
|
||||
' parentData: offset=Offset(0.0, 0.0); id=null\n'
|
||||
' constraints: MISSING\n'
|
||||
' size: MISSING\n'
|
||||
' additionalConstraints: BoxConstraints(w=100.0, 0.0<=h<=Infinity)\n',
|
||||
' additionalConstraints: BoxConstraints(w=100.0, 0.0<=h<=Infinity)\n'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -919,12 +919,50 @@ void main() {
|
||||
child4Attachment.reparent(parent: parent2);
|
||||
child4.requestFocus();
|
||||
await tester.pump();
|
||||
final FocusScopeNode rootScope = tester.binding.focusManager.rootScope;
|
||||
final List<FocusNode> preamble = <FocusNode>[
|
||||
rootScope.children.first.children.first, // The View Node,
|
||||
rootScope.children.first, // The FocusTraversal node above the view
|
||||
];
|
||||
expect(child4.ancestors, equals(<FocusNode>[parent2, scope2, tester.binding.focusManager.rootScope]));
|
||||
expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child2, parent1, scope1, child3, child4, parent2, scope2]));
|
||||
expect(
|
||||
rootScope.descendants,
|
||||
equals(<FocusNode>[
|
||||
...preamble,
|
||||
child1,
|
||||
child2,
|
||||
parent1,
|
||||
scope1,
|
||||
child3,
|
||||
child4,
|
||||
parent2,
|
||||
scope2,
|
||||
]));
|
||||
scope2Attachment.reparent(parent: child2);
|
||||
await tester.pump();
|
||||
expect(child4.ancestors, equals(<FocusNode>[parent2, scope2, child2, parent1, scope1, tester.binding.focusManager.rootScope]));
|
||||
expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child3, child4, parent2, scope2, child2, parent1, scope1]));
|
||||
expect(
|
||||
child4.ancestors,
|
||||
equals(<FocusNode>[
|
||||
parent2,
|
||||
scope2,
|
||||
child2,
|
||||
parent1,
|
||||
scope1,
|
||||
rootScope,
|
||||
]));
|
||||
expect(
|
||||
tester.binding.focusManager.rootScope.descendants,
|
||||
equals(<FocusNode>[
|
||||
...preamble,
|
||||
child1,
|
||||
child3,
|
||||
child4,
|
||||
parent2,
|
||||
scope2,
|
||||
child2,
|
||||
parent1,
|
||||
scope1,
|
||||
]));
|
||||
});
|
||||
|
||||
testWidgets('Can move focus between scopes and keep focus', (WidgetTester tester) async {
|
||||
@ -1459,6 +1497,36 @@ void main() {
|
||||
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
|
||||
});
|
||||
|
||||
testWidgets('Scopes can be focused without sending focus to descendants.', (WidgetTester tester) async {
|
||||
final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'Scope1',);
|
||||
final FocusNode childFocusNode = FocusNode(debugLabel: 'Child1',);
|
||||
await tester.pumpWidget(
|
||||
FocusScope.withExternalFocusNode(
|
||||
focusScopeNode: scopeNode,
|
||||
child: Focus(
|
||||
debugLabel: 'Parent1',
|
||||
child: FocusScope(
|
||||
debugLabel: 'Scope2',
|
||||
child: Focus.withExternalFocusNode(
|
||||
focusNode: childFocusNode,
|
||||
child: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
childFocusNode.requestFocus();
|
||||
await tester.pump();
|
||||
expect(scopeNode.hasFocus, isTrue);
|
||||
expect(childFocusNode.hasPrimaryFocus, isTrue);
|
||||
|
||||
scopeNode.requestScopeFocus();
|
||||
await tester.pump();
|
||||
expect(scopeNode.hasPrimaryFocus, isTrue);
|
||||
expect(childFocusNode.hasPrimaryFocus, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('implements debugFillProperties', (WidgetTester tester) async {
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope Label');
|
||||
@ -1517,16 +1585,25 @@ void main() {
|
||||
equalsIgnoringHashCodes(
|
||||
'FocusManager#00000\n'
|
||||
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
|
||||
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ [root]\n'
|
||||
' │ primaryFocusCreator: Container-[GlobalKey#00000] ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' │ ← _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawViewInternal-[_DeprecatedRawViewKey TestFlutterView#00000]\n'
|
||||
' │ ← RawView ← View ← [root]\n'
|
||||
' │\n'
|
||||
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusScopeNode#00000([IN FOCUS PATH])\n'
|
||||
' │\n'
|
||||
' ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
|
||||
' ├─Child 1: _FocusTraversalGroupNode#00000(FocusTraversalGroup)\n'
|
||||
' │ │ context: Focus\n'
|
||||
' │ │ NOT FOCUSABLE\n'
|
||||
' │ │\n'
|
||||
' │ └─Child 1: FocusScopeNode#00000(View Scope)\n'
|
||||
' │ context: _FocusScopeWithExternalFocusNode\n'
|
||||
' │\n'
|
||||
' ├─Child 2: FocusScopeNode#00000(Scope 1)\n'
|
||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ │\n'
|
||||
' │ └─Child 1: FocusNode#00000(Parent 1)\n'
|
||||
@ -1538,7 +1615,7 @@ void main() {
|
||||
' │ └─Child 2: FocusNode#00000\n'
|
||||
' │ context: Container-[GlobalKey#00000]\n'
|
||||
' │\n'
|
||||
' └─Child 2: FocusScopeNode#00000([IN FOCUS PATH])\n'
|
||||
' └─Child 3: FocusScopeNode#00000([IN FOCUS PATH])\n'
|
||||
' │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
|
||||
@ -2185,7 +2262,7 @@ void main() {
|
||||
debugPrint = oldDebugPrint;
|
||||
}
|
||||
final String messagesStr = messages.toString();
|
||||
expect(messagesStr, contains(RegExp(r' └─Child 1: FocusScopeNode#[a-f0-9]{5}\(parent1 \[PRIMARY FOCUS\]\)')));
|
||||
expect(messagesStr, contains(RegExp(r' └─Child \d+: FocusScopeNode#[a-f0-9]{5}\(parent1 \[PRIMARY FOCUS\]\)')));
|
||||
expect(messagesStr, contains('FOCUS: Notified 2 dirty nodes'));
|
||||
expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
|
||||
});
|
||||
|
@ -182,17 +182,27 @@ void main() {
|
||||
equalsIgnoringHashCodes(
|
||||
'FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node [IN FOCUS\n'
|
||||
' │ PATH])\n'
|
||||
' │ focusedChildren: FocusScopeNode#00000(View Scope [IN FOCUS PATH])\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusScopeNode#00000(Parent Scope Node [IN FOCUS PATH])\n'
|
||||
' │ context: FocusScope\n'
|
||||
' └─Child 1: _FocusTraversalGroupNode#00000(FocusTraversalGroup [IN FOCUS PATH])\n'
|
||||
' │ context: Focus\n'
|
||||
' │ NOT FOCUSABLE\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
||||
' context: Focus\n'
|
||||
' PRIMARY FOCUS\n',
|
||||
' └─Child 1: FocusScopeNode#00000(View Scope [IN FOCUS PATH])\n'
|
||||
' │ context: _FocusScopeWithExternalFocusNode\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node [IN FOCUS\n'
|
||||
' │ PATH])\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusScopeNode#00000(Parent Scope Node [IN FOCUS PATH])\n'
|
||||
' │ context: FocusScope\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
||||
' context: Focus\n'
|
||||
' PRIMARY FOCUS\n'
|
||||
),
|
||||
);
|
||||
|
||||
@ -730,9 +740,11 @@ void main() {
|
||||
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
||||
expect(find.text('b'), findsOneWidget);
|
||||
|
||||
expect(FocusManager.instance.rootScope.descendants.length, equals(7));
|
||||
await tester.pumpWidget(Container());
|
||||
|
||||
expect(FocusManager.instance.rootScope.children, isEmpty);
|
||||
expect(FocusManager.instance.rootScope.descendants.length, equals(2));
|
||||
expect(FocusManager.instance.rootScope.descendants, isNot(contains(aScope)));
|
||||
expect(FocusManager.instance.rootScope.descendants, isNot(contains(bScope)));
|
||||
});
|
||||
|
||||
// By "pinned", it means kept in the tree by a GlobalKey.
|
||||
@ -1093,7 +1105,7 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(rootNode.hasFocus, isTrue);
|
||||
expect(rootNode, equals(firstElement.owner!.focusManager.rootScope));
|
||||
expect(rootNode, equals(FocusManager.instance.rootScope.descendants.toList()[1]));
|
||||
});
|
||||
|
||||
testWidgets('Can autofocus a node.', (WidgetTester tester) async {
|
||||
@ -1278,9 +1290,9 @@ void main() {
|
||||
expect(Focus.maybeOf(element1), isNull);
|
||||
expect(Focus.maybeOf(element2), isNull);
|
||||
expect(Focus.maybeOf(element3), isNull);
|
||||
expect(Focus.of(element4).parent!.parent, equals(root));
|
||||
expect(Focus.of(element5).parent!.parent, equals(root));
|
||||
expect(Focus.of(element6).parent!.parent!.parent, equals(root));
|
||||
expect(Focus.of(element4).parent!.parent!.parent!.parent, equals(root));
|
||||
expect(Focus.of(element5).parent!.parent!.parent!.parent, equals(root));
|
||||
expect(Focus.of(element6).parent!.parent!.parent!.parent!.parent, equals(root));
|
||||
});
|
||||
testWidgets('Can traverse Focus children.', (WidgetTester tester) async {
|
||||
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
||||
@ -1492,8 +1504,9 @@ void main() {
|
||||
expect(node.hasFocus, isTrue);
|
||||
|
||||
await tester.pumpWidget(Container());
|
||||
|
||||
expect(FocusManager.instance.rootScope.descendants, isEmpty);
|
||||
// Even with no other focusable widgets, there will be the top level focus
|
||||
// traversal and view focus nodes.
|
||||
expect(FocusManager.instance.rootScope.descendants, hasLength(2));
|
||||
});
|
||||
|
||||
testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async {
|
||||
|
@ -222,19 +222,19 @@ void main() {
|
||||
tester.renderObject(find.byType(_Diagonal)).toStringDeep(),
|
||||
equalsIgnoringHashCodes(
|
||||
'_RenderDiagonal#00000 relayoutBoundary=up1\n'
|
||||
' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
|
||||
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||
' │ [root]\n'
|
||||
' │ creator: _Diagonal ← Align ← Directionality ←\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' │ ← _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||
' │ size: Size(190.0, 220.0)\n'
|
||||
' │\n'
|
||||
' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||
' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
|
||||
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||
' │ _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' │ _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' │ ← _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: BoxConstraints(unconstrained)\n'
|
||||
' │ size: Size(80.0, 100.0)\n'
|
||||
@ -242,13 +242,13 @@ void main() {
|
||||
' │\n'
|
||||
' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||
' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
|
||||
' MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||
' _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||
' TestFlutterView#00000] ← View ← [root]\n'
|
||||
' _FocusInheritedScope ← _FocusScopeWithExternalFocusNode ←\n'
|
||||
' _FocusInheritedScope ← Focus ← FocusTraversalGroup ← MediaQuery\n'
|
||||
' ← _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||
' parentData: offset=Offset(80.0, 100.0) (can use size)\n'
|
||||
' constraints: BoxConstraints(unconstrained)\n'
|
||||
' size: Size(110.0, 120.0)\n'
|
||||
' additionalConstraints: BoxConstraints(w=110.0, h=120.0)\n',
|
||||
' additionalConstraints: BoxConstraints(w=110.0, h=120.0)\n'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
@ -880,7 +880,7 @@ void main() {
|
||||
));
|
||||
expect(
|
||||
exception, endsWith(
|
||||
'← [root]"\n' // End of ownership chain.
|
||||
'_ViewScope ← ⋯"\n' // End of ownership chain.
|
||||
'Typically, the Directionality widget is introduced by the MaterialApp or WidgetsApp widget at the '
|
||||
'top of your application widget tree. It determines the ambient reading direction and is used, for '
|
||||
'example, to determine how to lay out text, how to interpret "start" and "end" values, and to resolve '
|
||||
|
@ -6,6 +6,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||
|
||||
@ -514,6 +515,52 @@ void main() {
|
||||
expect(child.debugCanParentUseSize, isTrue);
|
||||
expect(child.size, const Size(100, 200));
|
||||
});
|
||||
|
||||
testWidgets('ViewFocusEvents cause unfocusing and refocusing', (WidgetTester tester) async {
|
||||
late FlutterView view;
|
||||
late FocusNode focusNode;
|
||||
await tester.pumpWidget(
|
||||
Focus(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
view = View.of(context);
|
||||
focusNode = Focus.of(context);
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final ViewFocusEvent unfocusEvent = ViewFocusEvent(
|
||||
viewId: view.viewId,
|
||||
state: ViewFocusState.unfocused,
|
||||
direction: ViewFocusDirection.forward,
|
||||
);
|
||||
|
||||
final ViewFocusEvent focusEvent = ViewFocusEvent(
|
||||
viewId: view.viewId,
|
||||
state: ViewFocusState.focused,
|
||||
direction: ViewFocusDirection.backward,
|
||||
);
|
||||
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isFalse);
|
||||
|
||||
ServicesBinding.instance.platformDispatcher.onViewFocusChange?.call(unfocusEvent);
|
||||
await tester.pump();
|
||||
|
||||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||||
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue);
|
||||
|
||||
ServicesBinding.instance.platformDispatcher.onViewFocusChange?.call(focusEvent);
|
||||
await tester.pump();
|
||||
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
class SpyRenderWidget extends SizedBox {
|
||||
|
@ -154,6 +154,7 @@ class TestPlatformDispatcher implements PlatformDispatcher {
|
||||
}) : _platformDispatcher = platformDispatcher {
|
||||
_updateViewsAndDisplays();
|
||||
_platformDispatcher.onMetricsChanged = _handleMetricsChanged;
|
||||
_platformDispatcher.onViewFocusChange = _handleViewFocusChanged;
|
||||
}
|
||||
|
||||
/// The [PlatformDispatcher] that is wrapped by this [TestPlatformDispatcher].
|
||||
@ -176,12 +177,23 @@ class TestPlatformDispatcher implements PlatformDispatcher {
|
||||
set onMetricsChanged(VoidCallback? callback) {
|
||||
_onMetricsChanged = callback;
|
||||
}
|
||||
|
||||
void _handleMetricsChanged() {
|
||||
_updateViewsAndDisplays();
|
||||
_onMetricsChanged?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
ViewFocusChangeCallback? get onViewFocusChange => _platformDispatcher.onViewFocusChange;
|
||||
ViewFocusChangeCallback? _onViewFocusChange;
|
||||
@override
|
||||
set onViewFocusChange(ViewFocusChangeCallback? callback) {
|
||||
_onViewFocusChange = callback;
|
||||
}
|
||||
void _handleViewFocusChanged(ViewFocusEvent event) {
|
||||
_updateViewsAndDisplays();
|
||||
_onViewFocusChange?.call(event);
|
||||
}
|
||||
|
||||
@override
|
||||
Locale get locale => _localeTestValue ?? _platformDispatcher.locale;
|
||||
Locale? _localeTestValue;
|
||||
|
@ -2,7 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show AccessibilityFeatures, Brightness, Display, FlutterView, Locale, PlatformDispatcher, VoidCallback;
|
||||
import 'dart:ui' show AccessibilityFeatures, Brightness, Display, FlutterView,
|
||||
Locale, PlatformDispatcher, ViewFocusChangeCallback, VoidCallback;
|
||||
|
||||
import 'package:flutter/widgets.dart' show WidgetsBinding, WidgetsBindingObserver;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -303,4 +304,7 @@ class _FakePlatformDispatcher extends Fake implements PlatformDispatcher {
|
||||
|
||||
@override
|
||||
VoidCallback? onMetricsChanged;
|
||||
|
||||
@override
|
||||
ViewFocusChangeCallback? onViewFocusChange;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user