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,25 +427,28 @@ class SemanticsData extends Diagnosticable {
@override
int get hashCode {
return ui.hashValues(
flags,
actions,
label,
value,
increasedValue,
decreasedValue,
hint,
textDirection,
rect,
tags,
textSelection,
scrollChildCount,
scrollIndex,
scrollPosition,
scrollExtentMax,
scrollExtentMin,
transform,
elevation,
thickness,
ui.hashValues(
flags,
actions,
label,
value,
increasedValue,
decreasedValue,
hint,
textDirection,
rect,
tags,
textSelection,
scrollChildCount,
scrollIndex,
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