Unverified Commit 203ef6f7 authored by yaakovschectman's avatar yaakovschectman Committed by GitHub

Extract common functionality of iOS platformviews into superclasses (#128716)

Move most functionality of `UiKitView` and its supporting classes into
superclasses named `DarwinPlatformView`, etc., and create trivial or
near-trivial subclasses with the same names as the old classes.

I am currently awaiting approval for a macOS workstation that would
allow me to run the iOS/macOS tests and make sure all existing
functionality is preserved by this refactor. I can ensure that tests
will pass, but doing so may need to wait for a while.

Addresses [Add
AppKitView](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 `///`).
- [ ] 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: 's avatarLoïc Sharma <737941+loic-sharma@users.noreply.github.com>
Co-authored-by: 's avatarMichael Goderbauer <goderbauer@google.com>
Co-authored-by: 's avatarChris Bracken <chris@bracken.jp>
parent beb245c7
...@@ -269,41 +269,23 @@ class RenderAndroidView extends PlatformViewRenderBox { ...@@ -269,41 +269,23 @@ class RenderAndroidView extends PlatformViewRenderBox {
} }
} }
/// A render object for an iOS UIKit UIView. /// Common render-layer functionality for iOS and macOS platform views.
///
/// [RenderUiKitView] is responsible for sizing and displaying an iOS
/// [UIView](https://developer.apple.com/documentation/uikit/uiview).
///
/// UIViews are added as sub views of the FlutterView and are composited by Quartz.
///
/// {@macro flutter.rendering.RenderAndroidView.layout}
///
/// {@macro flutter.rendering.RenderAndroidView.gestures}
/// ///
/// See also: /// Provides the basic rendering logic for iOS and macOS platformviews.
/// /// Subclasses shall override handleEvent in order to execute custom event logic.
/// * [UiKitView] which is a widget that is used to show a UIView. /// T represents the class of the view controller for the corresponding widget.
/// * [PlatformViewsService] which is a service for controlling platform views. abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController> extends RenderBox {
class RenderUiKitView extends RenderBox { /// Creates a render object for a platform view.
/// Creates a render object for an iOS UIView. RenderDarwinPlatformView({
/// required T viewController,
/// The `viewId`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null.
RenderUiKitView({
required UiKitViewController viewController,
required this.hitTestBehavior, required this.hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, }) : _viewController = viewController;
}) : _viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
/// The unique identifier of the UIView controlled by this controller. /// The unique identifier of the platform view controlled by this controller.
/// T get viewController => _viewController;
/// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView T _viewController;
/// must have been created by calling [PlatformViewsService.initUiKitView]. set viewController(T value) {
UiKitViewController get viewController => _viewController;
UiKitViewController _viewController;
set viewController(UiKitViewController value) {
if (_viewController == value) { if (_viewController == value) {
return; return;
} }
...@@ -320,20 +302,6 @@ class RenderUiKitView extends RenderBox { ...@@ -320,20 +302,6 @@ class RenderUiKitView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate. // any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior; PlatformViewHitTestBehavior hitTestBehavior;
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
'There were multiple gesture recognizer factories for the same type, there must only be a single '
'gesture recognizer factory for each gesture recognizer type.',
);
if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) {
return;
}
_gestureRecognizer?.dispose();
_gestureRecognizer = _UiKitViewGestureRecognizer(viewController, gestureRecognizers);
}
@override @override
bool get sizedByParent => true; bool get sizedByParent => true;
...@@ -343,8 +311,6 @@ class RenderUiKitView extends RenderBox { ...@@ -343,8 +311,6 @@ class RenderUiKitView extends RenderBox {
@override @override
bool get isRepaintBoundary => true; bool get isRepaintBoundary => true;
_UiKitViewGestureRecognizer? _gestureRecognizer;
PointerEvent? _lastPointerDownEvent; PointerEvent? _lastPointerDownEvent;
@override @override
...@@ -372,15 +338,6 @@ class RenderUiKitView extends RenderBox { ...@@ -372,15 +338,6 @@ class RenderUiKitView extends RenderBox {
@override @override
bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent; bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent;
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is! PointerDownEvent) {
return;
}
_gestureRecognizer!.addPointer(event);
_lastPointerDownEvent = event.original ?? event;
}
// This is registered as a global PointerRoute while the render object is attached. // This is registered as a global PointerRoute while the render object is attached.
void _handleGlobalPointerEvent(PointerEvent event) { void _handleGlobalPointerEvent(PointerEvent event) {
if (event is! PointerDownEvent) { if (event is! PointerDownEvent) {
...@@ -415,6 +372,69 @@ class RenderUiKitView extends RenderBox { ...@@ -415,6 +372,69 @@ class RenderUiKitView extends RenderBox {
@override @override
void detach() { void detach() {
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent); GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
super.detach();
}
}
/// A render object for an iOS UIKit UIView.
///
/// [RenderUiKitView] is responsible for sizing and displaying an iOS
/// [UIView](https://developer.apple.com/documentation/uikit/uiview).
///
/// UIViews are added as subviews of the FlutterView and are composited by Quartz.
///
/// The viewController is typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView
/// must have been created by calling [PlatformViewsService.initUiKitView].
///
/// {@macro flutter.rendering.RenderAndroidView.layout}
///
/// {@macro flutter.rendering.RenderAndroidView.gestures}
///
/// See also:
///
/// * [UiKitView], which is a widget that is used to show a UIView.
/// * [PlatformViewsService], which is a service for controlling platform views.
class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
/// Creates a render object for an iOS UIView.
///
/// The `viewId`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null.
RenderUiKitView({
required super.viewController,
required super.hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers
}) {
updateGestureRecognizers(gestureRecognizers);
}
// TODO(schectman): Add gesture functionality to macOS platform view when implemented.
// https://github.com/flutter/flutter/issues/128519
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
'There were multiple gesture recognizer factories for the same type, there must only be a single '
'gesture recognizer factory for each gesture recognizer type.',
);
if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) {
return;
}
_gestureRecognizer?.dispose();
_gestureRecognizer = _UiKitViewGestureRecognizer(viewController, gestureRecognizers);
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is! PointerDownEvent) {
return;
}
_gestureRecognizer!.addPointer(event);
_lastPointerDownEvent = event.original ?? event;
}
_UiKitViewGestureRecognizer? _gestureRecognizer;
@override
void detach() {
_gestureRecognizer!.reset(); _gestureRecognizer!.reset();
super.detach(); super.detach();
} }
......
...@@ -1313,11 +1313,13 @@ class _HybridAndroidViewControllerInternals extends _AndroidViewControllerIntern ...@@ -1313,11 +1313,13 @@ class _HybridAndroidViewControllerInternals extends _AndroidViewControllerIntern
} }
} }
/// Controls an iOS UIView. /// Base class for iOS and macOS view controllers.
/// ///
/// Typically created with [PlatformViewsService.initUiKitView]. /// View controllers are used to create and interact with the UIView or NSView
class UiKitViewController { /// underlying a platform view.
UiKitViewController._( abstract class DarwinPlatformViewController {
/// Public default for subclasses to override.
DarwinPlatformViewController(
this.id, this.id,
TextDirection layoutDirection, TextDirection layoutDirection,
) : _layoutDirection = layoutDirection; ) : _layoutDirection = layoutDirection;
...@@ -1382,6 +1384,18 @@ class UiKitViewController { ...@@ -1382,6 +1384,18 @@ class UiKitViewController {
} }
} }
/// Controller for an iOS platform view.
///
/// View controllers create and interact with the underlying UIView.
///
/// Typically created with [PlatformViewsService.initUiKitView].
class UiKitViewController extends DarwinPlatformViewController {
UiKitViewController._(
super.id,
super.layoutDirection,
);
}
/// An interface for controlling a single platform view. /// An interface for controlling a single platform view.
/// ///
/// Used by [PlatformViewSurface] to interface with the platform view it embeds. /// Used by [PlatformViewSurface] to interface with the platform view it embeds.
......
...@@ -195,30 +195,15 @@ class AndroidView extends StatefulWidget { ...@@ -195,30 +195,15 @@ class AndroidView extends StatefulWidget {
State<AndroidView> createState() => _AndroidViewState(); State<AndroidView> createState() => _AndroidViewState();
} }
// TODO(amirh): describe the embedding mechanism. /// Common superclass for iOS and macOS platform views.
// TODO(ychris): remove the documentation for conic path not supported once https://github.com/flutter/flutter/issues/35062 is resolved.
/// Embeds an iOS view in the Widget hierarchy.
///
/// Embedding iOS views is an expensive operation and should be avoided when a Flutter
/// equivalent is possible.
///
/// {@macro flutter.widgets.AndroidView.layout}
///
/// {@macro flutter.widgets.AndroidView.gestures}
///
/// {@macro flutter.widgets.AndroidView.lifetime}
///
/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints
/// nothing while maintaining the same layout constraints.
/// ///
/// Clipping operations on a UiKitView can result slow performance. /// Platform views are used to embed native views in the widget hierarchy, with
/// If a conic path clipping is applied to a UIKitView, /// support for transforms, clips, and opacity similar to any other Flutter widget.
/// a quad path is used to approximate the clip due to limitation of Quartz. abstract class _DarwinView extends StatefulWidget {
class UiKitView extends StatefulWidget { /// Creates a widget that embeds a platform view.
/// Creates a widget that embeds an iOS view.
/// ///
/// {@macro flutter.widgets.AndroidView.constructorArgs} /// {@macro flutter.widgets.AndroidView.constructorArgs}
const UiKitView({ const _DarwinView({
super.key, super.key,
required this.viewType, required this.viewType,
this.onPlatformViewCreated, this.onPlatformViewCreated,
...@@ -299,6 +284,41 @@ class UiKitView extends StatefulWidget { ...@@ -299,6 +284,41 @@ class UiKitView extends StatefulWidget {
// TODO(amirh): get a list of GestureRecognizers here. // TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953 // https://github.com/flutter/flutter/issues/20953
final Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers; final Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers;
}
// TODO(amirh): describe the embedding mechanism.
// TODO(ychris): remove the documentation for conic path not supported once https://github.com/flutter/flutter/issues/35062 is resolved.
/// Embeds an iOS view in the Widget hierarchy.
///
/// Embedding iOS views is an expensive operation and should be avoided when a Flutter
/// equivalent is possible.
///
/// {@macro flutter.widgets.AndroidView.layout}
///
/// {@macro flutter.widgets.AndroidView.gestures}
///
/// {@macro flutter.widgets.AndroidView.lifetime}
///
/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints
/// nothing while maintaining the same layout constraints.
///
/// Clipping operations on a UiKitView can result slow performance.
/// If a conic path clipping is applied to a UIKitView,
/// a quad path is used to approximate the clip due to limitation of Quartz.
class UiKitView extends _DarwinView {
/// Creates a widget that embeds an iOS view.
///
/// {@macro flutter.widgets.AndroidView.constructorArgs}
const UiKitView({
super.key,
required super.viewType,
super.onPlatformViewCreated,
super.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
super.layoutDirection,
super.creationParams,
super.creationParamsCodec,
super.gestureRecognizers,
}) : assert(creationParams == null || creationParamsCodec != null);
@override @override
State<UiKitView> createState() => _UiKitViewState(); State<UiKitView> createState() => _UiKitViewState();
...@@ -573,8 +593,8 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -573,8 +593,8 @@ class _AndroidViewState extends State<AndroidView> {
} }
} }
class _UiKitViewState extends State<UiKitView> { abstract class _DarwinViewState<PlatformViewT extends _DarwinView, ControllerT extends DarwinPlatformViewController, RenderT extends RenderDarwinPlatformView<ControllerT>, ViewT extends _DarwinPlatformView<ControllerT, RenderT>> extends State<PlatformViewT> {
UiKitViewController? _controller; ControllerT? _controller;
TextDirection? _layoutDirection; TextDirection? _layoutDirection;
bool _initialized = false; bool _initialized = false;
...@@ -586,21 +606,19 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -586,21 +606,19 @@ class _UiKitViewState extends State<UiKitView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final UiKitViewController? controller = _controller; final ControllerT? controller = _controller;
if (controller == null) { if (controller == null) {
return const SizedBox.expand(); return const SizedBox.expand();
} }
return Focus( return Focus(
focusNode: focusNode, focusNode: focusNode,
onFocusChange: (bool isFocused) => _onFocusChange(isFocused, controller), onFocusChange: (bool isFocused) => _onFocusChange(isFocused, controller),
child: _UiKitPlatformView( child: childPlatformView()
controller: _controller!,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
),
); );
} }
ViewT childPlatformView();
void _initializeOnce() { void _initializeOnce() {
if (_initialized) { if (_initialized) {
return; return;
...@@ -625,7 +643,7 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -625,7 +643,7 @@ class _UiKitViewState extends State<UiKitView> {
} }
@override @override
void didUpdateWidget(UiKitView oldWidget) { void didUpdateWidget(PlatformViewT oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
final TextDirection newLayoutDirection = _findLayoutDirection(); final TextDirection newLayoutDirection = _findLayoutDirection();
...@@ -659,15 +677,8 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -659,15 +677,8 @@ class _UiKitViewState extends State<UiKitView> {
Future<void> _createNewUiKitView() async { Future<void> _createNewUiKitView() async {
final int id = platformViewsRegistry.getNextPlatformViewId(); final int id = platformViewsRegistry.getNextPlatformViewId();
final UiKitViewController controller = await PlatformViewsService.initUiKitView( final ControllerT controller = await createNewViewController(
id: id, id
viewType: widget.viewType,
layoutDirection: _layoutDirection!,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
onFocus: () {
focusNode?.requestFocus();
}
); );
if (!mounted) { if (!mounted) {
controller.dispose(); controller.dispose();
...@@ -680,7 +691,9 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -680,7 +691,9 @@ class _UiKitViewState extends State<UiKitView> {
}); });
} }
void _onFocusChange(bool isFocused, UiKitViewController controller) { Future<ControllerT> createNewViewController(int id);
void _onFocusChange(bool isFocused, ControllerT controller) {
if (!isFocused) { if (!isFocused) {
// Unlike Android, we do not need to send "clearFocus" channel message // Unlike Android, we do not need to send "clearFocus" channel message
// to the engine, because focusing on another view will automatically // to the engine, because focusing on another view will automatically
...@@ -694,6 +707,31 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -694,6 +707,31 @@ class _UiKitViewState extends State<UiKitView> {
} }
} }
class _UiKitViewState extends _DarwinViewState<UiKitView, UiKitViewController, RenderUiKitView, _UiKitPlatformView> {
@override
Future<UiKitViewController> createNewViewController(int id) async {
return PlatformViewsService.initUiKitView(
id: id,
viewType: widget.viewType,
layoutDirection: _layoutDirection!,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
onFocus: () {
focusNode?.requestFocus();
}
);
}
@override
_UiKitPlatformView childPlatformView() {
return _UiKitPlatformView(
controller: _controller!,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _DarwinViewState._emptyRecognizersSet,
);
}
}
class _AndroidPlatformView extends LeafRenderObjectWidget { class _AndroidPlatformView extends LeafRenderObjectWidget {
const _AndroidPlatformView({ const _AndroidPlatformView({
required this.controller, required this.controller,
...@@ -725,17 +763,29 @@ class _AndroidPlatformView extends LeafRenderObjectWidget { ...@@ -725,17 +763,29 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
} }
} }
class _UiKitPlatformView extends LeafRenderObjectWidget { abstract class _DarwinPlatformView<TController extends DarwinPlatformViewController, TRender extends RenderDarwinPlatformView<TController>> extends LeafRenderObjectWidget {
const _UiKitPlatformView({ const _DarwinPlatformView({
required this.controller, required this.controller,
required this.hitTestBehavior, required this.hitTestBehavior,
required this.gestureRecognizers, required this.gestureRecognizers,
}); });
final UiKitViewController controller; final TController controller;
final PlatformViewHitTestBehavior hitTestBehavior; final PlatformViewHitTestBehavior hitTestBehavior;
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers; final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override
@mustCallSuper
void updateRenderObject(BuildContext context, TRender renderObject) {
renderObject
..viewController = controller
..hitTestBehavior = hitTestBehavior;
}
}
class _UiKitPlatformView extends _DarwinPlatformView<UiKitViewController, RenderUiKitView> {
const _UiKitPlatformView({required super.controller, required super.hitTestBehavior, required super.gestureRecognizers});
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return RenderUiKitView( return RenderUiKitView(
...@@ -747,8 +797,7 @@ class _UiKitPlatformView extends LeafRenderObjectWidget { ...@@ -747,8 +797,7 @@ class _UiKitPlatformView extends LeafRenderObjectWidget {
@override @override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) { void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
renderObject.viewController = controller; super.updateRenderObject(context, renderObject);
renderObject.hitTestBehavior = hitTestBehavior;
renderObject.updateGestureRecognizers(gestureRecognizers); renderObject.updateGestureRecognizers(gestureRecognizers);
} }
} }
......
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