Add MacOS AppKitView class. (#132583)

Add derived classes from the Darwin platform view base classes for
MacOS. Functionality is largely the same as the `UiKitView`, but the two
are decoupled and and can further diverge in the future as needed. Some
unit tests remain skipped for now as the gesture recognizers for MacOS
are not yet implemented.

https://github.com/flutter/flutter/issues/128519

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat

---------

Co-authored-by: Chris Bracken <chris@bracken.jp>
This commit is contained in:
yaakovschectman 2023-08-31 17:48:12 -04:00 committed by GitHub
parent b4753c328d
commit e6e44de33c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1205 additions and 14 deletions

View File

@ -279,7 +279,10 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
RenderDarwinPlatformView({
required T viewController,
required this.hitTestBehavior,
}) : _viewController = viewController;
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : _viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
/// The unique identifier of the platform view controlled by this controller.
@ -313,6 +316,8 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
PointerEvent? _lastPointerDownEvent;
_UiKitViewGestureRecognizer? _gestureRecognizer;
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.biggest;
@ -374,6 +379,9 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
super.detach();
}
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers);
}
/// A render object for an iOS UIKit UIView.
@ -401,14 +409,11 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
RenderUiKitView({
required super.viewController,
required super.hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers
}) {
updateGestureRecognizers(gestureRecognizers);
}
required super.gestureRecognizers,
});
// TODO(schectman): Add gesture functionality to macOS platform view when implemented.
// https://github.com/flutter/flutter/issues/128519
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
@override
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
@ -431,8 +436,6 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
_lastPointerDownEvent = event.original ?? event;
}
_UiKitViewGestureRecognizer? _gestureRecognizer;
@override
void detach() {
_gestureRecognizer!.reset();
@ -440,6 +443,24 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
}
}
/// A render object for a macOS platform view.
class RenderAppKitView extends RenderDarwinPlatformView<AppKitViewController> {
/// Creates a render object for a macOS AppKitView.
RenderAppKitView({
required super.viewController,
required super.hitTestBehavior,
required super.gestureRecognizers,
});
// TODO(schectman): Add gesture functionality to macOS platform view when implemented.
// https://github.com/flutter/flutter/issues/128519
// This method will need to behave the same as the same-named method for RenderUiKitView,
// but use a _AppKitViewGestureRecognizer or equivalent, whose constructor shall accept an
// AppKitViewController.
@override
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {}
}
// This recognizer constructs gesture recognizers from a set of gesture recognizer factories
// it was give, adds all of them to a gesture arena team with the _UiKitViewGestureRecognizer
// as the team captain.

View File

@ -220,6 +220,7 @@ class PlatformViewsService {
assert(creationParams == null || creationParamsCodec != null);
// TODO(amirh): pass layoutDirection once the system channel supports it.
// https://github.com/flutter/flutter/issues/133682
final Map<String, dynamic> args = <String, dynamic>{
'id': id,
'viewType': viewType,
@ -238,6 +239,49 @@ class PlatformViewsService {
}
return UiKitViewController._(id, layoutDirection);
}
/// Factory method to create an `AppKitView`.
///
/// `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.
///
/// `onFocus` is a callback that will be invoked when the UIKit view asks to
/// get the input focus.
/// The `id, `viewType, and `layoutDirection` parameters must not be null.
/// If `creationParams` is non null then `creationParamsCodec` must not be null.
static Future<AppKitViewController> initAppKitView({
required int id,
required String viewType,
required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) async {
assert(creationParams == null || creationParamsCodec != null);
// TODO(amirh): pass layoutDirection once the system channel supports it.
// https://github.com/flutter/flutter/issues/133682
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<void>('create', args);
if (onFocus != null) {
_instance._focusCallbacks[id] = onFocus;
}
return AppKitViewController._(id, layoutDirection);
}
}
/// Properties of an Android pointer.
@ -1317,7 +1361,6 @@ abstract class DarwinPlatformViewController {
TextDirection layoutDirection,
) : _layoutDirection = layoutDirection;
/// The unique identifier of the iOS view controlled by this controller.
///
/// This identifier is typically generated by
@ -1389,6 +1432,14 @@ class UiKitViewController extends DarwinPlatformViewController {
);
}
/// Controller for a macOS platform view.
class AppKitViewController extends DarwinPlatformViewController {
AppKitViewController._(
super.id,
super.layoutDirection,
);
}
/// An interface for controlling a single platform view.
///
/// Used by [PlatformViewSurface] to interface with the platform view it embeds.

View File

@ -325,6 +325,40 @@ class UiKitView extends _DarwinView {
State<UiKitView> createState() => _UiKitViewState();
}
/// Widget that contains a macOS AppKit view.
///
/// Embedding macOS views is an expensive operation and should be avoided where
/// a Flutter equivalent is possible.
///
/// 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.
///
/// Construction of AppKitViews is done asynchronously, before the underlying
/// NSView is ready this widget paints nothing while maintaining the same
/// layout constraints.
class AppKitView extends _DarwinView {
/// Creates a widget that embeds a macOS AppKit NSView.
const AppKitView({
super.key,
required super.viewType,
super.onPlatformViewCreated,
super.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
super.layoutDirection,
super.creationParams,
super.creationParamsCodec,
super.gestureRecognizers,
});
@override
State<AppKitView> createState() => _AppKitViewState();
}
/// Callback signature for when the platform view's DOM element was created.
///
/// [element] is the DOM element that was created.
@ -711,6 +745,31 @@ class _UiKitViewState extends _DarwinViewState<UiKitView, UiKitViewController, R
}
}
class _AppKitViewState extends _DarwinViewState<AppKitView, AppKitViewController, RenderAppKitView, _AppKitPlatformView> {
@override
Future<AppKitViewController> createNewViewController(int id) async {
return PlatformViewsService.initAppKitView(
id: id,
viewType: widget.viewType,
layoutDirection: _layoutDirection!,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
onFocus: () {
focusNode?.requestFocus();
}
);
}
@override
_AppKitPlatformView childPlatformView() {
return _AppKitPlatformView(
controller: _controller!,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _DarwinViewState._emptyRecognizersSet,
);
}
}
class _AndroidPlatformView extends LeafRenderObjectWidget {
const _AndroidPlatformView({
required this.controller,
@ -758,7 +817,8 @@ abstract class _DarwinPlatformView<TController extends DarwinPlatformViewControl
void updateRenderObject(BuildContext context, TRender renderObject) {
renderObject
..viewController = controller
..hitTestBehavior = hitTestBehavior;
..hitTestBehavior = hitTestBehavior
..updateGestureRecognizers(gestureRecognizers);
}
}
@ -773,11 +833,18 @@ class _UiKitPlatformView extends _DarwinPlatformView<UiKitViewController, Render
gestureRecognizers: gestureRecognizers,
);
}
}
class _AppKitPlatformView extends _DarwinPlatformView<AppKitViewController, RenderAppKitView> {
const _AppKitPlatformView({required super.controller, required super.hitTestBehavior, required super.gestureRecognizers});
@override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
super.updateRenderObject(context, renderObject);
renderObject.updateGestureRecognizers(gestureRecognizers);
RenderObject createRenderObject(BuildContext context) {
return RenderAppKitView(
viewController: controller,
hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
);
}
}

View File

@ -471,6 +471,109 @@ class FakeIosPlatformViewsController {
}
}
class FakeMacosPlatformViewsController {
FakeMacosPlatformViewsController() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall);
}
Iterable<FakeAppKitView> get views => _views.values;
final Map<int, FakeAppKitView> _views = <int, FakeAppKitView>{};
final Set<String> _registeredViewTypes = <String>{};
// When this completer is non null, the 'create' method channel call will be
// delayed until it completes.
Completer<void>? creationDelay;
// Maps a view id to the number of gestures it accepted so far.
final Map<int, int> gesturesAccepted = <int, int>{};
// Maps a view id to the number of gestures it rejected so far.
final Map<int, int> gesturesRejected = <int, int>{};
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
void invokeViewFocused(int viewId) {
final MethodCodec codec = SystemChannels.platform_views.codec;
final ByteData data = codec.encodeMethodCall(MethodCall('viewFocused', viewId));
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.handlePlatformMessage(SystemChannels.platform_views.name, data, (ByteData? data) {});
}
Future<dynamic> _onMethodCall(MethodCall call) {
switch (call.method) {
case 'create':
return _create(call);
case 'dispose':
return _dispose(call);
case 'acceptGesture':
return _acceptGesture(call);
case 'rejectGesture':
return _rejectGesture(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 as Map<dynamic, dynamic>;
final int id = args['id'] as int;
final String viewType = args['viewType'] as String;
final Uint8List? creationParams = args['params'] as Uint8List?;
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] = FakeAppKitView(id, viewType, creationParams);
gesturesAccepted[id] = 0;
gesturesRejected[id] = 0;
return Future<int?>.sync(() => null);
}
Future<dynamic> _acceptGesture(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
final int id = args['id'] as int;
gesturesAccepted[id] = gesturesAccepted[id]! + 1;
return Future<int?>.sync(() => null);
}
Future<dynamic> _rejectGesture(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
final int id = args['id'] as int;
gesturesRejected[id] = gesturesRejected[id]! + 1;
return Future<int?>.sync(() => null);
}
Future<dynamic> _dispose(MethodCall call) {
final int id = call.arguments as int;
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);
}
}
@immutable
class FakeAndroidPlatformView {
const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection,
@ -585,3 +688,31 @@ class FakeUiKitView {
return 'FakeUiKitView(id: $id, type: $type, creationParams: $creationParams)';
}
}
@immutable
class FakeAppKitView {
const FakeAppKitView(this.id, this.type, [this.creationParams]);
final int id;
final String type;
final Uint8List? creationParams;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FakeAppKitView
&& other.id == id
&& other.type == type
&& other.creationParams == creationParams;
}
@override
int get hashCode => Object.hash(id, type);
@override
String toString() {
return 'FakeAppKitView(id: $id, type: $type, creationParams: $creationParams)';
}
}

View File

@ -2224,6 +2224,927 @@ void main() {
});
});
group('AppKitView', () {
testWidgets('Create AppView', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
);
expect(
viewsController.views,
unorderedEquals(<FakeAppKitView>[
FakeAppKitView(currentViewId + 1, 'webview'),
]),
);
});
testWidgets('Change AppKitView view type', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
viewsController.registerViewType('maps');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'maps', layoutDirection: TextDirection.ltr),
),
),
);
expect(
viewsController.views,
unorderedEquals(<FakeAppKitView>[
FakeAppKitView(currentViewId + 2, 'maps'),
]),
);
});
testWidgets('Dispose AppKitView ', (WidgetTester tester) async {
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
),
),
);
expect(
viewsController.views,
isEmpty,
);
});
testWidgets('Dispose AppKitView before creation completed ', (WidgetTester tester) async {
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
viewsController.creationDelay = Completer<void>();
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
),
),
);
viewsController.creationDelay!.complete();
expect(
viewsController.views,
isEmpty,
);
});
testWidgets('AppKitView survives widget tree change', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key),
),
),
);
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key),
),
),
);
expect(
viewsController.views,
unorderedEquals(<FakeAppKitView>[
FakeAppKitView(currentViewId + 1, 'webview'),
]),
);
});
testWidgets('Create AppKitView with params', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
creationParams: 'creation parameters',
creationParamsCodec: StringCodec(),
),
),
),
);
final FakeAppKitView fakeView = viewsController.views.first;
final Uint8List rawCreationParams = fakeView.creationParams!;
final ByteData byteData = ByteData.view(
rawCreationParams.buffer,
rawCreationParams.offsetInBytes,
rawCreationParams.lengthInBytes,
);
final dynamic actualParams = const StringCodec().decodeMessage(byteData);
expect(actualParams, 'creation parameters');
expect(
viewsController.views,
unorderedEquals(<FakeAppKitView>[
FakeAppKitView(currentViewId + 1, 'webview', fakeView.creationParams),
]),
);
});
// TODO(schectman): De-skip the following tests once macOS gesture recognizers are present.
// https://github.com/flutter/flutter/issues/128519
testWidgets('AppKitView accepts gestures', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
}, skip: true); // https://github.com/flutter/flutter/issues/128519
testWidgets('AppKitView transparent hit test behavior', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
int numPointerDownsOnParent = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent e) {
numPointerDownsOnParent++;
},
),
const Positioned(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.transparent,
layoutDirection: TextDirection.ltr,
),
),
),
],
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
expect(numPointerDownsOnParent, 1);
}, skip: true); // https://github.com/flutter/flutter/issues/128519
testWidgets('AppKitView translucent hit test behavior', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
int numPointerDownsOnParent = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent e) {
numPointerDownsOnParent++;
},
),
const Positioned(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.translucent,
layoutDirection: TextDirection.ltr,
),
),
),
],
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(numPointerDownsOnParent, 1);
}, skip: true); // https://github.com/flutter/flutter/issues/128519
testWidgets('AppKitView opaque hit test behavior', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
int numPointerDownsOnParent = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent e) {
numPointerDownsOnParent++;
},
),
const Positioned(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
],
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(numPointerDownsOnParent, 0);
}, skip: true); // https://github.com/flutter/flutter/issues/128519
testWidgets('UiKitView can lose gesture arenas', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
bool verticalDragAcceptedByParent = false;
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: Container(
margin: const EdgeInsets.all(10.0),
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
verticalDragAcceptedByParent = true;
},
child: const SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, true);
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
expect(viewsController.gesturesRejected[currentViewId + 1], 1);
});
testWidgets('UiKitView tap gesture recognizers', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
bool gestureAcceptedByParent = false;
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
gestureAcceptedByParent = true;
},
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() {
return VerticalDragGestureRecognizer();
},
),
},
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(gestureAcceptedByParent, false);
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
testWidgets('UiKitView long press gesture recognizers', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
bool gestureAcceptedByParent = false;
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onLongPress: () {
gestureAcceptedByParent = true;
},
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<LongPressGestureRecognizer>(
() {
return LongPressGestureRecognizer();
},
),
},
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
await tester.longPressAt(const Offset(50.0, 50.0));
expect(gestureAcceptedByParent, false);
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
testWidgets('UiKitView drag gesture recognizers', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
bool verticalDragAcceptedByParent = false;
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
verticalDragAcceptedByParent = true;
},
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<TapGestureRecognizer>(
() {
return TapGestureRecognizer();
},
),
},
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
await tester.tapAt(const Offset(50.0, 50.0));
expect(verticalDragAcceptedByParent, false);
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
testWidgets('UiKitView can claim gesture after all pointers are up', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
bool verticalDragAcceptedByParent = false;
// The long press recognizer rejects the gesture after the AndroidView gets the pointer up event.
// This test makes sure that the Android view can win the gesture after it got the pointer up event.
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
verticalDragAcceptedByParent = true;
},
onLongPress: () { },
child: const SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(verticalDragAcceptedByParent, false);
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
testWidgets('UiKitView rebuilt during gesture', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await tester.pumpWidget(
const Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
);
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
testWidgets('UiKitView with eager gesture recognizer', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) { },
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
},
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
await tester.startGesture(const Offset(50.0, 50.0));
// Normally (without the eager gesture recognizer) after just the pointer down event
// no gesture arena member will claim the arena (so no motion events will be dispatched to
// the Android view). Here we assert that with the eager recognizer in the gesture team the
// pointer down event is immediately dispatched.
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
testWidgets('UiKitView rejects gestures absorbed by siblings', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Stack(
alignment: Alignment.topLeft,
children: <Widget>[
const UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
Container(
color: const Color.fromARGB(255, 255, 255, 255),
width: 100,
height: 100,
),
],
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(viewsController.gesturesRejected[currentViewId + 1], 1);
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
});
testWidgets(
'UiKitView rejects gestures absorbed by siblings if the touch is outside of the platform view bounds but inside platform view frame',
(WidgetTester tester) async {
// UiKitView is positioned at (left=0, top=100, right=300, bottom=600).
// Opaque container is on top of the UiKitView positioned at (left=0, top=500, right=300, bottom=600).
// Touch on (550, 150) is expected to be absorbed by the container.
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
SizedBox(
width: 300,
height: 600,
child: Stack(
alignment: Alignment.topLeft,
children: <Widget>[
Transform.translate(
offset: const Offset(0, 100),
child: const SizedBox(
width: 300,
height: 500,
child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
Transform.translate(
offset: const Offset(0, 500),
child: Container(
color: const Color.fromARGB(255, 255, 255, 255),
width: 300,
height: 100,
),
),
],
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final TestGesture gesture = await tester.startGesture(const Offset(150, 550));
await gesture.up();
expect(viewsController.gesturesRejected[currentViewId + 1], 1);
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
},
);
testWidgets('UiKitView rebuilt with same gestureRecognizers', (WidgetTester tester) async {
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
int factoryInvocationCount = 0;
EagerGestureRecognizer constructRecognizer() {
factoryInvocationCount += 1;
return EagerGestureRecognizer();
}
await tester.pumpWidget(
UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<EagerGestureRecognizer>(constructRecognizer),
},
layoutDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(
UiKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.translucent,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<EagerGestureRecognizer>(constructRecognizer),
},
layoutDirection: TextDirection.ltr,
),
);
expect(factoryInvocationCount, 1);
});
testWidgets('AppKitView can take input focus', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
final GlobalKey containerKey = GlobalKey();
await tester.pumpWidget(
Center(
child: Column(
children: <Widget>[
const SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
Focus(
debugLabel: 'container',
child: Container(key: containerKey),
),
],
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final Focus uiKitViewFocusWidget = tester.widget(
find.descendant(
of: find.byType(AppKitView),
matching: find.byType(Focus),
),
);
final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!;
final Element containerElement = tester.element(find.byKey(containerKey));
final FocusNode containerFocusNode = Focus.of(containerElement);
containerFocusNode.requestFocus();
await tester.pump();
expect(containerFocusNode.hasFocus, isTrue);
expect(uiKitViewFocusNode.hasFocus, isFalse);
viewsController.invokeViewFocused(currentViewId + 1);
await tester.pump();
expect(containerFocusNode.hasFocus, isFalse);
expect(uiKitViewFocusNode.hasFocus, isTrue);
});
testWidgets('AppKitView sends TextInput.setPlatformViewClient when focused', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr)
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final Focus uiKitViewFocusWidget = tester.widget(
find.descendant(
of: find.byType(AppKitView),
matching: find.byType(Focus),
),
);
final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!;
late Map<String, dynamic> channelArguments;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall call) {
if (call.method == 'TextInput.setPlatformViewClient') {
channelArguments = call.arguments as Map<String, dynamic>;
}
return null;
});
expect(uiKitViewFocusNode.hasFocus, false);
uiKitViewFocusNode.requestFocus();
await tester.pump();
expect(uiKitViewFocusNode.hasFocus, true);
expect(channelArguments['platformViewId'], currentViewId + 1);
});
testWidgets('FocusNode is disposed on UIView dispose', (WidgetTester tester) async {
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
);
// casting to dynamic is required since the state class is private.
// ignore: avoid_dynamic_calls, invalid_assignment
final FocusNode node = (tester.state(find.byType(AppKitView)) as dynamic).focusNode;
expect(() => ChangeNotifier.debugAssertNotDisposed(node), isNot(throwsAssertionError));
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
),
),
);
expect(() => ChangeNotifier.debugAssertNotDisposed(node), throwsAssertionError);
});
testWidgets('AppKitView has correct semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
expect(currentViewId, greaterThanOrEqualTo(0));
final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Semantics(
container: true,
child: const Align(
alignment: Alignment.bottomRight,
child: SizedBox(
width: 200.0,
height: 100.0,
child: AppKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// 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.descendant(
of: find.byType(AppKitView),
matching: find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_AppKitPlatformView',
),
),
);
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();
});
});
group('Common PlatformView', () {
late FakePlatformViewController controller;