Unverified Commit 48f44172 authored by amirh's avatar amirh Committed by GitHub

Introduce PlatformViewsService for controlling platform views. (#19325)

Currently only Android views are supported.

This also includes a FakePlatformViewsController which is a fake
implementation of the engine side for testing.
parent cf83b790
...@@ -18,6 +18,7 @@ export 'src/services/message_codec.dart'; ...@@ -18,6 +18,7 @@ export 'src/services/message_codec.dart';
export 'src/services/message_codecs.dart'; export 'src/services/message_codecs.dart';
export 'src/services/platform_channel.dart'; export 'src/services/platform_channel.dart';
export 'src/services/platform_messages.dart'; export 'src/services/platform_messages.dart';
export 'src/services/platform_views.dart';
export 'src/services/raw_keyboard.dart'; export 'src/services/raw_keyboard.dart';
export 'src/services/system_channels.dart'; export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart'; export 'src/services/system_chrome.dart';
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'system_channels.dart';
/// The [PlatformViewsRegistry] responsible for generating unique identifiers for platform views.
final PlatformViewsRegistry platformViewsRegistry = PlatformViewsRegistry._instance();
/// A registry responsible for generating unique identifier for platform views.
///
/// A Flutter application has a single [PlatformViewsRegistry] which can be accesses
/// through the [platformViewsRegistry] getter.
///
/// See also:
/// * [PlatformView], a widget that shows a platform view.
class PlatformViewsRegistry {
PlatformViewsRegistry._instance();
int _nextPlatformViewId = 0;
/// Allocates a unique identifier for a platform view.
///
/// A platform view identifier can refer to a platform view that was never created,
/// a platform view that was disposed, or a platform view that is alive.
///
/// Typically a platform view identifier is passed to a [PlatformView] widget
/// which creates the platform view and manages its lifecycle.
int getNextPlatformViewId() => _nextPlatformViewId++;
}
/// Callback signature for when a platform view was created.
///
/// `id` is the platform view's unique identifier.
typedef void OnPlatformViewCreated(int id);
/// Provides access to the platform views service.
///
/// This service allows creating and controlling Android views.
///
/// See also: [PlatformView].
class PlatformViewsService {
PlatformViewsService._();
/// Creates a controller for a new Android view.
///
/// `id` is an unused unique identifier generated with [platformViewsRegistry].
///
/// `viewType` is the identifier of the Android 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.
/// Plugins can register a platform view factory with
/// [PlatformViewRegistry#registerViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewRegistry.html#registerViewFactory-java.lang.String-io.flutter.plugin.platform.PlatformViewFactory-).
///
/// The Android view will only be created after [AndroidViewController.setSize] is called for the
/// first time.
static AndroidViewController initAndroidView({
@required int id,
@required String viewType,
OnPlatformViewCreated onPlatformViewCreated,
}) {
assert(id != null);
assert(viewType != null);
return new AndroidViewController._(
id,
viewType,
onPlatformViewCreated
);
}
}
enum _AndroidViewState {
waitingForSize,
creating,
created,
createFailed,
disposed,
}
/// Controls an Android view.
///
/// Typically created with [PlatformViewsService.initAndroidView].
class AndroidViewController {
AndroidViewController._(
this.id,
String viewType,
OnPlatformViewCreated onPlatformViewCreated,
) : assert(id != null),
assert(viewType != null),
_viewType = viewType,
_onPlatformViewCreated = onPlatformViewCreated,
_state = _AndroidViewState.waitingForSize;
/// The unique identifier of the Android view controlled by this controller.
final int id;
final String _viewType;
final OnPlatformViewCreated _onPlatformViewCreated;
/// The texture entry id into which the Android view is rendered.
int _textureId;
/// Returns the texture entry id that the Android view is rendering into.
///
/// Returns null if the Android view has not been successfully created, or if it has been
/// disposed.
int get textureId => _textureId;
_AndroidViewState _state;
/// Disposes the Android view.
///
/// The [AndroidViewController] object is unusable after calling this.
/// The identifier of the platform view cannot be reused after the view is
/// disposed.
Future<void> dispose() async {
if (_state == _AndroidViewState.creating || _state == _AndroidViewState.created)
await SystemChannels.platform_views.invokeMethod('dispose', id);
_state = _AndroidViewState.disposed;
}
/// Sizes the Android View.
///
/// `size` is the view's new size in logical pixel, and must not be null.
///
/// 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 new FlutterError('trying to size a disposed Android View. View id: $id');
assert(size != null);
if (_state == _AndroidViewState.waitingForSize)
return _create(size);
await SystemChannels.platform_views.invokeMethod('resize', <String, dynamic> {
'id': id,
'width': size.width,
'height': size.height,
});
}
Future<void> _create(Size size) async {
_textureId = await SystemChannels.platform_views.invokeMethod('create', <String, dynamic> {
'id': id,
'viewType': _viewType,
'width': size.width,
'height': size.height,
});
if (_onPlatformViewCreated != null)
_onPlatformViewCreated(id);
_state = _AndroidViewState.created;
}
}
...@@ -216,4 +216,12 @@ class SystemChannels { ...@@ -216,4 +216,12 @@ class SystemChannels {
const StandardMessageCodec(), const StandardMessageCodec(),
); );
/// A [MethodChannel] for controlling platform views.
///
/// See also: [PlatformViewsService] for the available operations on this channel.
static const MethodChannel platform_views = const MethodChannel(
'flutter/platform_views',
const StandardMethodCodec(),
);
} }
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
class FakePlatformViewsController {
FakePlatformViewsController(this.targetPlatform) : assert(targetPlatform != null) {
SystemChannels.platform_views.setMockMethodCallHandler(_onMethodCall);
}
final TargetPlatform targetPlatform;
final Map<int, FakePlatformView> _views = <int, FakePlatformView>{};
final Set<String> _registeredViewTypes = new Set<String>();
int _textureCounter = 0;
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
Future<dynamic> _onMethodCall(MethodCall call) {
if (targetPlatform == TargetPlatform.android)
return _onMethodCallAndroid(call);
return new Future<Null>.sync(() => null);
}
Future<dynamic> _onMethodCallAndroid(MethodCall call) {
switch(call.method) {
case 'create':
return _create(call);
case 'dispose':
return _dispose(call);
case 'resize':
return _resize(call);
}
return new Future<Null>.sync(() => null);
}
Future<dynamic> _create(MethodCall call) {
final Map<dynamic, dynamic> args = call.arguments;
final int id = args['id'];
final String viewType = args['viewType'];
final double width = args['width'];
final double height = args['height'];
if (_views.containsKey(id))
throw new PlatformException(
code: 'error',
message: 'Trying to create an already created platform view, view id: $id',
);
if (!_registeredViewTypes.contains(viewType))
throw new PlatformException(
code: 'error',
message: 'Trying to create a platform view of unregistered type: $viewType',
);
_views[id] = new FakePlatformView(id, viewType, new Size(width, height));
final int textureId = _textureCounter++;
return new Future<int>.sync(() => textureId);
}
Future<dynamic> _dispose(MethodCall call) {
final int id = call.arguments;
if (!_views.containsKey(id))
throw new PlatformException(
code: 'error',
message: 'Trying to dispose a platform view with unknown id: $id',
);
_views.remove(id);
return new Future<Null>.sync(() => null);
}
Future<dynamic> _resize(MethodCall call) {
final Map<dynamic, dynamic> args = call.arguments;
final int id = args['id'];
final double width = args['width'];
final double height = args['height'];
if (!_views.containsKey(id))
throw new PlatformException(
code: 'error',
message: 'Trying to resize a platform view with unknown id: $id',
);
_views[id].size = new Size(width, height);
return new Future<Null>.sync(() => null);
}
Iterable<FakePlatformView> get views => _views.values;
}
class FakePlatformView {
FakePlatformView(this.id, this.type, this.size);
final int id;
final String type;
Size size;
@override
bool operator ==(dynamic other) {
if (other is! FakePlatformView)
return false;
final FakePlatformView typedOther = other;
return id == typedOther.id &&
type == typedOther.type &&
size == typedOther.size;
}
@override
int get hashCode => hashValues(id, type, size);
@override
String toString() {
return 'FakePlatformView(id: $id, type: $type, size: $size)';
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 'package:test/test.dart';
import 'fake_platform_views.dart';
void main() {
FakePlatformViewsController viewsController;
group('Android', () {
setUp(() {
viewsController = new FakePlatformViewsController(TargetPlatform.android);
});
test('create Android view of unregistered type', () async {
expect(
() => PlatformViewsService.initAndroidView(
id: 0, viewType: 'web').setSize(const Size(100.0, 100.0)),
throwsA(const isInstanceOf<PlatformException>()));
});
test('create Android views', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0));
await PlatformViewsService.initAndroidView(
id: 1, viewType: 'webview').setSize(const Size(200.0, 300.0));
expect(
viewsController.views,
unorderedEquals(<FakePlatformView>[
new FakePlatformView(0, 'webview', const Size(100.0, 100.0)),
new FakePlatformView(1, 'webview', const Size(200.0, 300.0)),
]));
});
test('reuse Android view id', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0));
expect(
() => PlatformViewsService.initAndroidView(
id: 0, viewType: 'web').setSize(const Size(100.0, 100.0)),
throwsA(const isInstanceOf<PlatformException>()));
});
test('dispose Android view', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0));
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview');
await viewController.setSize(const Size(200.0, 300.0));
viewController.dispose();
expect(
viewsController.views,
unorderedEquals(<FakePlatformView>[
new FakePlatformView(0, 'webview', const Size(100.0, 100.0)),
]));
});
test('dispose inexisting Android view', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0));
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview');
await viewController.setSize(const Size(200.0, 300.0));
await viewController.dispose();
await viewController.dispose();
});
test('resize Android view', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0));
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview');
await viewController.setSize(const Size(200.0, 300.0));
await viewController.setSize(const Size(500.0, 500.0));
expect(
viewsController.views,
unorderedEquals(<FakePlatformView>[
new FakePlatformView(0, 'webview', const Size(100.0, 100.0)),
new FakePlatformView(1, 'webview', const Size(500.0, 500.0)),
]));
});
test('OnPlatformViewCreated callback', () async {
viewsController.registerViewType('webview');
final List<int> createdViews = <int>[];
final OnPlatformViewCreated callback = (int id) { createdViews.add(id); };
final AndroidViewController controller1 = PlatformViewsService.initAndroidView(
id: 0, viewType: 'webview', onPlatformViewCreated: 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', onPlatformViewCreated: callback);
expect(createdViews, orderedEquals(<int>[0]));
await controller2.setSize(const Size(100.0, 200.0));
expect(createdViews, orderedEquals(<int>[0, 5]));
});
});
}
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