Flutter iOS Embedder
FlutterPlatformViews.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #import <WebKit/WebKit.h>
8 
9 #include "flutter/display_list/effects/dl_image_filter.h"
10 #include "flutter/fml/platform/darwin/cf_utils.h"
12 
14 
15 namespace {
16 static CGRect GetCGRectFromDlRect(const flutter::DlRect& clipDlRect) {
17  return CGRectMake(clipDlRect.GetX(), //
18  clipDlRect.GetY(), //
19  clipDlRect.GetWidth(), //
20  clipDlRect.GetHeight());
21 }
22 
23 CATransform3D GetCATransform3DFromDlMatrix(const flutter::DlMatrix& matrix) {
24  CATransform3D transform = CATransform3DIdentity;
25  transform.m11 = matrix.m[0];
26  transform.m12 = matrix.m[1];
27  transform.m13 = matrix.m[2];
28  transform.m14 = matrix.m[3];
29 
30  transform.m21 = matrix.m[4];
31  transform.m22 = matrix.m[5];
32  transform.m23 = matrix.m[6];
33  transform.m24 = matrix.m[7];
34 
35  transform.m31 = matrix.m[8];
36  transform.m32 = matrix.m[9];
37  transform.m33 = matrix.m[10];
38  transform.m34 = matrix.m[11];
39 
40  transform.m41 = matrix.m[12];
41  transform.m42 = matrix.m[13];
42  transform.m43 = matrix.m[14];
43  transform.m44 = matrix.m[15];
44  return transform;
45 }
46 
47 class CGPathReceiver final : public flutter::DlPathReceiver {
48  public:
49  void MoveTo(const flutter::DlPoint& p2, bool will_be_closed) override { //
50  CGPathMoveToPoint(path_ref_, nil, p2.x, p2.y);
51  }
52  void LineTo(const flutter::DlPoint& p2) override {
53  CGPathAddLineToPoint(path_ref_, nil, p2.x, p2.y);
54  }
55  void QuadTo(const flutter::DlPoint& cp, const flutter::DlPoint& p2) override {
56  CGPathAddQuadCurveToPoint(path_ref_, nil, cp.x, cp.y, p2.x, p2.y);
57  }
58  // bool conic_to(...) { CGPath has no equivalent to the conic curve type }
59  void CubicTo(const flutter::DlPoint& cp1,
60  const flutter::DlPoint& cp2,
61  const flutter::DlPoint& p2) override {
62  CGPathAddCurveToPoint(path_ref_, nil, //
63  cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y);
64  }
65  void Close() override { CGPathCloseSubpath(path_ref_); }
66 
67  CGMutablePathRef TakePath() const { return path_ref_; }
68 
69  private:
70  CGMutablePathRef path_ref_ = CGPathCreateMutable();
71 };
72 } // namespace
73 
74 @interface PlatformViewFilter ()
75 
76 // `YES` if the backdropFilterView has been configured at least once.
77 @property(nonatomic) BOOL backdropFilterViewConfigured;
78 @property(nonatomic) UIVisualEffectView* backdropFilterView;
79 
80 // Updates the `visualEffectView` with the current filter parameters.
81 // Also sets `self.backdropFilterView` to the updated visualEffectView.
82 - (void)updateVisualEffectView:(UIVisualEffectView*)visualEffectView;
83 
84 @end
85 
86 @implementation PlatformViewFilter
87 
88 static NSObject* _gaussianBlurFilter = nil;
89 // The index of "_UIVisualEffectBackdropView" in UIVisualEffectView's subViews.
90 static NSInteger _indexOfBackdropView = -1;
91 // The index of "_UIVisualEffectSubview" in UIVisualEffectView's subViews.
92 static NSInteger _indexOfVisualEffectSubview = -1;
93 static BOOL _preparedOnce = NO;
94 
95 - (instancetype)initWithFrame:(CGRect)frame
96  blurRadius:(CGFloat)blurRadius
97  visualEffectView:(UIVisualEffectView*)visualEffectView {
98  if (self = [super init]) {
99  _frame = frame;
100  _blurRadius = blurRadius;
101  [PlatformViewFilter prepareOnce:visualEffectView];
102  if (![PlatformViewFilter isUIVisualEffectViewImplementationValid]) {
103  FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to "
104  "access the gaussianBlur CAFilter.";
105  return nil;
106  }
107  _backdropFilterView = visualEffectView;
108  _backdropFilterViewConfigured = NO;
109  }
110  return self;
111 }
112 
113 + (void)resetPreparation {
114  _preparedOnce = NO;
115  _gaussianBlurFilter = nil;
118 }
119 
120 + (void)prepareOnce:(UIVisualEffectView*)visualEffectView {
121  if (_preparedOnce) {
122  return;
123  }
124  for (NSUInteger i = 0; i < visualEffectView.subviews.count; i++) {
125  UIView* view = visualEffectView.subviews[i];
126  if ([NSStringFromClass([view class]) hasSuffix:@"BackdropView"]) {
128  for (NSObject* filter in view.layer.filters) {
129  if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"] &&
130  [[filter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) {
131  _gaussianBlurFilter = filter;
132  break;
133  }
134  }
135  } else if ([NSStringFromClass([view class]) hasSuffix:@"VisualEffectSubview"]) {
137  }
138  }
139  _preparedOnce = YES;
140 }
141 
142 + (BOOL)isUIVisualEffectViewImplementationValid {
144 }
145 
146 - (UIVisualEffectView*)backdropFilterView {
147  FML_DCHECK(_backdropFilterView);
148  if (!self.backdropFilterViewConfigured) {
149  [self updateVisualEffectView:_backdropFilterView];
150  self.backdropFilterViewConfigured = YES;
151  }
152  return _backdropFilterView;
153 }
154 
155 - (void)updateVisualEffectView:(UIVisualEffectView*)visualEffectView {
156  NSObject* gaussianBlurFilter = [_gaussianBlurFilter copy];
157  FML_DCHECK(gaussianBlurFilter);
158  UIView* backdropView = visualEffectView.subviews[_indexOfBackdropView];
159  [gaussianBlurFilter setValue:@(_blurRadius) forKey:@"inputRadius"];
160  backdropView.layer.filters = @[ gaussianBlurFilter ];
161 
162  UIView* visualEffectSubview = visualEffectView.subviews[_indexOfVisualEffectSubview];
163  visualEffectSubview.layer.backgroundColor = UIColor.clearColor.CGColor;
164  visualEffectView.frame = _frame;
165 
166  self.backdropFilterView = visualEffectView;
167 }
168 
169 @end
170 
171 @interface ChildClippingView ()
172 
173 @property(nonatomic, copy) NSArray<PlatformViewFilter*>* filters;
174 @property(nonatomic) NSMutableArray<UIVisualEffectView*>* backdropFilterSubviews;
175 
176 @end
177 
178 @implementation ChildClippingView
179 
180 // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to
181 // be hit tested and consumed by this view if they are inside the embedded platform view which could
182 // be smaller the embedded platform view is rotated.
183 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
184  for (UIView* view in self.subviews) {
185  if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
186  return YES;
187  }
188  }
189  return NO;
190 }
191 
192 - (void)applyBlurBackdropFilters:(NSArray<PlatformViewFilter*>*)filters {
193  FML_DCHECK(self.filters.count == self.backdropFilterSubviews.count);
194  if (self.filters.count == 0 && filters.count == 0) {
195  return;
196  }
197  self.filters = filters;
198  NSUInteger index = 0;
199  for (index = 0; index < self.filters.count; index++) {
200  UIVisualEffectView* backdropFilterView;
201  PlatformViewFilter* filter = self.filters[index];
202  if (self.backdropFilterSubviews.count <= index) {
203  backdropFilterView = filter.backdropFilterView;
204  [self addSubview:backdropFilterView];
205  [self.backdropFilterSubviews addObject:backdropFilterView];
206  } else {
207  [filter updateVisualEffectView:self.backdropFilterSubviews[index]];
208  }
209  }
210  for (NSUInteger i = self.backdropFilterSubviews.count; i > index; i--) {
211  [self.backdropFilterSubviews[i - 1] removeFromSuperview];
212  [self.backdropFilterSubviews removeLastObject];
213  }
214 }
215 
216 - (NSMutableArray*)backdropFilterSubviews {
217  if (!_backdropFilterSubviews) {
218  _backdropFilterSubviews = [[NSMutableArray alloc] init];
219  }
220  return _backdropFilterSubviews;
221 }
222 
223 @end
224 
226 
227 // A `CATransform3D` matrix represnts a scale transform that revese UIScreen.scale.
228 //
229 // The transform matrix passed in clipRect/clipRRect/clipPath methods are in device coordinate
230 // space. The transfrom matrix concats `reverseScreenScale` to create a transform matrix in the iOS
231 // logical coordinates (points).
232 //
233 // See https://developer.apple.com/documentation/uikit/uiscreen/1617836-scale?language=objc for
234 // information about screen scale.
235 @property(nonatomic) CATransform3D reverseScreenScale;
236 
237 - (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
238 
239 @end
240 
241 @implementation FlutterClippingMaskView {
242  std::vector<fml::CFRef<CGPathRef>> paths_;
244  CGRect rectSoFar_;
245 }
246 
247 - (instancetype)initWithFrame:(CGRect)frame {
248  return [self initWithFrame:frame screenScale:[UIScreen mainScreen].scale];
249 }
250 
251 - (instancetype)initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale {
252  if (self = [super initWithFrame:frame]) {
253  self.backgroundColor = UIColor.clearColor;
254  _reverseScreenScale = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);
255  rectSoFar_ = self.bounds;
257  }
258  return self;
259 }
260 
261 + (Class)layerClass {
262  return [CAShapeLayer class];
263 }
264 
265 - (CAShapeLayer*)shapeLayer {
266  return (CAShapeLayer*)self.layer;
267 }
268 
269 - (void)reset {
270  paths_.clear();
271  rectSoFar_ = self.bounds;
273  [self shapeLayer].path = nil;
274  [self setNeedsDisplay];
275 }
276 
277 // In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added
278 // this view as a subview of the ChildClippingView.
279 // This results this view blocking touch events on the ChildClippingView.
280 // So we should always ignore any touch events sent to this view.
281 // See https://github.com/flutter/flutter/issues/66044
282 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
283  return NO;
284 }
285 
286 - (void)drawRect:(CGRect)rect {
287  // It's hard to compute intersection of arbitrary non-rect paths.
288  // So we fallback to software rendering.
289  if (containsNonRectPath_ && paths_.size() > 1) {
290  CGContextRef context = UIGraphicsGetCurrentContext();
291  CGContextSaveGState(context);
292 
293  // For mask view, only the alpha channel is used.
294  CGContextSetAlpha(context, 1);
295 
296  for (size_t i = 0; i < paths_.size(); i++) {
297  CGContextAddPath(context, paths_.at(i));
298  CGContextClip(context);
299  }
300  CGContextFillRect(context, rect);
301  CGContextRestoreGState(context);
302  } else {
303  // Either a single path, or multiple rect paths.
304  // Use hardware rendering with CAShapeLayer.
305  [super drawRect:rect];
306  if (![self shapeLayer].path) {
307  if (paths_.size() == 1) {
308  // A single path, either rect or non-rect.
309  [self shapeLayer].path = paths_.at(0);
310  } else {
311  // Multiple paths, all paths must be rects.
312  CGPathRef pathSoFar = CGPathCreateWithRect(rectSoFar_, nil);
313  [self shapeLayer].path = pathSoFar;
314  CGPathRelease(pathSoFar);
315  }
316  }
317  }
318 }
319 
320 - (void)clipRect:(const flutter::DlRect&)clipDlRect matrix:(const flutter::DlMatrix&)matrix {
321  CGRect clipRect = GetCGRectFromDlRect(clipDlRect);
322  CGPathRef path = CGPathCreateWithRect(clipRect, nil);
323  // The `matrix` is based on the physical pixels, convert it to UIKit points.
324  CATransform3D matrixInPoints =
325  CATransform3DConcat(GetCATransform3DFromDlMatrix(matrix), _reverseScreenScale);
326  paths_.push_back([self getTransformedPath:path matrix:matrixInPoints]);
327  CGAffineTransform affine = [self affineWithMatrix:matrixInPoints];
328  // Make sure the rect is not rotated (only translated or scaled).
329  if (affine.b == 0 && affine.c == 0) {
330  rectSoFar_ = CGRectIntersection(rectSoFar_, CGRectApplyAffineTransform(clipRect, affine));
331  } else {
332  containsNonRectPath_ = YES;
333  }
334 }
335 
336 - (void)clipRRect:(const flutter::DlRoundRect&)clipDlRRect matrix:(const flutter::DlMatrix&)matrix {
337  if (clipDlRRect.IsEmpty()) {
338  return;
339  } else if (clipDlRRect.IsRect()) {
340  [self clipRect:clipDlRRect.GetBounds() matrix:matrix];
341  return;
342  } else {
343  CGPathRef pathRef = nullptr;
344  containsNonRectPath_ = YES;
345 
346  if (clipDlRRect.GetRadii().AreAllCornersSame()) {
347  CGRect clipRect = GetCGRectFromDlRect(clipDlRRect.GetBounds());
348  auto radii = clipDlRRect.GetRadii();
349  pathRef =
350  CGPathCreateWithRoundedRect(clipRect, radii.top_left.width, radii.top_left.height, nil);
351  } else {
352  CGMutablePathRef mutablePathRef = CGPathCreateMutable();
353  // Complex types, we manually add each corner.
354  flutter::DlRect clipDlRect = clipDlRRect.GetBounds();
355  auto left = clipDlRect.GetLeft();
356  auto top = clipDlRect.GetTop();
357  auto right = clipDlRect.GetRight();
358  auto bottom = clipDlRect.GetBottom();
359  flutter::DlRoundingRadii radii = clipDlRRect.GetRadii();
360  auto& top_left = radii.top_left;
361  auto& top_right = radii.top_right;
362  auto& bottom_left = radii.bottom_left;
363  auto& bottom_right = radii.bottom_right;
364 
365  // Start drawing RRect
366  // These calculations are off, the AddCurve methods add a Bezier curve
367  // which, for round rects should be a "magic distance" from the end
368  // point of the horizontal/vertical section to the corner.
369  // Move point to the top left corner adding the top left radii's x.
370  CGPathMoveToPoint(mutablePathRef, nil, //
371  left + top_left.width, top);
372  // Move point horizontally right to the top right corner and add the top right curve.
373  CGPathAddLineToPoint(mutablePathRef, nil, //
374  right - top_right.width, top);
375  CGPathAddCurveToPoint(mutablePathRef, nil, //
376  right, top, //
377  right, top + top_right.height, //
378  right, top + top_right.height);
379  // Move point vertically down to the bottom right corner and add the bottom right curve.
380  CGPathAddLineToPoint(mutablePathRef, nil, //
381  right, bottom - bottom_right.height);
382  CGPathAddCurveToPoint(mutablePathRef, nil, //
383  right, bottom, //
384  right - bottom_right.width, bottom, //
385  right - bottom_right.width, bottom);
386  // Move point horizontally left to the bottom left corner and add the bottom left curve.
387  CGPathAddLineToPoint(mutablePathRef, nil, //
388  left + bottom_left.width, bottom);
389  CGPathAddCurveToPoint(mutablePathRef, nil, //
390  left, bottom, //
391  left, bottom - bottom_left.height, //
392  left, bottom - bottom_left.height);
393  // Move point vertically up to the top left corner and add the top left curve.
394  CGPathAddLineToPoint(mutablePathRef, nil, //
395  left, top + top_left.height);
396  CGPathAddCurveToPoint(mutablePathRef, nil, //
397  left, top, //
398  left + top_left.width, top, //
399  left + top_left.width, top);
400  CGPathCloseSubpath(mutablePathRef);
401  pathRef = mutablePathRef;
402  }
403  // The `matrix` is based on the physical pixels, convert it to UIKit points.
404  CATransform3D matrixInPoints =
405  CATransform3DConcat(GetCATransform3DFromDlMatrix(matrix), _reverseScreenScale);
406  // TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated
407  // that the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard
408  // edge clipping on iOS.
409  paths_.push_back([self getTransformedPath:pathRef matrix:matrixInPoints]);
410  }
411 }
412 
413 - (void)clipPath:(const flutter::DlPath&)dlPath matrix:(const flutter::DlMatrix&)matrix {
414  containsNonRectPath_ = YES;
415 
416  CGPathReceiver receiver;
417 
418  // TODO(flar): https://github.com/flutter/flutter/issues/164826
419  // CGPaths do not have an inherit fill type, we would need to remember
420  // the fill type and employ it when we use the path.
421  dlPath.Dispatch(receiver);
422 
423  // The `matrix` is based on the physical pixels, convert it to UIKit points.
424  CATransform3D matrixInPoints =
425  CATransform3DConcat(GetCATransform3DFromDlMatrix(matrix), _reverseScreenScale);
426  paths_.push_back([self getTransformedPath:receiver.TakePath() matrix:matrixInPoints]);
427 }
428 
429 - (CGAffineTransform)affineWithMatrix:(CATransform3D)matrix {
430  return CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41,
431  matrix.m42);
432 }
433 
434 - (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix {
435  CGAffineTransform affine = [self affineWithMatrix:matrix];
436  CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine);
437 
438  CGPathRelease(path);
439  return fml::CFRef<CGPathRef>(transformedPath);
440 }
441 
442 @end
443 
445 
446 // The maximum number of `FlutterClippingMaskView` the pool can contain.
447 // This prevents the pool to grow infinately and limits the maximum memory a pool can use.
448 @property(nonatomic) NSUInteger capacity;
449 
450 // The pool contains the views that are available to use.
451 // The number of items in the pool must not excceds `capacity`.
452 @property(nonatomic) NSMutableSet<FlutterClippingMaskView*>* pool;
453 
454 @end
455 
456 @implementation FlutterClippingMaskViewPool : NSObject
457 
458 - (instancetype)initWithCapacity:(NSInteger)capacity {
459  if (self = [super init]) {
460  // Most of cases, there are only one PlatformView in the scene.
461  // Thus init with the capacity of 1.
462  _pool = [[NSMutableSet alloc] initWithCapacity:1];
463  _capacity = capacity;
464  }
465  return self;
466 }
467 
468 - (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame {
469  FML_DCHECK(self.pool.count <= self.capacity);
470  if (self.pool.count == 0) {
471  // The pool is empty, alloc a new one.
472  return [[FlutterClippingMaskView alloc] initWithFrame:frame
473  screenScale:UIScreen.mainScreen.scale];
474  }
475  FlutterClippingMaskView* maskView = [self.pool anyObject];
476  maskView.frame = frame;
477  [maskView reset];
478  [self.pool removeObject:maskView];
479  return maskView;
480 }
481 
482 - (void)insertViewToPoolIfNeeded:(FlutterClippingMaskView*)maskView {
483  FML_DCHECK(![self.pool containsObject:maskView]);
484  FML_DCHECK(self.pool.count <= self.capacity);
485  if (self.pool.count == self.capacity) {
486  return;
487  }
488  [self.pool addObject:maskView];
489 }
490 
491 @end
492 
493 @implementation UIView (FirstResponder)
495  if (self.isFirstResponder) {
496  return YES;
497  }
498  for (UIView* subview in self.subviews) {
499  if (subview.flt_hasFirstResponderInViewHierarchySubtree) {
500  return YES;
501  }
502  }
503  return NO;
504 }
505 @end
506 
508 @property(nonatomic, weak, readonly) UIView* embeddedView;
509 @property(nonatomic, readonly) FlutterDelayingGestureRecognizer* delayingRecognizer;
510 @property(nonatomic, readonly) FlutterPlatformViewGestureRecognizersBlockingPolicy blockingPolicy;
511 @end
512 
514 - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
515  platformViewsController:(FlutterPlatformViewsController*)platformViewsController
516  gestureRecognizersBlockingPolicy:
518  self = [super initWithFrame:embeddedView.frame];
519  if (self) {
520  self.multipleTouchEnabled = YES;
521  _embeddedView = embeddedView;
522  embeddedView.autoresizingMask =
523  (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
524 
525  [self addSubview:embeddedView];
526 
527  ForwardingGestureRecognizer* forwardingRecognizer =
528  [[ForwardingGestureRecognizer alloc] initWithTarget:self
529  platformViewsController:platformViewsController];
530 
531  _delayingRecognizer =
532  [[FlutterDelayingGestureRecognizer alloc] initWithTarget:self
533  action:nil
534  forwardingRecognizer:forwardingRecognizer];
535  _blockingPolicy = blockingPolicy;
536 
537  [self addGestureRecognizer:_delayingRecognizer];
538  [self addGestureRecognizer:forwardingRecognizer];
539  }
540  return self;
541 }
542 
543 - (void)forceResetForwardingGestureRecognizerState {
544  // When iPad pencil is involved in a finger touch gesture, the gesture is not reset to "possible"
545  // state and is stuck on "failed" state, which causes subsequent touches to be blocked. As a
546  // workaround, we force reset the state by recreating the forwarding gesture recognizer. See:
547  // https://github.com/flutter/flutter/issues/136244
548  ForwardingGestureRecognizer* oldForwardingRecognizer =
549  (ForwardingGestureRecognizer*)self.delayingRecognizer.forwardingRecognizer;
550  ForwardingGestureRecognizer* newForwardingRecognizer =
551  [oldForwardingRecognizer recreateRecognizerWithTarget:self];
552  self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer;
553  [self removeGestureRecognizer:oldForwardingRecognizer];
554  [self addGestureRecognizer:newForwardingRecognizer];
555 }
556 
557 - (void)releaseGesture {
558  self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
559 }
560 
561 - (BOOL)containsWebView:(UIView*)view remainingSubviewDepth:(int)remainingSubviewDepth {
562  if (remainingSubviewDepth < 0) {
563  return NO;
564  }
565  if ([view isKindOfClass:[WKWebView class]]) {
566  return YES;
567  }
568  for (UIView* subview in view.subviews) {
569  if ([self containsWebView:subview remainingSubviewDepth:remainingSubviewDepth - 1]) {
570  return YES;
571  }
572  }
573  return NO;
574 }
575 
576 - (void)blockGesture {
577  switch (_blockingPolicy) {
579  // We block all other gesture recognizers immediately in this policy.
580  self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
581 
582  // On iOS 18.2, WKWebView's internal recognizer likely caches the old state of its blocking
583  // recognizers (i.e. delaying recognizer), resulting in non-tappable links. See
584  // https://github.com/flutter/flutter/issues/158961. Removing and adding back the delaying
585  // recognizer solves the problem, possibly because UIKit notifies all the recognizers related
586  // to (blocking or blocked by) this recognizer. It is not possible to inject this workaround
587  // from the web view plugin level. Right now we only observe this issue for
588  // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar
589  // issue arises for the other policy.
590  if (@available(iOS 18.2, *)) {
591  // This workaround is designed for WKWebView only. The 1P web view plugin provides a
592  // WKWebView itself as the platform view. However, some 3P plugins provide wrappers of
593  // WKWebView instead. So we perform DFS to search the view hierarchy (with a depth limit).
594  // Passing a limit of 0 means only searching for platform view itself; Pass 1 to include its
595  // children as well, and so on. We should be conservative and start with a small number. The
596  // AdMob banner has a WKWebView at depth 7.
597  if ([self containsWebView:self.embeddedView remainingSubviewDepth:1]) {
598  [self removeGestureRecognizer:self.delayingRecognizer];
599  [self addGestureRecognizer:self.delayingRecognizer];
600  }
601  }
602 
603  break;
605  if (self.delayingRecognizer.touchedEndedWithoutBlocking) {
606  // If touchesEnded of the `DelayingGesureRecognizer` has been already invoked,
607  // we want to set the state of the `DelayingGesureRecognizer` to
608  // `UIGestureRecognizerStateEnded` as soon as possible.
609  self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
610  } else {
611  // If touchesEnded of the `DelayingGesureRecognizer` has not been invoked,
612  // We will set a flag to notify the `DelayingGesureRecognizer` to set the state to
613  // `UIGestureRecognizerStateEnded` when touchesEnded is called.
614  self.delayingRecognizer.shouldEndInNextTouchesEnded = YES;
615  }
616  break;
617  default:
618  break;
619  }
620 }
621 
622 // We want the intercepting view to consume the touches and not pass the touches up to the parent
623 // view. Make the touch event method not call super will not pass the touches up to the parent view.
624 // Hence we overide the touch event methods and do nothing.
625 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
626 }
627 
628 - (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
629 }
630 
631 - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
632 }
633 
634 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
635 }
636 
638  return self.flutterAccessibilityContainer;
639 }
640 
641 @end
642 
644 
645 - (instancetype)initWithTarget:(id)target
646  action:(SEL)action
647  forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer {
648  self = [super initWithTarget:target action:action];
649  if (self) {
650  self.delaysTouchesBegan = YES;
651  self.delaysTouchesEnded = YES;
652  self.delegate = self;
653  _shouldEndInNextTouchesEnded = NO;
654  _touchedEndedWithoutBlocking = NO;
655  _forwardingRecognizer = forwardingRecognizer;
656  }
657  return self;
658 }
659 
660 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
661  shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
662  // The forwarding gesture recognizer should always get all touch events, so it should not be
663  // required to fail by any other gesture recognizer.
664  return otherGestureRecognizer != _forwardingRecognizer && otherGestureRecognizer != self;
665 }
666 
667 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
668  shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
669  return otherGestureRecognizer == self;
670 }
671 
672 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
673  self.touchedEndedWithoutBlocking = NO;
674  [super touchesBegan:touches withEvent:event];
675 }
676 
677 - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
678  if (self.shouldEndInNextTouchesEnded) {
679  self.state = UIGestureRecognizerStateEnded;
680  self.shouldEndInNextTouchesEnded = NO;
681  } else {
682  self.touchedEndedWithoutBlocking = YES;
683  }
684  [super touchesEnded:touches withEvent:event];
685 }
686 
687 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
688  self.state = UIGestureRecognizerStateFailed;
689 }
690 @end
691 
693  // Weak reference to PlatformViewsController. The PlatformViewsController has
694  // a reference to the FlutterViewController, where we can dispatch pointer events to.
695  //
696  // The lifecycle of PlatformViewsController is bind to FlutterEngine, which should always
697  // outlives the FlutterViewController. And ForwardingGestureRecognizer is owned by a subview of
698  // FlutterView, so the ForwardingGestureRecognizer never out lives FlutterViewController.
699  // Therefore, `_platformViewsController` should never be nullptr.
700  __weak FlutterPlatformViewsController* _platformViewsController;
701  // Counting the pointers that has started in one touch sequence.
703  // We can't dispatch events to the framework without this back pointer.
704  // This gesture recognizer retains the `FlutterViewController` until the
705  // end of a gesture sequence, that is all the touches in touchesBegan are concluded
706  // with |touchesCancelled| or |touchesEnded|.
707  UIViewController<FlutterViewResponder>* _flutterViewController;
708 }
709 
710 - (instancetype)initWithTarget:(id)target
711  platformViewsController:(FlutterPlatformViewsController*)platformViewsController {
712  self = [super initWithTarget:target action:nil];
713  if (self) {
714  self.delegate = self;
715  FML_DCHECK(platformViewsController);
716  _platformViewsController = platformViewsController;
718  }
719  return self;
720 }
721 
722 - (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target {
723  return [[ForwardingGestureRecognizer alloc] initWithTarget:target
724  platformViewsController:_platformViewsController];
725 }
726 
727 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
728  FML_DCHECK(_currentTouchPointersCount >= 0);
729  if (_currentTouchPointersCount == 0) {
730  // At the start of each gesture sequence, we reset the `_flutterViewController`,
731  // so that all the touch events in the same sequence are forwarded to the same
732  // `_flutterViewController`.
733  _flutterViewController = _platformViewsController.flutterViewController;
734  }
735  [_flutterViewController touchesBegan:touches withEvent:event];
736  _currentTouchPointersCount += touches.count;
737 }
738 
739 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
740  [_flutterViewController touchesMoved:touches withEvent:event];
741 }
742 
743 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
744  [_flutterViewController touchesEnded:touches withEvent:event];
745  _currentTouchPointersCount -= touches.count;
746  // Touches in one touch sequence are sent to the touchesEnded method separately if different
747  // fingers stop touching the screen at different time. So one touchesEnded method triggering does
748  // not necessarially mean the touch sequence has ended. We Only set the state to
749  // UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended.
750  if (_currentTouchPointersCount == 0) {
751  self.state = UIGestureRecognizerStateFailed;
753  [self forceResetStateIfNeeded];
754  }
755 }
756 
757 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
758  // In the event of platform view is removed, iOS generates a "stationary" change type instead of
759  // "cancelled" change type.
760  // Flutter needs all the cancelled touches to be "cancelled" change types in order to correctly
761  // handle gesture sequence.
762  // We always override the change type to "cancelled".
763  [_flutterViewController forceTouchesCancelled:touches];
764  _currentTouchPointersCount -= touches.count;
765  if (_currentTouchPointersCount == 0) {
766  self.state = UIGestureRecognizerStateFailed;
768  [self forceResetStateIfNeeded];
769  }
770 }
771 
772 - (void)forceResetStateIfNeeded {
773  __weak ForwardingGestureRecognizer* weakSelf = self;
774  dispatch_async(dispatch_get_main_queue(), ^{
775  ForwardingGestureRecognizer* strongSelf = weakSelf;
776  if (!strongSelf) {
777  return;
778  }
779  if (strongSelf.state != UIGestureRecognizerStatePossible) {
780  [(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
781  }
782  });
783 }
784 
785 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
786  shouldRecognizeSimultaneouslyWithGestureRecognizer:
787  (UIGestureRecognizer*)otherGestureRecognizer {
788  return YES;
789 }
790 @end
BOOL containsNonRectPath_
static NSInteger _indexOfVisualEffectSubview
static NSInteger _indexOfBackdropView
static BOOL _preparedOnce
UIViewController< FlutterViewResponder > * _flutterViewController
NSInteger _currentTouchPointersCount
CGRect rectSoFar_
static NSObject * _gaussianBlurFilter
static CATransform3D GetCATransform3DFromDlMatrix(const DlMatrix &matrix)
static CGRect GetCGRectFromDlRect(const DlRect &clipDlRect)
FlutterPlatformViewGestureRecognizersBlockingPolicy
@ FlutterPlatformViewGestureRecognizersBlockingPolicyEager
@ FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded
instancetype initWithFrame
NSMutableArray * backdropFilterSubviews()
void CubicTo(const flutter::DlPoint &cp1, const flutter::DlPoint &cp2, const flutter::DlPoint &p2) override
void MoveTo(const flutter::DlPoint &p2, bool will_be_closed) override
void LineTo(const flutter::DlPoint &p2) override
void QuadTo(const flutter::DlPoint &cp, const flutter::DlPoint &p2) override
UIVisualEffectView * backdropFilterView