diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart index 049e9627b4..3c88dd17c3 100644 --- a/packages/flutter/lib/rendering.dart +++ b/packages/flutter/lib/rendering.dart @@ -48,6 +48,7 @@ export 'src/rendering/list_wheel_viewport.dart'; export 'src/rendering/object.dart'; export 'src/rendering/paragraph.dart'; export 'src/rendering/performance_overlay.dart'; +export 'src/rendering/platform_view.dart'; export 'src/rendering/proxy_box.dart'; export 'src/rendering/rotated_box.dart'; export 'src/rendering/shifted_box.dart'; diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart new file mode 100644 index 0000000000..2b7f36facd --- /dev/null +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -0,0 +1,99 @@ +// 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 'package:flutter/services.dart'; + +import 'box.dart'; +import 'layer.dart'; +import 'object.dart'; + + +enum _PlatformViewState { + uninitialized, + resizing, + ready, +} + +/// A render object for an Android view. +/// +/// [RenderAndroidView] is responsible for sizing and displaying an Android [View](https://developer.android.com/reference/android/view/View). +/// +/// The render object's layout behavior is to fill all available space, the parent of this object must +/// provide bounded layout constraints +/// +/// See also: +/// * [AndroidView] which is a widget that is used to show an Android view. +/// * [PlatformViewsService] which is a service for controlling platform views. +class RenderAndroidView extends RenderBox { + + /// Creates a render object for an Android view. + RenderAndroidView({ + @required AndroidViewController viewController, + }) : assert(viewController != null), + _viewController = viewController; + + _PlatformViewState _state = _PlatformViewState.uninitialized; + + /// The Android view controller for the Android view associated with this render object. + AndroidViewController get viewcontroller => _viewController; + AndroidViewController _viewController; + /// Sets a new Android view controller. + /// + /// `viewController` must not be null. + set viewController(AndroidViewController viewController) { + assert(_viewController != null); + _viewController = viewController; + _sizePlatformView(); + } + + @override + bool get sizedByParent => true; + + @override + bool get alwaysNeedsCompositing => true; + + @override + bool get isRepaintBoundary => true; + + @override + void performResize() { + size = constraints.biggest; + _sizePlatformView(); + } + + Future _sizePlatformView() async { + if (_state == _PlatformViewState.resizing) { + return; + } + + _state = _PlatformViewState.resizing; + + Size targetSize; + do { + targetSize = size; + await _viewController.setSize(size); + // We've resized the platform view to targetSize, but it is possible that + // while we were resizing the render object's size was changed again. + // In that case we will resize the platform view again. + } while (size != targetSize); + + _state = _PlatformViewState.ready; + markNeedsPaint(); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (_viewController.textureId == null) + return; + + context.addLayer(new TextureLayer( + rect: offset & size, + textureId: _viewController.textureId, + )); + } +} diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart new file mode 100644 index 0000000000..784c3c524a --- /dev/null +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -0,0 +1,124 @@ +// 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/rendering.dart'; +import 'package:flutter/services.dart'; + +import 'framework.dart'; + +/// Embeds an Android view in the Widget hierarchy. +/// +/// 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. +/// +/// The widget fill all available space, the parent of this object must provide bounded layout +/// constraints. +/// +/// 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", new WebViewFactory(registrar.messenger())); +/// } +/// ``` +/// +/// The Android 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 the 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. +class AndroidView extends StatefulWidget { + /// Creates a widget that embeds an Android view. + /// + /// The `viewType` parameter must not be null. + const AndroidView({ + Key key, + @required this.viewType, + this.onPlatformViewCreated + }) : assert(viewType != 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; + + /// Callback to invoke when after the Android view has been created. + /// + /// May be null. + final OnPlatformViewCreated onPlatformViewCreated; + + @override + State createState() => new _AndroidViewState(); +} + +class _AndroidViewState extends State { + int _id; + AndroidViewController _controller; + + @override + Widget build(BuildContext context) { + return new _AndroidPlatformView(controller: _controller); + } + + @override + void initState() { + super.initState(); + _createNewAndroidView(); + } + + @override + void didUpdateWidget(AndroidView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.viewType == oldWidget.viewType) + return; + _controller.dispose(); + _createNewAndroidView(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _createNewAndroidView() { + _id = platformViewsRegistry.getNextPlatformViewId(); + _controller = PlatformViewsService.initAndroidView( + id: _id, + viewType: widget.viewType, + onPlatformViewCreated: widget.onPlatformViewCreated + ); + } + +} + +class _AndroidPlatformView extends LeafRenderObjectWidget { + const _AndroidPlatformView({ + Key key, + @required this.controller, + }) : assert(controller != null), + super(key: key); + + final AndroidViewController controller; + + @override + RenderObject createRenderObject(BuildContext context) => + new RenderAndroidView(viewController: controller); + + @override + void updateRenderObject(BuildContext context, RenderAndroidView renderObject) { + renderObject.viewController = controller; + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 9b64a7bf97..156cfc6bc9 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -63,6 +63,7 @@ export 'src/widgets/page_view.dart'; export 'src/widgets/pages.dart'; export 'src/widgets/performance_overlay.dart'; export 'src/widgets/placeholder.dart'; +export 'src/widgets/platform_view.dart'; export 'src/widgets/preferred_size.dart'; export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/raw_keyboard_listener.dart'; diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart new file mode 100644 index 0000000000..425b096e2d --- /dev/null +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -0,0 +1,163 @@ +// 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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +import '../services/fake_platform_views.dart'; + +void main() { + + testWidgets('Create Android view', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 200.0, + height: 100.0, + child: const AndroidView(viewType: 'webview'), + ) + ) + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0)) + ]) + ); + }); + + testWidgets('Resize Android view', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 200.0, + height: 100.0, + child: const AndroidView(viewType: 'webview'), + ) + ) + ); + + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 400.0, + height: 200.0, + child: const AndroidView(viewType: 'webview'), + ) + ) + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'webview', const Size(400.0, 200.0)) + ]) + ); + }); + + testWidgets('Change Android view type', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('webview'); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 200.0, + height: 100.0, + child: const AndroidView(viewType: 'webview'), + ) + ) + ); + + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 200.0, + height: 100.0, + child: const AndroidView(viewType: 'maps'), + ) + ) + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 2, 'maps', const Size(200.0, 100.0)) + ]) + ); + }); + + testWidgets('Dispose Android view', (WidgetTester tester) async { + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 200.0, + height: 100.0, + child: const AndroidView(viewType: 'webview'), + ) + ) + ); + + await tester.pumpWidget( + const Center( + child: const SizedBox( + width: 200.0, + height: 100.0, + ) + ) + ); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgets('Android view survives widget tree change', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('webview'); + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget( + new Center( + child: new SizedBox( + width: 200.0, + height: 100.0, + child: new AndroidView(viewType: 'webview', key: key), + ) + ) + ); + + await tester.pumpWidget( + new Center( + child: new Container( + child: new SizedBox( + width: 200.0, + height: 100.0, + child: new AndroidView(viewType: 'webview', key: key), + ), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0)) + ]) + ); + }); +}