blob: 0b1580f044560e3c41125896e3d88c898b49095c [file] [log] [blame]
// 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/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
/// Embeds an Android view in the Widget hierarchy.
///
/// Requires Android API level 20 or greater.
///
/// Embedding Android views is an expensive operation and should be avoided when a Flutter
/// equivalent is possible.
///
/// The embedded Android view is painted just like any other Flutter widget and transformations
/// apply to it as well.
///
/// {@template flutter.widgets.platformViews.layout}
/// The widget fills all available space, the parent of this object must provide bounded layout
/// constraints.
/// {@endtemplate}
///
/// {@template flutter.widgets.platformViews.gestures}
/// The widget participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// platform view iff it won the arena. Specific gestures that should be dispatched to the platform
/// view can be specified in the `gestureRecognizers` constructor parameter. If
/// the set of gesture recognizers is empty, a gesture will be dispatched to the platform
/// view iff it was not claimed by any other gesture recognizer.
/// {@endtemplate}
///
/// The Android view object is created using a [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html).
/// Plugins can register platform view factories with [PlatformViewRegistry#registerViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewRegistry.html#registerViewFactory-java.lang.String-io.flutter.plugin.platform.PlatformViewFactory-).
///
/// Registration is typically done in the plugin's registerWith method, e.g:
///
/// ```java
/// public static void registerWith(Registrar registrar) {
/// registrar.platformViewRegistry().registerViewFactory("webview", WebViewFactory(registrar.messenger()));
/// }
/// ```
///
/// {@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.
/// If `creationParams` is not null then `creationParamsCodec` must not be null.
/// {@endtemplate}
const AndroidView({
Key key,
@required this.viewType,
this.onPlatformViewCreated,
this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
this.layoutDirection,
this.gestureRecognizers,
this.creationParams,
this.creationParamsCodec,
}) : assert(viewType != null),
assert(hitTestBehavior != null),
assert(creationParams == null || creationParamsCodec != null),
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;
/// {@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.
///
/// {@template flutter.widgets.platformViews.gestureRecognizersDescHead}
/// The gesture recognizers built by factories in this set participate in the gesture arena for
/// each pointer that was put down on the widget. If any of these recognizers win the
/// gesture arena, the entire pointer event sequence starting from the pointer down event
/// will be dispatched to the platform view.
///
/// When null, an empty set of gesture recognizer factories is used, in which case a pointer event sequence
/// will only be dispatched to the platform view if no other member of the arena claimed it.
/// {@endtemplate}
///
/// For example, with the following setup vertical drags will not be dispatched to the Android
/// view as the vertical drag gesture is claimed by the parent [GestureDetector].
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: AndroidView(
/// viewType: 'webview',
/// ),
/// )
/// ```
/// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag
/// gesture recognizer factory in [gestureRecognizers] e.g:
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
/// new Factory<OneSequenceGestureRecognizer>(
/// () => new EagerGestureRecognizer(),
/// ),
/// ].toSet(),
/// ),
/// ),
/// )
/// ```
///
/// {@template flutter.widgets.platformViews.gestureRecognizersDescFoot}
/// A platform view can be configured to consume all pointers that were put down in its bounds
/// by passing a factory for an [EagerGestureRecognizer] in [gestureRecognizers].
/// [EagerGestureRecognizer] is a special gesture recognizer that immediately claims the gesture
/// after a pointer down event.
///
/// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type].
///
/// Changing `gestureRecognizers` results in rejection of any active gesture arenas (if the
/// platform view is actively participating in an arena).
/// {@endtemplate}
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
/// Passed as the args argument of [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-)
///
/// This can be used by plugins to pass constructor parameters to the embedded Android view.
final dynamic creationParams;
/// The codec used to encode `creationParams` before sending it to the
/// platform side. It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-).
///
/// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec].
///
/// This must not be null if [creationParams] is not null.
final MessageCodec<dynamic> creationParamsCodec;
@override
State<AndroidView> createState() => _AndroidViewState();
}
// TODO(amirh): describe the embedding mechanism.
/// Embeds an iOS view in the Widget hierarchy.
///
/// {@macro flutter.rendering.platformView.preview}
///
/// 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.gestures}
///
/// {@macro flutter.widgets.platformViews.lifetime}
///
/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints
/// nothing while maintaining the same layout constraints.
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,
this.creationParams,
this.creationParamsCodec,
this.gestureRecognizers,
}) : assert(viewType != null),
assert(hitTestBehavior != null),
assert(creationParams == null || creationParamsCodec != 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;
/// Passed as the `arguments` argument of [-\[FlutterPlatformViewFactory createWithFrame:viewIdentifier:arguments:\]](/objcdoc/Protocols/FlutterPlatformViewFactory.html#/c:objc(pl)FlutterPlatformViewFactory(im)createWithFrame:viewIdentifier:arguments:)
///
/// This can be used by plugins to pass constructor parameters to the embedded Android view.
final dynamic creationParams;
/// The codec used to encode `creationParams` before sending it to the
/// platform side. It should match the codec returned by [-\[FlutterPlatformViewFactory createArgsCodec:\]](/objcdoc/Protocols/FlutterPlatformViewFactory.html#/c:objc(pl)FlutterPlatformViewFactory(im)createArgsCodec)
///
/// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec].
///
/// This must not be null if [creationParams] is not null.
final MessageCodec<dynamic> creationParamsCodec;
/// Which gestures should be forwarded to the UIKit view.
///
/// {@macro flutter.widgets.platformViews.gestureRecognizersDescHead}
///
/// For example, with the following setup vertical drags will not be dispatched to the UIKit
/// view as the vertical drag gesture is claimed by the parent [GestureDetector].
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: UiKitView(
/// viewType: 'webview',
/// ),
/// )
/// ```
/// To get the [UiKitView] to claim the vertical drag gestures we can pass a vertical drag
/// gesture recognizer factory in [gestureRecognizers] e.g:
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
/// child: UiKitView(
/// viewType: 'webview',
/// gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
/// new Factory<OneSequenceGestureRecognizer>(
/// () => new EagerGestureRecognizer(),
/// ),
/// ].toSet(),
/// ),
/// ),
/// )
/// ```
///
/// {@macro flutter.widgets.platformViews.gestureRecognizersDescFoot}
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override
State<UiKitView> createState() => _UiKitViewState();
}
class _AndroidViewState extends State<AndroidView> {
int _id;
AndroidViewController _controller;
TextDirection _layoutDirection;
bool _initialized = false;
static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
Set<Factory<OneSequenceGestureRecognizer>>();
@override
Widget build(BuildContext context) {
return _AndroidPlatformView(
controller: _controller,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
);
}
void _initializeOnce() {
if (_initialized) {
return;
}
_initialized = true;
_createNewAndroidView();
}
@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(AndroidView oldWidget) {
super.didUpdateWidget(oldWidget);
final TextDirection newLayoutDirection = _findLayoutDirection();
final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection;
_layoutDirection = newLayoutDirection;
if (widget.viewType != oldWidget.viewType) {
_controller.dispose();
_createNewAndroidView();
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();
}
void _createNewAndroidView() {
_id = platformViewsRegistry.getNextPlatformViewId();
_controller = PlatformViewsService.initAndroidView(
id: _id,
viewType: widget.viewType,
layoutDirection: _layoutDirection,
onPlatformViewCreated: widget.onPlatformViewCreated,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
);
}
}
class _UiKitViewState extends State<UiKitView> {
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(
controller: _controller,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
);
}
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 {
final int id = platformViewsRegistry.getNextPlatformViewId();
final UiKitViewController controller = await PlatformViewsService.initUiKitView(
id: id,
viewType: widget.viewType,
layoutDirection: _layoutDirection,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
);
if (!mounted) {
controller.dispose();
return;
}
if (widget.onPlatformViewCreated != null) {
widget.onPlatformViewCreated(id);
}
setState(() { _controller = controller; });
}
}
class _AndroidPlatformView extends LeafRenderObjectWidget {
const _AndroidPlatformView({
Key key,
@required this.controller,
@required this.hitTestBehavior,
@required this.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
super(key: key);
final AndroidViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior;
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override
RenderObject createRenderObject(BuildContext context) =>
RenderAndroidView(
viewController: controller,
hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
);
@override
void updateRenderObject(BuildContext context, RenderAndroidView renderObject) {
renderObject.viewController = controller;
renderObject.hitTestBehavior = hitTestBehavior;
renderObject.updateGestureRecognizers(gestureRecognizers);
}
}
class _UiKitPlatformView extends LeafRenderObjectWidget {
const _UiKitPlatformView({
Key key,
@required this.controller,
@required this.hitTestBehavior,
@required this.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
super(key: key);
final UiKitViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior;
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderUiKitView(
viewController: controller,
hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
);
}
@override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
renderObject.viewController = controller;
renderObject.hitTestBehavior = hitTestBehavior;
renderObject.updateGestureRecognizers(gestureRecognizers);
}
}