Unverified Commit 13bd52bf authored by Harry Terkelsen's avatar Harry Terkelsen Committed by GitHub

Add HtmlElementView (the Flutter Web platform view) (#37819)

* Implement HtmlView, the platform view widget for Flutter Web

* Document HtmlView constructor

* Respond to review comments

* - Rename to HtmlElementView
- Assert running on web
- Move test to Chrome-only file
parent 2a59acc0
......@@ -296,6 +296,124 @@ class UiKitView extends StatefulWidget {
State<UiKitView> createState() => _UiKitViewState();
}
/// Embeds an HTML element in the Widget hierarchy in Flutter Web.
///
/// *NOTE*: This only works in Flutter Web. To embed web content on other
/// platforms, consider using the `flutter_webview` plugin.
///
/// Embedding HTML is an expensive operation and should be avoided when a
/// Flutter equivalent is possible.
///
/// The embedded HTML is painted just like any other Flutter widget and
/// transformations apply to it as well. This widget should only be used in
/// Flutter Web.
///
/// {@macro flutter.widgets.platformViews.layout}
///
/// Due to security restrictions with cross-origin `<iframe>` elements, Flutter
/// cannot dispatch pointer events to an HTML view. If an `<iframe>` is the
/// target of an event, the window containing the `<iframe>` is not notified
/// of the event. In particular, this means that any pointer events which land
/// on an `<iframe>` will not be seen by Flutter, and so the HTML view cannot
/// participate in gesture detection with other widgets.
///
/// The way we enable accessibility on Flutter for web is to have a full-page
/// button which waits for a double tap. Placing this full-page button in front
/// of the scene would cause platform views not to receive pointer events. The
/// tradeoff is that by placing the scene in front of the semantics placeholder
/// will cause platform views to block pointer events from reaching the
/// placeholder. This means that in order to enable accessibility, you must
/// double tap the app *outside of a platform view*. As a consequence, a
/// full-screen platform view will make it impossible to enable accessibility.
/// Make sure that your HTML views are sized no larger than necessary, or you
/// may cause difficulty for users trying to enable accessibility.
///
/// {@macro flutter.widgets.platformViews.lifetime}
class HtmlElementView extends StatelessWidget {
/// Creates a platform view for Flutter Web.
///
/// `viewType` identifies the type of platform view to create.
const HtmlElementView({
Key key,
@required this.viewType,
}) : assert(viewType != null),
assert(kIsWeb, 'HtmlElementView is only available on Flutter Web.'),
super(key: key);
/// The unique identifier for the HTML view type to be embedded by this widget.
///
/// A PlatformViewFactory for this type must have been registered.
final String viewType;
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: viewType,
onCreatePlatformView: _createHtmlElementView,
surfaceFactory: (BuildContext context, PlatformViewController controller) {
return PlatformViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);
}
/// Creates the controller and kicks off its initialization.
_HtmlElementViewController _createHtmlElementView(PlatformViewCreationParams params) {
final _HtmlElementViewController controller = _HtmlElementViewController(params.id, viewType);
controller._initialize().then((_) { params.onPlatformViewCreated(params.id); });
return controller;
}
}
class _HtmlElementViewController extends PlatformViewController {
_HtmlElementViewController(
this.viewId,
this.viewType,
);
@override
final int viewId;
/// The unique identifier for the HTML view type to be embedded by this widget.
///
/// A PlatformViewFactory for this type must have been registered.
final String viewType;
bool _initialized = false;
Future<void> _initialize() async {
final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': viewType,
};
await SystemChannels.platform_views.invokeMethod<void>('create', args);
_initialized = true;
}
@override
void clearFocus() {
// Currently this does nothing on Flutter Web.
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
}
@override
void dispatchPointerEvent(PointerEvent event) {
// We do not dispatch pointer events to HTML views because they may contain
// cross-origin iframes, which only accept user-generated events.
}
@override
void dispose() {
if (_initialized) {
// Asynchronously dispose this view.
SystemChannels.platform_views.invokeMethod<void>('dispose', viewId);
}
}
}
class _AndroidViewState extends State<AndroidView> {
int _id;
AndroidViewController _controller;
......
......@@ -310,6 +310,73 @@ class FakeIosPlatformViewsController {
}
}
class FakeHtmlPlatformViewsController {
FakeHtmlPlatformViewsController() {
SystemChannels.platform_views.setMockMethodCallHandler(_onMethodCall);
}
Iterable<FakeHtmlPlatformView> get views => _views.values;
final Map<int, FakeHtmlPlatformView> _views = <int, FakeHtmlPlatformView>{};
final Set<String> _registeredViewTypes = <String>{};
Completer<void> resizeCompleter;
Completer<void> createCompleter;
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 {
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',
);
if (createCompleter != null) {
await createCompleter.future;
}
_views[id] = FakeHtmlPlatformView(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]);
......@@ -393,3 +460,27 @@ class FakeUiKitView {
return 'FakeUiKitView(id: $id, type: $type, creationParams: $creationParams)';
}
}
class FakeHtmlPlatformView {
FakeHtmlPlatformView(this.id, this.type);
final int id;
final String type;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != FakeHtmlPlatformView)
return false;
final FakeHtmlPlatformView typedOther = other;
return id == typedOther.id &&
type == typedOther.type;
}
@override
int get hashCode => hashValues(id, type);
@override
String toString() {
return 'FakeHtmlPlatformView(id: $id, type: $type)';
}
}
// Copyright 2019 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.
@TestOn('chrome')
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../services/fake_platform_views.dart';
void main() {
group('HtmlElementView', () {
testWidgets('Create HTML view', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
expect(
viewsController.views,
unorderedEquals(<FakeHtmlPlatformView>[
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
]),
);
});
testWidgets('Resize HTML view', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
viewsController.resizeCompleter = Completer<void>();
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 100.0,
height: 50.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
viewsController.resizeCompleter.complete();
await tester.pump();
expect(
viewsController.views,
unorderedEquals(<FakeHtmlPlatformView>[
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
]),
);
});
testWidgets('Change HTML view type', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController();
viewsController.registerViewType('webview');
viewsController.registerViewType('maps');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'maps'),
),
),
);
expect(
viewsController.views,
unorderedEquals(<FakeHtmlPlatformView>[
FakeHtmlPlatformView(currentViewId + 2, 'maps'),
]),
);
});
testWidgets('Dispose HTML view', (WidgetTester tester) async {
final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
),
),
);
expect(
viewsController.views,
isEmpty,
);
});
testWidgets('HTML view survives widget tree change', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController();
viewsController.registerViewType('webview');
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview', key: key),
),
),
);
await tester.pumpWidget(
Center(
child: Container(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview', key: key),
),
),
),
);
expect(
viewsController.views,
unorderedEquals(<FakeHtmlPlatformView>[
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
]),
);
});
testWidgets('HtmlElementView has correct semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
expect(currentViewId, greaterThanOrEqualTo(0));
final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Semantics(
container: true,
child: const Align(
alignment: Alignment.bottomRight,
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(
viewType: 'webview',
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final SemanticsNode semantics = tester.getSemantics(find.byType(HtmlElementView));
expect(semantics.platformViewId, currentViewId + 1);
expect(semantics.rect, const Rect.fromLTWH(0, 0, 200, 100));
// A 200x100 rect positioned at bottom right of a 800x600 box.
expect(semantics.transform, Matrix4.translationValues(600, 500, 0));
expect(semantics.childrenCount, 0);
handle.dispose();
});
});
}
......@@ -15,7 +15,6 @@ import 'package:flutter_test/flutter_test.dart';
import '../services/fake_platform_views.dart';
void main() {
group('AndroidView', () {
testWidgets('Create Android view', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
......
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