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 {
}
}
/// 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 sub views of the FlutterView and are composited by Quartz.
///
/// {@macro flutter.rendering.RenderAndroidView.layout}
///
/// {@macro flutter.rendering.RenderAndroidView.gestures}
/// Common render-layer functionality for iOS and macOS platform views.
///
/// 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 RenderBox {
/// Creates a render object for an iOS UIView.
///
/// The `viewId`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null.
RenderUiKitView({
required UiKitViewController viewController,
/// Provides the basic rendering logic for iOS and macOS platformviews.
/// Subclasses shall override handleEvent in order to execute custom event logic.
/// T represents the class of the view controller for the corresponding widget.
abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController> extends RenderBox {
/// Creates a render object for a platform view.
RenderDarwinPlatformView({
required T viewController,
required this.hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : _viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
}) : _viewController = viewController;
/// The unique identifier of the UIView controlled by this controller.
///
/// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView
/// must have been created by calling [PlatformViewsService.initUiKitView].
UiKitViewController get viewController => _viewController;
UiKitViewController _viewController;
set viewController(UiKitViewController value) {
/// The unique identifier of the platform view controlled by this controller.
T get viewController => _viewController;
T _viewController;
set viewController(T value) {
if (_viewController == value) {
return;
}
......@@ -320,20 +302,6 @@ class RenderUiKitView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate.
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
bool get sizedByParent => true;
......@@ -343,8 +311,6 @@ class RenderUiKitView extends RenderBox {
@override
bool get isRepaintBoundary => true;
_UiKitViewGestureRecognizer? _gestureRecognizer;
PointerEvent? _lastPointerDownEvent;
@override
......@@ -372,15 +338,6 @@ class RenderUiKitView extends RenderBox {
@override
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.
void _handleGlobalPointerEvent(PointerEvent event) {
if (event is! PointerDownEvent) {
......@@ -415,6 +372,69 @@ class RenderUiKitView extends RenderBox {
@override
void detach() {
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();
super.detach();
}
......
......@@ -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].
class UiKitViewController {
UiKitViewController._(
/// View controllers are used to create and interact with the UIView or NSView
/// underlying a platform view.
abstract class DarwinPlatformViewController {
/// Public default for subclasses to override.
DarwinPlatformViewController(
this.id,
TextDirection layoutDirection,
) : _layoutDirection = layoutDirection;
......@@ -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.
///
/// Used by [PlatformViewSurface] to interface with the platform view it embeds.
......
......@@ -195,30 +195,15 @@ class AndroidView extends StatefulWidget {
State<AndroidView> createState() => _AndroidViewState();
}
// 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.
/// Common superclass for iOS and macOS platform views.
///
/// 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 StatefulWidget {
/// Creates a widget that embeds an iOS view.
/// Platform views are used to embed native views in the widget hierarchy, with
/// support for transforms, clips, and opacity similar to any other Flutter widget.
abstract class _DarwinView extends StatefulWidget {
/// Creates a widget that embeds a platform view.
///
/// {@macro flutter.widgets.AndroidView.constructorArgs}
const UiKitView({
const _DarwinView({
super.key,
required this.viewType,
this.onPlatformViewCreated,
......@@ -299,6 +284,41 @@ class UiKitView extends StatefulWidget {
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
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
State<UiKitView> createState() => _UiKitViewState();
......@@ -573,8 +593,8 @@ class _AndroidViewState extends State<AndroidView> {
}
}
class _UiKitViewState extends State<UiKitView> {
UiKitViewController? _controller;
abstract class _DarwinViewState<PlatformViewT extends _DarwinView, ControllerT extends DarwinPlatformViewController, RenderT extends RenderDarwinPlatformView<ControllerT>, ViewT extends _DarwinPlatformView<ControllerT, RenderT>> extends State<PlatformViewT> {
ControllerT? _controller;
TextDirection? _layoutDirection;
bool _initialized = false;
......@@ -586,21 +606,19 @@ class _UiKitViewState extends State<UiKitView> {
@override
Widget build(BuildContext context) {
final UiKitViewController? controller = _controller;
final ControllerT? controller = _controller;
if (controller == null) {
return const SizedBox.expand();
}
return Focus(
focusNode: focusNode,
onFocusChange: (bool isFocused) => _onFocusChange(isFocused, controller),
child: _UiKitPlatformView(
controller: _controller!,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
),
child: childPlatformView()
);
}
ViewT childPlatformView();
void _initializeOnce() {
if (_initialized) {
return;
......@@ -625,7 +643,7 @@ class _UiKitViewState extends State<UiKitView> {
}
@override
void didUpdateWidget(UiKitView oldWidget) {
void didUpdateWidget(PlatformViewT oldWidget) {
super.didUpdateWidget(oldWidget);
final TextDirection newLayoutDirection = _findLayoutDirection();
......@@ -659,15 +677,8 @@ class _UiKitViewState extends State<UiKitView> {
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,
onFocus: () {
focusNode?.requestFocus();
}
final ControllerT controller = await createNewViewController(
id
);
if (!mounted) {
controller.dispose();
......@@ -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) {
// Unlike Android, we do not need to send "clearFocus" channel message
// to the engine, because focusing on another view will automatically
......@@ -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 {
const _AndroidPlatformView({
required this.controller,
......@@ -725,17 +763,29 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
}
}
class _UiKitPlatformView extends LeafRenderObjectWidget {
const _UiKitPlatformView({
abstract class _DarwinPlatformView<TController extends DarwinPlatformViewController, TRender extends RenderDarwinPlatformView<TController>> extends LeafRenderObjectWidget {
const _DarwinPlatformView({
required this.controller,
required this.hitTestBehavior,
required this.gestureRecognizers,
});
final UiKitViewController controller;
final TController controller;
final PlatformViewHitTestBehavior hitTestBehavior;
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
RenderObject createRenderObject(BuildContext context) {
return RenderUiKitView(
......@@ -747,8 +797,7 @@ class _UiKitPlatformView extends LeafRenderObjectWidget {
@override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
renderObject.viewController = controller;
renderObject.hitTestBehavior = hitTestBehavior;
super.updateRenderObject(context, renderObject);
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