Re-land "Initial framework support for iOS platform views." (#23781)
Re-landing #23412 with a fix to the PlatformLayer's addToScene signature. #23412 was broken by the change to Layer done in #23434. It seemed green as the presubmits were done before #23434 was landed, and when #23412 landed it broke the build. Reverts #23779
This commit is contained in:
parent
7d74a30168
commit
8de86d412e
@ -357,6 +357,41 @@ class TextureLayer extends Layer {
|
||||
S find<S>(Offset regionOffset) => null;
|
||||
}
|
||||
|
||||
/// A layer that shows an embedded [UIView](https://developer.apple.com/documentation/uikit/uiview)
|
||||
/// on iOS.
|
||||
class PlatformViewLayer extends Layer {
|
||||
/// Creates a platform view layer.
|
||||
///
|
||||
/// The `rect` and `viewId` parameters must not be null.
|
||||
PlatformViewLayer({
|
||||
@required this.rect,
|
||||
@required this.viewId,
|
||||
}): assert(rect != null), assert(viewId != null);
|
||||
|
||||
/// Bounding rectangle of this layer in the global coordinate space.
|
||||
final Rect rect;
|
||||
|
||||
/// The unique identifier of the UIView displayed on this layer.
|
||||
///
|
||||
/// A UIView with this identifier must have been created by [PlatformViewsServices.initUiKitView].
|
||||
final int viewId;
|
||||
|
||||
@override
|
||||
ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
|
||||
final Rect shiftedRect = rect.shift(layerOffset);
|
||||
builder.addPlatformView(
|
||||
viewId,
|
||||
offset: shiftedRect.topLeft,
|
||||
width: shiftedRect.width,
|
||||
height: shiftedRect.height,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
S find<S>(Offset regionOffset) => null;
|
||||
}
|
||||
|
||||
/// A layer that indicates to the compositor that it should display
|
||||
/// certain performance statistics within it.
|
||||
///
|
||||
|
@ -43,8 +43,10 @@ enum _PlatformViewState {
|
||||
/// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an
|
||||
/// Android [View](https://developer.android.com/reference/android/view/View).
|
||||
///
|
||||
/// {@template flutter.rendering.platformView.layout}
|
||||
/// The render object's layout behavior is to fill all available space, the parent of this object must
|
||||
/// provide bounded layout constraints.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// RenderAndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
|
||||
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
|
||||
@ -233,6 +235,81 @@ class RenderAndroidView extends RenderBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is work in progress, not yet ready to be used, and requires a custom engine build. A render object for an iOS UIKit UIView.
|
||||
///
|
||||
/// [RenderUiKitView] is responsible for sizing and displaying an iOS
|
||||
/// [UIView](https://developer.apple.com/documentation/uikit/uiview).
|
||||
///
|
||||
/// UIViews are added as sub views of the FlutterView and are composited by Quartz.
|
||||
///
|
||||
/// {@macro flutter.rendering.platformView.layout}
|
||||
///
|
||||
/// See also:
|
||||
/// * [UiKitView] which is a widget that is used to show a UIView.
|
||||
/// * [PlatformViewsService] which is a service for controlling platform views.
|
||||
class RenderUiKitView extends RenderBox {
|
||||
/// Creates a render object for an iOS UIView.
|
||||
///
|
||||
/// The `viewId` and `hitTestBehavior` parameters must not be null.
|
||||
RenderUiKitView({
|
||||
@required int viewId,
|
||||
@required this.hitTestBehavior,
|
||||
}) : assert(viewId != null),
|
||||
assert(hitTestBehavior != null),
|
||||
_viewId = viewId;
|
||||
|
||||
|
||||
/// The unique identifier of the UIView controlled by this controller.
|
||||
///
|
||||
/// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView
|
||||
/// must have been created by calling [PlatformViewsService.initUiKitView].
|
||||
int get viewId => _viewId;
|
||||
int _viewId;
|
||||
set viewId(int viewId) {
|
||||
assert(viewId != null);
|
||||
_viewId = viewId;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// How to behave during hit testing.
|
||||
// The implicit setter is enough here as changing this value will just affect
|
||||
// any newly arriving events there's nothing we need to invalidate.
|
||||
PlatformViewHitTestBehavior hitTestBehavior;
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => true;
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
|
||||
@override
|
||||
void performResize() {
|
||||
size = constraints.biggest;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.addLayer(PlatformViewLayer(
|
||||
rect: offset & size,
|
||||
viewId: _viewId,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(HitTestResult result, { Offset position }) {
|
||||
if (hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position))
|
||||
return false;
|
||||
result.add(BoxHitTestEntry(this, position));
|
||||
return hitTestBehavior == PlatformViewHitTestBehavior.opaque;
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent;
|
||||
}
|
||||
|
||||
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) {
|
||||
team = GestureArenaTeam();
|
||||
|
@ -91,6 +91,46 @@ class PlatformViewsService {
|
||||
onPlatformViewCreated,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands.
|
||||
/// This is work in progress, not yet ready to be used, and requires a custom engine build. Creates a controller for a new iOS UIView.
|
||||
///
|
||||
/// `id` is an unused unique identifier generated with [platformViewsRegistry].
|
||||
///
|
||||
/// `viewType` is the identifier of the iOS view type to be created, a
|
||||
/// factory for this view type must have been registered on the platform side.
|
||||
/// Platform view factories are typically registered by plugin code.
|
||||
///
|
||||
/// The `id, `viewType, and `layoutDirection` parameters must not be null.
|
||||
/// If `creationParams` is non null then `cretaionParamsCodec` must not be null.
|
||||
static Future<UiKitViewController> initUiKitView({
|
||||
@required int id,
|
||||
@required String viewType,
|
||||
@required TextDirection layoutDirection,
|
||||
dynamic creationParams,
|
||||
MessageCodec<dynamic> creationParamsCodec,
|
||||
}) async {
|
||||
assert(id != null);
|
||||
assert(viewType != null);
|
||||
assert(layoutDirection != null);
|
||||
assert(creationParams == null || creationParamsCodec != null);
|
||||
|
||||
// TODO(amirh): pass layoutDirection once the system channel supports it.
|
||||
final Map<String, dynamic> args = <String, dynamic> {
|
||||
'id': id,
|
||||
'viewType': viewType,
|
||||
};
|
||||
if (creationParams != null) {
|
||||
final ByteData paramsByteData = creationParamsCodec.encodeMessage(creationParams);
|
||||
args['params'] = Uint8List.view(
|
||||
paramsByteData.buffer,
|
||||
0,
|
||||
paramsByteData.lengthInBytes,
|
||||
);
|
||||
}
|
||||
await SystemChannels.platform_views.invokeMethod('create', args);
|
||||
return UiKitViewController._(id, layoutDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties of an Android pointer.
|
||||
@ -455,8 +495,7 @@ class AndroidViewController {
|
||||
///
|
||||
/// The first time a size is set triggers the creation of the Android view.
|
||||
Future<void> setSize(Size size) async {
|
||||
if (_state == _AndroidViewState.disposed)
|
||||
throw FlutterError('trying to size a disposed Android View. View id: $id');
|
||||
assert(_state != _AndroidViewState.disposed, 'trying to size a disposed Android View. View id: $id');
|
||||
|
||||
assert(size != null);
|
||||
assert(!size.isEmpty);
|
||||
@ -473,8 +512,7 @@ class AndroidViewController {
|
||||
|
||||
/// Sets the layout direction for the Android view.
|
||||
Future<void> setLayoutDirection(TextDirection layoutDirection) async {
|
||||
if (_state == _AndroidViewState.disposed)
|
||||
throw FlutterError('trying to set a layout direction for a disposed Android View. View id: $id');
|
||||
assert(_state != _AndroidViewState.disposed,'trying to set a layout direction for a disposed UIView. View id: $id');
|
||||
|
||||
if (layoutDirection == _layoutDirection)
|
||||
return;
|
||||
@ -544,3 +582,48 @@ class AndroidViewController {
|
||||
_state = _AndroidViewState.created;
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls an iOS UIView.
|
||||
///
|
||||
/// Typically created with [PlatformViewsService.initUiKitView].
|
||||
class UiKitViewController {
|
||||
UiKitViewController._(
|
||||
this.id,
|
||||
TextDirection layoutDirection,
|
||||
) : assert(id != null),
|
||||
assert(layoutDirection != null),
|
||||
_layoutDirection = layoutDirection;
|
||||
|
||||
|
||||
/// The unique identifier of the iOS view controlled by this controller.
|
||||
///
|
||||
/// This identifer is typically generated by [PlatformViewsRegistry.getNextPlatformViewId].
|
||||
final int id;
|
||||
|
||||
bool _debugDisposed = false;
|
||||
|
||||
TextDirection _layoutDirection;
|
||||
|
||||
/// Sets the layout direction for the Android view.
|
||||
Future<void> setLayoutDirection(TextDirection layoutDirection) async {
|
||||
assert(!_debugDisposed, 'trying to set a layout direction for a disposed Android View. View id: $id');
|
||||
|
||||
if (layoutDirection == _layoutDirection)
|
||||
return;
|
||||
|
||||
assert(layoutDirection != null);
|
||||
_layoutDirection = layoutDirection;
|
||||
|
||||
// TODO(amirh): invoke the iOS platform views channel direction method once available.
|
||||
}
|
||||
|
||||
/// Disposes the view.
|
||||
///
|
||||
/// The [UiKitViewController] object is unusable after calling this.
|
||||
/// The `id` of the platform view cannot be reused after the view is
|
||||
/// disposed.
|
||||
Future<void> dispose() async {
|
||||
_debugDisposed = true;
|
||||
await SystemChannels.platform_views.invokeMethod('dispose', id);
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,10 @@ import 'framework.dart';
|
||||
/// The embedded Android view is painted just like any other Flutter widget and transformations
|
||||
/// apply to it as well.
|
||||
///
|
||||
/// The widget fill all available space, the parent of this object must provide bounded layout
|
||||
/// {@template flutter.widgets.platformViews.layout}
|
||||
/// The widget fills all available space, the parent of this object must provide bounded layout
|
||||
/// constraints.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// AndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
|
||||
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
|
||||
@ -41,19 +43,23 @@ import 'framework.dart';
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The Android view's lifetime is the same as the lifetime of the [State] object for this widget.
|
||||
/// {@template flutter.widgets.platformViews.lifetime}
|
||||
/// The platform view's lifetime is the same as the lifetime of the [State] object for this widget.
|
||||
/// When the [State] is disposed the platform view (and auxiliary resources) are lazily
|
||||
/// released (some resources are immediately released and some by platform garbage collector).
|
||||
/// A stateful widget's state is disposed when the widget is removed from the tree or when it is
|
||||
/// moved within the tree. If the stateful widget has a key and it's only moved relative to its siblings,
|
||||
/// or it has a [GlobalKey] and it's moved within the tree, it will not be disposed.
|
||||
/// {@endtemplate}
|
||||
class AndroidView extends StatefulWidget {
|
||||
/// Creates a widget that embeds an Android view.
|
||||
///
|
||||
/// {@template flutter.widgets.platformViews.constructorParams}
|
||||
/// The `viewType` and `hitTestBehavior` parameters must not be null.
|
||||
/// {@endtemplate}
|
||||
/// If `creationParams` is not null then `creationParamsCodec` must not be null.
|
||||
AndroidView({ // ignore: prefer_const_constructors_in_immutables
|
||||
// TODO(aam): Remove lint ignore above once dartbug.com/34297 is fixed
|
||||
// TODO(aam): Remove lint ignore above once https://dartbug.com/34297 is fixed
|
||||
Key key,
|
||||
@required this.viewType,
|
||||
this.onPlatformViewCreated,
|
||||
@ -68,25 +74,32 @@ class AndroidView extends StatefulWidget {
|
||||
super(key: key);
|
||||
|
||||
/// The unique identifier for Android view type to be embedded by this widget.
|
||||
///
|
||||
/// A [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html)
|
||||
/// for this type must have been registered.
|
||||
///
|
||||
/// See also: [AndroidView] for an example of registering a platform view factory.
|
||||
final String viewType;
|
||||
|
||||
/// Callback to invoke after the Android view has been created.
|
||||
/// {@template flutter.widgets.platformViews.createdParam}
|
||||
/// Callback to invoke after the platform view has been created.
|
||||
///
|
||||
/// May be null.
|
||||
/// {@endtemplate}
|
||||
final PlatformViewCreatedCallback onPlatformViewCreated;
|
||||
|
||||
/// {@template flutter.widgets.platformViews.hittestParam}
|
||||
/// How this widget should behave during hit testing.
|
||||
///
|
||||
/// This defaults to [PlatformViewHitTestBehavior.opaque].
|
||||
/// {@endtemplate}
|
||||
final PlatformViewHitTestBehavior hitTestBehavior;
|
||||
|
||||
/// {@template flutter.widgets.platformViews.directionParam}
|
||||
/// The text direction to use for the embedded view.
|
||||
///
|
||||
/// If this is null, the ambient [Directionality] is used instead.
|
||||
/// {@endtemplate}
|
||||
final TextDirection layoutDirection;
|
||||
|
||||
/// Which gestures should be forwarded to the Android view.
|
||||
@ -157,7 +170,50 @@ class AndroidView extends StatefulWidget {
|
||||
final MessageCodec<dynamic> creationParamsCodec;
|
||||
|
||||
@override
|
||||
State createState() => _AndroidViewState();
|
||||
State<AndroidView> createState() => _AndroidViewState();
|
||||
}
|
||||
|
||||
// TODO(amirh): describe the embedding mechanism.
|
||||
/// This is work in progress, not yet ready to be used, and requires a custom engine build. Embeds an iOS view in the Widget hierarchy.
|
||||
///
|
||||
/// Embedding iOS views is an expensive operation and should be avoided when a Flutter
|
||||
/// equivalent is possible.
|
||||
///
|
||||
/// {@macro flutter.widgets.platformViews.layout}
|
||||
///
|
||||
/// {@macro flutter.widgets.platformViews.lifetime}
|
||||
class UiKitView extends StatefulWidget {
|
||||
/// Creates a widget that embeds an iOS view.
|
||||
///
|
||||
/// {@macro flutter.widgets.platformViews.constructorParams}
|
||||
UiKitView({ // ignore: prefer_const_constructors_in_immutables
|
||||
// TODO(aam): Remove lint ignore above once https://dartbug.com/34297 is fixed
|
||||
Key key,
|
||||
@required this.viewType,
|
||||
this.onPlatformViewCreated,
|
||||
this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
|
||||
this.layoutDirection,
|
||||
}) : assert(viewType != null),
|
||||
assert(hitTestBehavior != null),
|
||||
super(key: key);
|
||||
|
||||
// TODO(amirh): reference the iOS API doc once avaliable.
|
||||
/// The unique identifier for iOS view type to be embedded by this widget.
|
||||
///
|
||||
/// A PlatformViewFactory for this type must have been registered.
|
||||
final String viewType;
|
||||
|
||||
/// {@macro flutter.widgets.platformViews.createdParam}
|
||||
final PlatformViewCreatedCallback onPlatformViewCreated;
|
||||
|
||||
/// {@macro flutter.widgets.platformViews.hittestParam}
|
||||
final PlatformViewHitTestBehavior hitTestBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.platformViews.directionParam}
|
||||
final TextDirection layoutDirection;
|
||||
|
||||
@override
|
||||
State<UiKitView> createState() => _UiKitViewState();
|
||||
}
|
||||
|
||||
class _AndroidViewState extends State<AndroidView> {
|
||||
@ -183,19 +239,17 @@ class _AndroidViewState extends State<AndroidView> {
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
_layoutDirection = _findLayoutDirection();
|
||||
_createNewAndroidView();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_initializeOnce();
|
||||
|
||||
final TextDirection newLayoutDirection = _findLayoutDirection();
|
||||
final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection;
|
||||
_layoutDirection = newLayoutDirection;
|
||||
|
||||
_initializeOnce();
|
||||
if (didChangeLayoutDirection) {
|
||||
// The native view will update asynchronously, in the meantime we don't want
|
||||
// to block the framework. (so this is intentionally not awaiting).
|
||||
@ -246,6 +300,97 @@ class _AndroidViewState extends State<AndroidView> {
|
||||
}
|
||||
}
|
||||
|
||||
class _UiKitViewState extends State<UiKitView> {
|
||||
int _id;
|
||||
UiKitViewController _controller;
|
||||
TextDirection _layoutDirection;
|
||||
bool _initialized = false;
|
||||
|
||||
static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
|
||||
Set<Factory<OneSequenceGestureRecognizer>>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_controller == null) {
|
||||
return const SizedBox.expand();
|
||||
}
|
||||
return _UiKitPlatformView(
|
||||
viewId: _id,
|
||||
hitTestBehavior: widget.hitTestBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
void _initializeOnce() {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
_createNewUiKitView();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final TextDirection newLayoutDirection = _findLayoutDirection();
|
||||
final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection;
|
||||
_layoutDirection = newLayoutDirection;
|
||||
|
||||
_initializeOnce();
|
||||
if (didChangeLayoutDirection) {
|
||||
// The native view will update asynchronously, in the meantime we don't want
|
||||
// to block the framework. (so this is intentionally not awaiting).
|
||||
_controller?.setLayoutDirection(_layoutDirection);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(UiKitView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final TextDirection newLayoutDirection = _findLayoutDirection();
|
||||
final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection;
|
||||
_layoutDirection = newLayoutDirection;
|
||||
|
||||
if (widget.viewType != oldWidget.viewType) {
|
||||
_controller?.dispose();
|
||||
_createNewUiKitView();
|
||||
return;
|
||||
}
|
||||
|
||||
if (didChangeLayoutDirection) {
|
||||
_controller?.setLayoutDirection(_layoutDirection);
|
||||
}
|
||||
}
|
||||
|
||||
TextDirection _findLayoutDirection() {
|
||||
assert(widget.layoutDirection != null || debugCheckHasDirectionality(context));
|
||||
return widget.layoutDirection ?? Directionality.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _createNewUiKitView() async {
|
||||
_id = platformViewsRegistry.getNextPlatformViewId();
|
||||
final UiKitViewController controller = await PlatformViewsService.initUiKitView(
|
||||
id: _id,
|
||||
viewType: widget.viewType,
|
||||
layoutDirection: _layoutDirection,
|
||||
);
|
||||
if (!mounted) {
|
||||
controller.dispose();
|
||||
return;
|
||||
}
|
||||
if (widget.onPlatformViewCreated != null) {
|
||||
widget.onPlatformViewCreated(_id);
|
||||
}
|
||||
setState(() { _controller = controller; });
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidPlatformView extends LeafRenderObjectWidget {
|
||||
const _AndroidPlatformView({
|
||||
Key key,
|
||||
@ -276,3 +421,30 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
|
||||
renderObject.updateGestureRecognizers(gestureRecognizers);
|
||||
}
|
||||
}
|
||||
|
||||
class _UiKitPlatformView extends LeafRenderObjectWidget {
|
||||
const _UiKitPlatformView({
|
||||
Key key,
|
||||
@required this.viewId,
|
||||
@required this.hitTestBehavior,
|
||||
}) : assert(viewId != null),
|
||||
assert(hitTestBehavior != null),
|
||||
super(key: key);
|
||||
|
||||
final int viewId;
|
||||
final PlatformViewHitTestBehavior hitTestBehavior;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderUiKitView(
|
||||
viewId: viewId,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
|
||||
renderObject.viewId = viewId;
|
||||
renderObject.hitTestBehavior = hitTestBehavior;
|
||||
}
|
||||
}
|
||||
|
@ -6,21 +6,19 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class FakePlatformViewsController {
|
||||
FakePlatformViewsController(this.targetPlatform) : assert(targetPlatform != null) {
|
||||
class FakeAndroidPlatformViewsController {
|
||||
FakeAndroidPlatformViewsController() {
|
||||
SystemChannels.platform_views.setMockMethodCallHandler(_onMethodCall);
|
||||
}
|
||||
|
||||
final TargetPlatform targetPlatform;
|
||||
|
||||
Iterable<FakePlatformView> get views => _views.values;
|
||||
final Map<int, FakePlatformView> _views = <int, FakePlatformView>{};
|
||||
Iterable<FakeAndroidPlatformView> get views => _views.values;
|
||||
final Map<int, FakeAndroidPlatformView> _views = <int, FakeAndroidPlatformView>{};
|
||||
|
||||
final Map<int, List<FakeMotionEvent>> motionEvents = <int, List<FakeMotionEvent>>{};
|
||||
final Map<int, List<FakeAndroidMotionEvent>> motionEvents = <int, List<FakeAndroidMotionEvent>>{};
|
||||
|
||||
final Set<String> _registeredViewTypes = Set<String>();
|
||||
|
||||
@ -33,12 +31,6 @@ class FakePlatformViewsController {
|
||||
}
|
||||
|
||||
Future<dynamic> _onMethodCall(MethodCall call) {
|
||||
if (targetPlatform == TargetPlatform.android)
|
||||
return _onMethodCallAndroid(call);
|
||||
return Future<dynamic>.sync(() => null);
|
||||
}
|
||||
|
||||
Future<dynamic> _onMethodCallAndroid(MethodCall call) {
|
||||
switch(call.method) {
|
||||
case 'create':
|
||||
return _create(call);
|
||||
@ -75,7 +67,7 @@ class FakePlatformViewsController {
|
||||
message: 'Trying to create a platform view of unregistered type: $viewType',
|
||||
);
|
||||
|
||||
_views[id] = FakePlatformView(id, viewType, Size(width, height), layoutDirection, creationParams);
|
||||
_views[id] = FakeAndroidPlatformView(id, viewType, Size(width, height), layoutDirection, creationParams);
|
||||
final int textureId = _textureCounter++;
|
||||
return Future<int>.sync(() => textureId);
|
||||
}
|
||||
@ -129,9 +121,9 @@ class FakePlatformViewsController {
|
||||
}
|
||||
|
||||
if (!motionEvents.containsKey(id))
|
||||
motionEvents[id] = <FakeMotionEvent> [];
|
||||
motionEvents[id] = <FakeAndroidMotionEvent> [];
|
||||
|
||||
motionEvents[id].add(FakeMotionEvent(action, pointerIds, pointerOffsets));
|
||||
motionEvents[id].add(FakeAndroidMotionEvent(action, pointerIds, pointerOffsets));
|
||||
return Future<dynamic>.sync(() => null);
|
||||
}
|
||||
|
||||
@ -152,9 +144,77 @@ class FakePlatformViewsController {
|
||||
}
|
||||
}
|
||||
|
||||
class FakePlatformView {
|
||||
class FakeIosPlatformViewsController {
|
||||
FakeIosPlatformViewsController() {
|
||||
SystemChannels.platform_views.setMockMethodCallHandler(_onMethodCall);
|
||||
}
|
||||
|
||||
FakePlatformView(this.id, this.type, this.size, this.layoutDirection, [this.creationParams]);
|
||||
|
||||
Iterable<FakeUiKitView> get views => _views.values;
|
||||
final Map<int, FakeUiKitView> _views = <int, FakeUiKitView>{};
|
||||
|
||||
final Set<String> _registeredViewTypes = Set<String>();
|
||||
|
||||
// When this completer is non null, the 'create' method channel call will be
|
||||
// delayed until it completes.
|
||||
Completer<void> creationDelay;
|
||||
|
||||
void registerViewType(String viewType) {
|
||||
_registeredViewTypes.add(viewType);
|
||||
}
|
||||
|
||||
Future<dynamic> _onMethodCall(MethodCall call) {
|
||||
switch(call.method) {
|
||||
case 'create':
|
||||
return _create(call);
|
||||
case 'dispose':
|
||||
return _dispose(call);
|
||||
}
|
||||
return Future<dynamic>.sync(() => null);
|
||||
}
|
||||
|
||||
Future<dynamic> _create(MethodCall call) async {
|
||||
if (creationDelay != null)
|
||||
await creationDelay.future;
|
||||
final Map<dynamic, dynamic> args = call.arguments;
|
||||
final int id = args['id'];
|
||||
final String viewType = args['viewType'];
|
||||
|
||||
if (_views.containsKey(id)) {
|
||||
throw PlatformException(
|
||||
code: 'error',
|
||||
message: 'Trying to create an already created platform view, view id: $id',
|
||||
);
|
||||
}
|
||||
|
||||
if (!_registeredViewTypes.contains(viewType)) {
|
||||
throw PlatformException(
|
||||
code: 'error',
|
||||
message: 'Trying to create a platform view of unregistered type: $viewType',
|
||||
);
|
||||
}
|
||||
|
||||
_views[id] = FakeUiKitView(id, viewType);
|
||||
return Future<int>.sync(() => null);
|
||||
}
|
||||
|
||||
Future<dynamic> _dispose(MethodCall call) {
|
||||
final int id = call.arguments;
|
||||
|
||||
if (!_views.containsKey(id)) {
|
||||
throw PlatformException(
|
||||
code: 'error',
|
||||
message: 'Trying to dispose a platform view with unknown id: $id',
|
||||
);
|
||||
}
|
||||
|
||||
_views.remove(id);
|
||||
return Future<dynamic>.sync(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeAndroidPlatformView {
|
||||
FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, [this.creationParams]);
|
||||
|
||||
final int id;
|
||||
final String type;
|
||||
@ -164,9 +224,9 @@ class FakePlatformView {
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other is! FakePlatformView)
|
||||
if (other.runtimeType != FakeAndroidPlatformView)
|
||||
return false;
|
||||
final FakePlatformView typedOther = other;
|
||||
final FakeAndroidPlatformView typedOther = other;
|
||||
return id == typedOther.id &&
|
||||
type == typedOther.type &&
|
||||
creationParams == typedOther.creationParams &&
|
||||
@ -178,12 +238,12 @@ class FakePlatformView {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FakePlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection, creationParams: $creationParams)';
|
||||
return 'FakeAndroidPlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection, creationParams: $creationParams)';
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMotionEvent {
|
||||
const FakeMotionEvent(this.action, this.pointerIds, this.pointers);
|
||||
class FakeAndroidMotionEvent {
|
||||
const FakeAndroidMotionEvent(this.action, this.pointerIds, this.pointers);
|
||||
|
||||
final int action;
|
||||
final List<Offset> pointers;
|
||||
@ -192,9 +252,9 @@ class FakeMotionEvent {
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other is! FakeMotionEvent)
|
||||
if (other is! FakeAndroidMotionEvent)
|
||||
return false;
|
||||
final FakeMotionEvent typedOther = other;
|
||||
final FakeAndroidMotionEvent typedOther = other;
|
||||
const ListEquality<Offset> offsetsEq = ListEquality<Offset>();
|
||||
const ListEquality<int> pointersEq = ListEquality<int>();
|
||||
return pointersEq.equals(pointerIds, typedOther.pointerIds) &&
|
||||
@ -207,6 +267,30 @@ class FakeMotionEvent {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FakeMotionEvent(action: $action, pointerIds: $pointerIds, pointers: $pointers)';
|
||||
return 'FakeAndroidMotionEvent(action: $action, pointerIds: $pointerIds, pointers: $pointers)';
|
||||
}
|
||||
}
|
||||
|
||||
class FakeUiKitView {
|
||||
FakeUiKitView(this.id, this.type);
|
||||
|
||||
final int id;
|
||||
final String type;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != FakeUiKitView)
|
||||
return false;
|
||||
final FakeUiKitView typedOther = other;
|
||||
return id == typedOther.id &&
|
||||
type == typedOther.type;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(id, type);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FakeIosPlatformView(id: $id, type: $type)';
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../flutter_test_alternative.dart';
|
||||
@ -10,11 +9,11 @@ import '../flutter_test_alternative.dart';
|
||||
import 'fake_platform_views.dart';
|
||||
|
||||
void main() {
|
||||
FakePlatformViewsController viewsController;
|
||||
|
||||
group('Android', () {
|
||||
FakeAndroidPlatformViewsController viewsController;
|
||||
setUp(() {
|
||||
viewsController = FakePlatformViewsController(TargetPlatform.android);
|
||||
viewsController = FakeAndroidPlatformViewsController();
|
||||
});
|
||||
|
||||
test('create Android view of unregistered type', () async {
|
||||
@ -38,9 +37,9 @@ void main() {
|
||||
.setSize(const Size(200.0, 300.0));
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
FakePlatformView(1, 'webview', const Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl),
|
||||
unorderedEquals(<FakeAndroidPlatformView>[
|
||||
FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
FakeAndroidPlatformView(1, 'webview', const Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl),
|
||||
]));
|
||||
});
|
||||
|
||||
@ -65,11 +64,11 @@ void main() {
|
||||
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
|
||||
await viewController.setSize(const Size(200.0, 300.0));
|
||||
|
||||
viewController.dispose();
|
||||
await viewController.dispose();
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
unorderedEquals(<FakeAndroidPlatformView>[
|
||||
FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
]));
|
||||
});
|
||||
|
||||
@ -94,9 +93,9 @@ void main() {
|
||||
await viewController.setSize(const Size(500.0, 500.0));
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
FakePlatformView(1, 'webview', const Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
unorderedEquals(<FakeAndroidPlatformView>[
|
||||
FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
FakeAndroidPlatformView(1, 'webview', const Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
]));
|
||||
});
|
||||
|
||||
@ -129,8 +128,8 @@ void main() {
|
||||
await viewController.setSize(const Size(100.0, 100.0));
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
unorderedEquals(<FakeAndroidPlatformView>[
|
||||
FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
|
||||
]));
|
||||
});
|
||||
|
||||
@ -142,9 +141,88 @@ void main() {
|
||||
await viewController.setLayoutDirection(TextDirection.rtl);
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl),
|
||||
unorderedEquals(<FakeAndroidPlatformView>[
|
||||
FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl),
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
group('iOS', ()
|
||||
{
|
||||
FakeIosPlatformViewsController viewsController;
|
||||
setUp(() {
|
||||
viewsController = FakeIosPlatformViewsController();
|
||||
});
|
||||
|
||||
test('create iOS view of unregistered type', () async {
|
||||
expect(
|
||||
() {
|
||||
return PlatformViewsService.initUiKitView(
|
||||
id: 0,
|
||||
viewType: 'web',
|
||||
layoutDirection: TextDirection.ltr,
|
||||
);
|
||||
},
|
||||
throwsA(isInstanceOf<PlatformException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('create iOS views', () async {
|
||||
viewsController.registerViewType('webview');
|
||||
await PlatformViewsService.initUiKitView(
|
||||
id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
|
||||
await PlatformViewsService.initUiKitView(
|
||||
id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl);
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakeUiKitView>[
|
||||
FakeUiKitView(0, 'webview'),
|
||||
FakeUiKitView(1, 'webview'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('reuse iOS view id', () async {
|
||||
viewsController.registerViewType('webview');
|
||||
await PlatformViewsService.initUiKitView(
|
||||
id: 0,
|
||||
viewType: 'webview',
|
||||
layoutDirection: TextDirection.ltr,
|
||||
);
|
||||
expect(
|
||||
() => PlatformViewsService.initUiKitView(
|
||||
id: 0, viewType: 'web', layoutDirection: TextDirection.ltr),
|
||||
throwsA(isInstanceOf<PlatformException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('dispose iOS view', () async {
|
||||
viewsController.registerViewType('webview');
|
||||
await PlatformViewsService.initUiKitView(
|
||||
id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
|
||||
final UiKitViewController viewController = await PlatformViewsService.initUiKitView(
|
||||
id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
|
||||
|
||||
viewController.dispose();
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakeUiKitView>[
|
||||
FakeUiKitView(0, 'webview'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('dispose inexisting iOS view', () async {
|
||||
viewsController.registerViewType('webview');
|
||||
await PlatformViewsService.initUiKitView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
|
||||
final UiKitViewController viewController = await PlatformViewsService.initUiKitView(
|
||||
id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
|
||||
await viewController.dispose();
|
||||
expect(
|
||||
() async {
|
||||
await viewController.dispose();
|
||||
},
|
||||
throwsA(isInstanceOf<PlatformException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user