Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 156 additions & 3 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include "flutter/fml/platform/darwin/scoped_nsobject.h"
#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterChannels.h"

#import <UIKit/UIGestureRecognizerSubclass.h>

namespace shell {

FlutterPlatformViewsController::FlutterPlatformViewsController(
Expand All @@ -30,6 +32,8 @@
OnCreate(call, result);
} else if ([[call method] isEqualToString:@"dispose"]) {
OnDispose(call, result);
} else if ([[call method] isEqualToString:@"acceptGesture"]) {
OnAcceptGesture(call, result);
} else {
result(FlutterMethodNotImplemented);
}
Expand Down Expand Up @@ -57,9 +61,10 @@
}

// TODO(amirh): decode and pass the creation args.
views_[viewId] = fml::scoped_nsobject<UIView>([[factory createWithFrame:CGRectZero
viewIdentifier:viewId
arguments:nil] retain]);
TouchInterceptingView* view = [[[TouchInterceptingView alloc]
initWithSubView:[factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:nil]
flutterView:flutter_view_] autorelease];
views_[viewId] = fml::scoped_nsobject<TouchInterceptingView>([view retain]);

FlutterView* flutter_view = flutter_view_.get();
[flutter_view addSubview:views_[viewId].get()];
Expand All @@ -83,6 +88,24 @@
result(nil);
}

void FlutterPlatformViewsController::OnAcceptGesture(FlutterMethodCall* call,
FlutterResult& result) {
NSDictionary<NSString*, id>* args = [call arguments];
int64_t viewId = [args[@"id"] longLongValue];

if (views_[viewId] == nil) {
result([FlutterError errorWithCode:@"unknown_view"
message:@"trying to set gesture state for an unknown view"
details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
return;
}

TouchInterceptingView* view = views_[viewId].get();
[view releaseGesture];

result(nil);
}

void FlutterPlatformViewsController::RegisterViewFactory(
NSObject<FlutterPlatformViewFactory>* factory,
NSString* factoryId) {
Expand All @@ -107,3 +130,133 @@
}

} // namespace shell

// This recognizers delays touch events from being dispatched to the responder chain until it failed
// recognizing a gesture.
//
// We only fail this recognizer when asked to do so by the Flutter framework (which does so by
// invoking an acceptGesture method on the platform_views channel). And this is how we allow the
// Flutter framework to delay or prevent the embedded view from getting a touch sequence.
@interface DelayingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Prefix the names of all private iOS classes with the Flutter prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@end

// While the DelayingGestureRecognizer is preventing touches from hitting the responder chain
// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter
// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView
// while during this phase.
//
// If the Flutter framework decides to dispatch events to the embedded view, we fail the
// DelayingGestureRecognizer which sends the events up the responder chain. But since the events
// are handled by the embedded view they are not delivered to the Flutter framework in this phase
// as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events
// directly to the FlutterView.
@interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
- (instancetype)initWithTarget:(id)target flutterView:(UIView*)flutterView;
@end

@implementation TouchInterceptingView {
ForwardingGestureRecognizer* forwardingRecognizer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Prefix the ivar names with _ according to the Objective-C style guide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

DelayingGestureRecognizer* delayingRecognizer;
}
- (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutterView {
self = [super initWithFrame:embeddedView.frame];
if (self) {
self.multipleTouchEnabled = YES;
embeddedView.autoresizingMask =
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);

[self addSubview:embeddedView];

forwardingRecognizer =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have an ivar, explicitly retain a reference to the same instead of the strong reference being maintained by the collection of gesture recognizers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

[[[ForwardingGestureRecognizer alloc] initWithTarget:self
flutterView:flutterView] autorelease];

delayingRecognizer = [[[DelayingGestureRecognizer alloc] initWithTarget:self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of these ivars at all? You don't end up using the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, I think it would be better to hold a strong reference to the recognizer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the delaying recognizer is used when releaseGesture is called (which is triggered by the framework).

I removed the forwardingrecognizer ivar reference.

action:nil] autorelease];

[self addGestureRecognizer:delayingRecognizer];
[self addGestureRecognizer:forwardingRecognizer];
}
return self;
}

- (void)releaseGesture {
delayingRecognizer.state = UIGestureRecognizerStateFailed;
}
@end

@implementation DelayingGestureRecognizer
- (instancetype)initWithTarget:(id)target action:(SEL)action {
self = [super initWithTarget:target action:action];
if (self) {
self.delaysTouchesBegan = YES;
self.delegate = self;
}
return self;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
return otherGestureRecognizer != self;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
return otherGestureRecognizer == self;
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
// The gesture has ended, and the delaying gesture recognizer was not failed, we recognize
// the gesture to prevent the touches from being dispatched to the embedded view.
//
// This doesn't work well with gestures that are recognized by the Flutter framework after
// all pointers are up.
//
// TODO(amirh): explore if we can instead set this to recognized when the next touch sequence
// begins, or we can use a framework signal for restarting the recognizers (e.g when the
// gesture arena is resolved).
self.state = UIGestureRecognizerStateRecognized;
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
self.state = UIGestureRecognizerStateCancelled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an illegal recognizer. You cannot go to the cancelled state without beginning a recognition. Please follow all the rules of either continuous or discrete gesture recognition. This will allow these recognizers to interact correctly with other recognizers on the system.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need continuous recognizers here BTW.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to recognized in this case.

}
@end

@implementation ForwardingGestureRecognizer {
UIView* _flutterView;
}

- (instancetype)initWithTarget:(id)target flutterView:(UIView*)flutterView {
self = [super initWithTarget:target action:nil];
if (self) {
self.delegate = self;
_flutterView = flutterView;
}
return self;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[_flutterView touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
[_flutterView touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
[_flutterView touchesEnded:touches withEvent:event];
self.state = UIGestureRecognizerStateRecognized;
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
[_flutterView touchesCancelled:touches withEvent:event];
self.state = UIGestureRecognizerStateCancelled;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
return YES;
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterChannels.h"
#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h"

// A UIView that is used as the parent for embedded UIViews.
//
// This view has 2 roles:
// 1. Delay or prevent touch events from arriving the embedded view.
// 2. Dispatching all events that are hittested to the embedded view to the FlutterView.
@interface TouchInterceptingView : UIView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Prefix this with Flutter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

- (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutterView;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initWithEmbeddedView:flutterView:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Stop delaying any active touch sequence (and let it arrive the embedded view).
- (void)releaseGesture;
@end

namespace shell {

class FlutterPlatformViewsController : public flow::ExternalViewEmbedder {
Expand All @@ -28,11 +40,12 @@ class FlutterPlatformViewsController : public flow::ExternalViewEmbedder {
fml::scoped_nsobject<FlutterMethodChannel> channel_;
fml::scoped_nsobject<FlutterView> flutter_view_;
std::map<std::string, fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>> factories_;
std::map<int64_t, fml::scoped_nsobject<UIView>> views_;
std::map<int64_t, fml::scoped_nsobject<TouchInterceptingView>> views_;

void OnMethodCall(FlutterMethodCall* call, FlutterResult& result);
void OnCreate(FlutterMethodCall* call, FlutterResult& result);
void OnDispose(FlutterMethodCall* call, FlutterResult& result);
void OnAcceptGesture(FlutterMethodCall* call, FlutterResult& result);

FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewsController);
};
Expand Down