Unverified Commit 816ae4b1 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Include platformViewId in semantics tree (#28953)

parent 126c58ef
......@@ -7,6 +7,7 @@ import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'box.dart';
......@@ -87,6 +88,7 @@ class RenderAndroidView extends RenderBox {
_viewController = viewController {
_motionEventsDispatcher = _MotionEventsDispatcher(globalToLocal, viewController);
updateGestureRecognizers(gestureRecognizers);
_viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
}
_PlatformViewState _state = _PlatformViewState.uninitialized;
......@@ -99,10 +101,20 @@ class RenderAndroidView extends RenderBox {
/// `viewController` must not be null.
set viewController(AndroidViewController viewController) {
assert(_viewController != null);
assert(viewController != null);
if (_viewController == viewController)
return;
_viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated);
_viewController = viewController;
_sizePlatformView();
if (_viewController.isCreated) {
markNeedsSemanticsUpdate();
}
_viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
}
void _onPlatformViewCreated(int id) {
markNeedsSemanticsUpdate();
}
/// How to behave during hit testing.
......@@ -235,6 +247,17 @@ class RenderAndroidView extends RenderBox {
}
}
@override
void describeSemanticsConfiguration (SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
if (_viewController.isCreated) {
config.platformViewId = _viewController.id;
}
}
@override
void detach() {
_gestureRecognizer.reset();
......@@ -286,9 +309,9 @@ class RenderUiKitView extends RenderBox {
/// must have been created by calling [PlatformViewsService.initUiKitView].
UiKitViewController get viewController => _viewController;
UiKitViewController _viewController;
set viewController(UiKitViewController viewId) {
assert(viewId != null);
_viewController = viewId;
set viewController(UiKitViewController viewController) {
assert(viewController != null);
_viewController = viewController;
markNeedsPaint();
}
......
......@@ -196,6 +196,7 @@ class SemanticsData extends Diagnosticable {
@required this.scrollPosition,
@required this.scrollExtentMax,
@required this.scrollExtentMin,
@required this.platformViewId,
this.tags,
this.transform,
this.customSemanticsActionIds,
......@@ -295,6 +296,19 @@ class SemanticsData extends Diagnosticable {
/// * [ScrollPosition.minScrollExtent], from where this value is usually taken.
final double scrollExtentMin;
/// The id of the platform view, whose semantics nodes will be added as
/// children to this node.
///
/// If this value is non-null, the SemanticsNode must not have any children
/// as those would be replaced by the semantics nodes of the referenced
/// platform view.
///
/// See also:
///
/// * [AndroidView], which is the platform view for Android.
/// * [UiKitView], which is the platform view for iOS.
final int platformViewId;
/// The bounding box for this node in its coordinate system.
final Rect rect;
......@@ -374,6 +388,7 @@ class SemanticsData extends Diagnosticable {
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
if (textSelection?.isValid == true)
properties.add(MessageProperty('textSelection', '[${textSelection.start}, ${textSelection.end}]'));
properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
......@@ -402,6 +417,7 @@ class SemanticsData extends Diagnosticable {
&& typedOther.scrollPosition == scrollPosition
&& typedOther.scrollExtentMax == scrollExtentMax
&& typedOther.scrollExtentMin == scrollExtentMin
&& typedOther.platformViewId == platformViewId
&& typedOther.transform == transform
&& typedOther.elevation == elevation
&& typedOther.thickness == thickness
......@@ -411,6 +427,7 @@ class SemanticsData extends Diagnosticable {
@override
int get hashCode {
return ui.hashValues(
ui.hashValues(
flags,
actions,
label,
......@@ -427,9 +444,11 @@ class SemanticsData extends Diagnosticable {
scrollPosition,
scrollExtentMax,
scrollExtentMin,
platformViewId,
transform,
elevation,
thickness,
),
ui.hashList(customSemanticsActionIds),
);
}
......@@ -1464,6 +1483,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_scrollExtentMin != config._scrollExtentMin ||
_actionsAsBits != config._actionsAsBits ||
indexInParent != config.indexInParent ||
platformViewId != config.platformViewId ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
......@@ -1663,6 +1683,20 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
double get scrollExtentMin => _scrollExtentMin;
double _scrollExtentMin;
/// The id of the platform view, whose semantics nodes will be added as
/// children to this node.
///
/// If this value is non-null, the SemanticsNode must not have any children
/// as those would be replaced by the semantics nodes of the referenced
/// platform view.
///
/// See also:
///
/// * [AndroidView], which is the platform view for Android.
/// * [UiKitView], which is the platform view for iOS.
int get platformViewId => _platformViewId;
int _platformViewId;
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
......@@ -1684,6 +1718,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
if (_isDifferentFromCurrentSemanticAnnotation(config))
_markDirty();
assert(
config.platformViewId == null || childrenInInversePaintOrder.isEmpty,
'SemanticsNodes with children must not specify a platformViewId.'
);
_label = config.label;
_decreasedValue = config.decreasedValue;
_value = config.value;
......@@ -1706,6 +1745,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_scrollChildCount = config.scrollChildCount;
_scrollIndex = config.scrollIndex;
indexInParent = config.indexInParent;
_platformViewId = config._platformViewId;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert(
......@@ -1740,6 +1780,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
double scrollPosition = _scrollPosition;
double scrollExtentMax = _scrollExtentMax;
double scrollExtentMin = _scrollExtentMin;
int platformViewId = _platformViewId;
final double elevation = _elevation;
double thickness = _thickness;
final Set<int> customSemanticsActionIds = <int>{};
......@@ -1774,6 +1815,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
scrollPosition ??= node._scrollPosition;
scrollExtentMax ??= node._scrollExtentMax;
scrollExtentMin ??= node._scrollExtentMin;
platformViewId ??= node._platformViewId;
if (value == '' || value == null)
value = node._value;
if (increasedValue == '' || increasedValue == null)
......@@ -1843,6 +1885,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
platformViewId: platformViewId,
customSemanticsActionIds: customSemanticsActionIds.toList()..sort(),
);
}
......@@ -1898,6 +1941,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
textDirection: data.textDirection,
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
platformViewId: data.platformViewId != null ? data.platformViewId : -1,
scrollChildren: data.scrollChildCount != null ? data.scrollChildCount : 0,
scrollIndex: data.scrollIndex != null ? data.scrollIndex : 0 ,
scrollPosition: data.scrollPosition != null ? data.scrollPosition : double.nan,
......@@ -2038,6 +2082,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
if (_textSelection?.isValid == true)
properties.add(MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
......@@ -3100,6 +3145,16 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
/// The id of the platform view, whose semantics nodes will be added as
/// children to this node.
int get platformViewId => _platformViewId;
int _platformViewId;
set platformViewId(int value) {
if (value == platformViewId)
return;
_platformViewId = value;
_hasBeenAnnotated = true;
}
/// Whether the semantic information provided by the owning [RenderObject] and
/// all of its descendants should be treated as one logical entity.
......@@ -3583,6 +3638,9 @@ class SemanticsConfiguration {
return false;
if ((_flags & other._flags) != 0)
return false;
if (_platformViewId != null && other._platformViewId != null) {
return false;
}
if (_value != null && _value.isNotEmpty && other._value != null && other._value.isNotEmpty)
return false;
return true;
......@@ -3617,6 +3675,7 @@ class SemanticsConfiguration {
_indexInParent ??= child.indexInParent;
_scrollIndex ??= child._scrollIndex;
_scrollChildCount ??= child._scrollChildCount;
_platformViewId ??= child._platformViewId;
textDirection ??= child.textDirection;
_sortKey ??= child._sortKey;
......@@ -3672,6 +3731,7 @@ class SemanticsConfiguration {
.._indexInParent = indexInParent
.._scrollIndex = _scrollIndex
.._scrollChildCount = _scrollChildCount
.._platformViewId = _platformViewId
.._actions.addAll(_actions)
.._customSemanticsActions.addAll(_customSemanticsActions);
}
......
......@@ -25,6 +25,8 @@ final PlatformViewsRegistry platformViewsRegistry = PlatformViewsRegistry._insta
class PlatformViewsRegistry {
PlatformViewsRegistry._instance();
// Always non-negative. The id value -1 is used in the accessibility bridge
// to indicate the absence of a platform view.
int _nextPlatformViewId = 0;
/// Allocates a unique identifier for a platform view.
......@@ -77,7 +79,6 @@ class PlatformViewsService {
@required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic> creationParamsCodec,
PlatformViewCreatedCallback onPlatformViewCreated,
}) {
assert(id != null);
assert(viewType != null);
......@@ -89,7 +90,6 @@ class PlatformViewsService {
creationParams,
creationParamsCodec,
layoutDirection,
onPlatformViewCreated,
);
}
......@@ -406,7 +406,6 @@ class AndroidViewController {
dynamic creationParams,
MessageCodec<dynamic> creationParamsCodec,
TextDirection layoutDirection,
PlatformViewCreatedCallback onPlatformViewCreated,
) : assert(id != null),
assert(viewType != null),
assert(layoutDirection != null),
......@@ -415,7 +414,6 @@ class AndroidViewController {
_creationParams = creationParams,
_creationParamsCodec = creationParamsCodec,
_layoutDirection = layoutDirection,
_onPlatformViewCreated = onPlatformViewCreated,
_state = _AndroidViewState.waitingForSize;
/// Action code for when a primary pointer touched the screen.
......@@ -459,8 +457,6 @@ class AndroidViewController {
final String _viewType;
final PlatformViewCreatedCallback _onPlatformViewCreated;
/// The texture entry id into which the Android view is rendered.
int _textureId;
......@@ -478,6 +474,25 @@ class AndroidViewController {
MessageCodec<dynamic> _creationParamsCodec;
final List<PlatformViewCreatedCallback> _platformViewCreatedCallbacks = <PlatformViewCreatedCallback>[];
/// Whether the platform view has already been created.
bool get isCreated => _state == _AndroidViewState.created;
/// Adds a callback that will get invoke after the platform view has been
/// created.
void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
assert(listener != null);
assert(_state != _AndroidViewState.disposed);
_platformViewCreatedCallbacks.add(listener);
}
/// Removes a callback added with [addOnPlatformViewCreatedListener].
void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
assert(_state != _AndroidViewState.disposed);
_platformViewCreatedCallbacks.remove(listener);
}
/// Disposes the Android view.
///
/// The [AndroidViewController] object is unusable after calling this.
......@@ -486,6 +501,7 @@ class AndroidViewController {
Future<void> dispose() async {
if (_state == _AndroidViewState.creating || _state == _AndroidViewState.created)
await SystemChannels.platform_views.invokeMethod<void>('dispose', id);
_platformViewCreatedCallbacks.clear();
_state = _AndroidViewState.disposed;
}
......@@ -578,9 +594,10 @@ class AndroidViewController {
);
}
_textureId = await SystemChannels.platform_views.invokeMethod('create', args);
if (_onPlatformViewCreated != null)
_onPlatformViewCreated(id);
_state = _AndroidViewState.created;
for (PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) {
callback(id);
}
}
}
......
......@@ -367,10 +367,12 @@ class _AndroidViewState extends State<AndroidView> {
id: _id,
viewType: widget.viewType,
layoutDirection: _layoutDirection,
onPlatformViewCreated: widget.onPlatformViewCreated,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
);
if (widget.onPlatformViewCreated != null) {
_controller.addOnPlatformViewCreatedListener(widget.onPlatformViewCreated);
}
}
}
......
......@@ -346,6 +346,7 @@ void main() {
' hint: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' platformViewId: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
......@@ -440,6 +441,7 @@ void main() {
' hint: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' platformViewId: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
......
......@@ -26,6 +26,8 @@ class FakeAndroidPlatformViewsController {
Completer<void> resizeCompleter;
Completer<void> createCompleter;
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
......@@ -46,7 +48,7 @@ class FakeAndroidPlatformViewsController {
return Future<dynamic>.sync(() => null);
}
Future<dynamic> _create(MethodCall call) {
Future<dynamic> _create(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments;
final int id = args['id'];
final String viewType = args['viewType'];
......@@ -67,6 +69,10 @@ class FakeAndroidPlatformViewsController {
message: 'Trying to create a platform view of unregistered type: $viewType',
);
if (createCompleter != null) {
await createCompleter.future;
}
_views[id] = FakeAndroidPlatformView(id, viewType, Size(width, height), layoutDirection, creationParams);
final int textureId = _textureCounter++;
return Future<int>.sync(() => textureId);
......
......@@ -105,14 +105,20 @@ void main() {
final PlatformViewCreatedCallback callback = (int id) { createdViews.add(id); };
final AndroidViewController controller1 = PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, onPlatformViewCreated: callback);
id: 0,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
)..addOnPlatformViewCreatedListener(callback);
expect(createdViews, isEmpty);
await controller1.setSize(const Size(100.0, 100.0));
expect(createdViews, orderedEquals(<int>[0]));
final AndroidViewController controller2 = PlatformViewsService.initAndroidView(
id: 5, viewType: 'webview', layoutDirection: TextDirection.ltr, onPlatformViewCreated: callback);
id: 5,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
)..addOnPlatformViewCreatedListener(callback);
expect(createdViews, orderedEquals(<int>[0]));
await controller2.setSize(const Size(100.0, 200.0));
......
......@@ -806,6 +806,53 @@ void main() {
expect(factoryInvocationCount, 1);
});
testWidgets('AndroidView has correct semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
expect(currentViewId, greaterThanOrEqualTo(0));
final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController();
viewsController.registerViewType('webview');
viewsController.createCompleter = Completer<void>();
await tester.pumpWidget(
Semantics(
container: true,
child: const Align(
alignment: Alignment.bottomRight,
child: SizedBox(
width: 200.0,
height: 100.0,
child: AndroidView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
),
);
final SemanticsNode semantics = tester.getSemantics(find.byType(AndroidView));
// Platform view has not been created yet, no platformViewId.
expect(semantics.platformViewId, null);
expect(semantics.rect, Rect.fromLTWH(0, 0, 200, 100));
// A 200x100 rect positioned at bottom right of a 800x600 box.
expect(semantics.transform, Matrix4.translationValues(600, 500, 0));
expect(semantics.childrenCount, 0);
viewsController.createCompleter.complete();
await tester.pumpAndSettle();
expect(semantics.platformViewId, currentViewId + 1);
expect(semantics.rect, Rect.fromLTWH(0, 0, 200, 100));
// A 200x100 rect positioned at bottom right of a 800x600 box.
expect(semantics.transform, Matrix4.translationValues(600, 500, 0));
expect(semantics.childrenCount, 0);
handle.dispose();
});
});
group('UiKitView', () {
......
......@@ -364,6 +364,7 @@ Matcher matchesSemantics({
Size size,
double elevation,
double thickness,
int platformViewId,
// Flags //
bool hasCheckedState = false,
bool isChecked = false,
......@@ -514,6 +515,7 @@ Matcher matchesSemantics({
size: size,
elevation: elevation,
thickness: thickness,
platformViewId: platformViewId,
customActions: customActions,
hintOverrides: hintOverrides,
children: children,
......@@ -1698,6 +1700,7 @@ class _MatchesSemanticsData extends Matcher {
this.size,
this.elevation,
this.thickness,
this.platformViewId,
this.customActions,
this.hintOverrides,
this.children,
......@@ -1717,6 +1720,7 @@ class _MatchesSemanticsData extends Matcher {
final Size size;
final double elevation;
final double thickness;
final int platformViewId;
final List<Matcher> children;
@override
......@@ -1746,6 +1750,8 @@ class _MatchesSemanticsData extends Matcher {
description.add(' with elevation: $elevation');
if (thickness != null)
description.add(' with thickness: $thickness');
if (platformViewId != null)
description.add(' with platformViewId: $platformViewId');
if (customActions != null)
description.add(' with custom actions: $customActions');
if (hintOverrides != null)
......@@ -1786,6 +1792,8 @@ class _MatchesSemanticsData extends Matcher {
return failWithDescription(matchState, 'elevation was: ${data.elevation}');
if (thickness != null && thickness != data.thickness)
return failWithDescription(matchState, 'thickness was: ${data.thickness}');
if (platformViewId != null && platformViewId != data.platformViewId)
return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
if (actions != null) {
int actionBits = 0;
for (SemanticsAction action in actions)
......
......@@ -502,6 +502,7 @@ void main() {
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
platformViewId: 105,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
);
final _FakeSemanticsNode node = _FakeSemanticsNode();
......@@ -512,6 +513,7 @@ void main() {
size: const Size(10.0, 10.0),
elevation: 3.0,
thickness: 4.0,
platformViewId: 105,
/* Flags */
hasCheckedState: true,
isChecked: true,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment