Unverified Commit 67ffe1c2 authored by Amir Hardon's avatar Amir Hardon Committed by GitHub

Initial framework support for iOS platform views. (#23412)

This PR adds the full framework stack (layer->render object->widget, and
service) for embedding iOS views with minimal functionality.

I allowed myself to throw the entire framework stack in one PR since we're mostly
mirroring the structure we already established for embedded Android views, so this PR
is a little bigger than usual. I'm happy to break it down to the
different pieces of the stack if reviewers prefer.

Specifically this PR adds:
* A UiKitView widget for embedding a UIView in the widget tree.
* A RenderUiKitView which is the render object for showing a
  UIView.
* A PlatformViewLayer which denotes the position of a UIView in the
  layer tree.
* The iOS platform_views system channel client code in services/platform_views.dart
* Splits the fake platform views controller to an Android and iOS
  controllers.

TBD in following PRs:
* Plumb the layout direction all the way to the platform view (currently
  there is still no engine support for it).
* Integrate with gesture arenas (engine support is still missing as
  well).
parent 50098f14
......@@ -357,6 +357,40 @@ 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
void 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,
);
}
@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);
}
Iterable<FakeUiKitView> get views => _views.values;
final Map<int, FakeUiKitView> _views = <int, FakeUiKitView>{};
FakePlatformView(this.id, this.type, this.size, this.layoutDirection, [this.creationParams]);
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>()),
);
});
});
}
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