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