| 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
5 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
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"
11 #import "flutter/shell/platform/darwin/ios/ios_surface.h"
12
13 FLUTTER_ASSERT_ARC
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
78 + (void)resetPreparation {
79 _preparedOnce = NO;
80 _gaussianBlurFilter = nil;
81 _indexOfBackdropView = -1;
82 _indexOfVisualEffectSubview = -1;
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"]) {
92 _indexOfBackdropView = i;
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"]) {
101 _indexOfVisualEffectSubview = i;
102 }
103 }
104 _preparedOnce = YES;
105 }
106
107 + (BOOL)isUIVisualEffectViewImplementationValid {
108 return _indexOfBackdropView > -1 && _indexOfVisualEffectSubview > -1 && _gaussianBlurFilter;
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
190 @interface FlutterClippingMaskView ()
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_;
208 BOOL containsNonRectPath_;
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;
221 containsNonRectPath_ = NO;
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;
237 containsNonRectPath_ = NO;
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
452 @interface FlutterClippingMaskViewPool ()
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
515 @interface FlutterTouchInterceptingView ()
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:(FlutterPlatformViewsController*)platformViewsController
524 gestureRecognizersBlockingPolicy:
525 (FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy {
526 self = [super initWithFrame:embeddedView.frame];
527 if (self) {
528 self.multipleTouchEnabled = YES;
529 _embeddedView = embeddedView;
530 embeddedView.autoresizingMask =
531 (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
532
533 [self addSubview:embeddedView];
534
535 ForwardingGestureRecognizer* forwardingRecognizer =
536 [[ForwardingGestureRecognizer alloc] initWithTarget:self
537 platformViewsController:platformViewsController];
538
539 _delayingRecognizer =
540 [[FlutterDelayingGestureRecognizer alloc] initWithTarget:self
541 action:nil
542 forwardingRecognizer:forwardingRecognizer];
543 _blockingPolicy = blockingPolicy;
544
545 [self addGestureRecognizer:_delayingRecognizer];
546 [self addGestureRecognizer:forwardingRecognizer];
547 }
548 return self;
549 }
550
551 - (void)forceResetForwardingGestureRecognizerState {
552 // When iPad pencil is involved in a finger touch gesture, the gesture is not reset to "possible"
553 // state and is stuck on "failed" state, which causes subsequent touches to be blocked. As a
554 // workaround, we force reset the state by recreating the forwarding gesture recognizer. See:
555 // https://github.com/flutter/flutter/issues/136244
556 ForwardingGestureRecognizer* oldForwardingRecognizer =
557 (ForwardingGestureRecognizer*)self.delayingRecognizer.forwardingRecognizer;
558 ForwardingGestureRecognizer* newForwardingRecognizer =
559 [oldForwardingRecognizer recreateRecognizerWithTarget:self];
560 self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer;
561 [self removeGestureRecognizer:oldForwardingRecognizer];
562 [self addGestureRecognizer:newForwardingRecognizer];
563 }
564
565 - (void)releaseGesture {
566 self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
567 }
568
569 - (BOOL)containsWebView:(UIView*)view remainingSubviewDepth:(int)remainingSubviewDepth {
570 if (remainingSubviewDepth < 0) {
571 return NO;
572 }
573 if ([view isKindOfClass:[WKWebView class]]) {
574 return YES;
575 }
576 for (UIView* subview in view.subviews) {
577 if ([self containsWebView:subview remainingSubviewDepth:remainingSubviewDepth - 1]) {
578 return YES;
579 }
580 }
581 return NO;
582 }
583
584 - (void)blockGesture {
585 switch (_blockingPolicy) {
586 case FlutterPlatformViewGestureRecognizersBlockingPolicyEager:
587 // We block all other gesture recognizers immediately in this policy.
588 self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
589
590 // On iOS 18.2, WKWebView's internal recognizer likely caches the old state of its blocking
591 // recognizers (i.e. delaying recognizer), resulting in non-tappable links. See
592 // https://github.com/flutter/flutter/issues/158961. Removing and adding back the delaying
593 // recognizer solves the problem, possibly because UIKit notifies all the recognizers related
594 // to (blocking or blocked by) this recognizer. It is not possible to inject this workaround
595 // from the web view plugin level. Right now we only observe this issue for
596 // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar
597 // issue arises for the other policy.
598 if (@available(iOS 18.2, *)) {
599 // This workaround is designed for WKWebView only. The 1P web view plugin provides a
600 // WKWebView itself as the platform view. However, some 3P plugins provide wrappers of
601 // WKWebView instead. So we perform DFS to search the view hierarchy (with a depth limit).
602 // Passing a limit of 0 means only searching for platform view itself; Pass 1 to include its
603 // children as well, and so on. We should be conservative and start with a small number. The
604 // AdMob banner has a WKWebView at depth 7.
605 if ([self containsWebView:self.embeddedView remainingSubviewDepth:1]) {
606 [self removeGestureRecognizer:self.delayingRecognizer];
607 [self addGestureRecognizer:self.delayingRecognizer];
608 }
609 }
610
611 break;
612 case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
613 if (self.delayingRecognizer.touchedEndedWithoutBlocking) {
614 // If touchesEnded of the `DelayingGesureRecognizer` has been already invoked,
615 // we want to set the state of the `DelayingGesureRecognizer` to
616 // `UIGestureRecognizerStateEnded` as soon as possible.
617 self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
618 } else {
619 // If touchesEnded of the `DelayingGesureRecognizer` has not been invoked,
620 // We will set a flag to notify the `DelayingGesureRecognizer` to set the state to
621 // `UIGestureRecognizerStateEnded` when touchesEnded is called.
622 self.delayingRecognizer.shouldEndInNextTouchesEnded = YES;
623 }
624 break;
625 default:
626 break;
627 }
628 }
629
630 // We want the intercepting view to consume the touches and not pass the touches up to the parent
631 // view. Make the touch event method not call super will not pass the touches up to the parent view.
632 // Hence we overide the touch event methods and do nothing.
633 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
634 }
635
636 - (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
637 }
638
639 - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
640 }
641
642 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
643 }
644
645 - (id)accessibilityContainer {
646 return self.flutterAccessibilityContainer;
647 }
648
649 @end
650
651 @implementation FlutterDelayingGestureRecognizer
652
653 - (instancetype)initWithTarget:(id)target
654 action:(SEL)action
655 forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer {
656 self = [super initWithTarget:target action:action];
657 if (self) {
658 self.delaysTouchesBegan = YES;
659 self.delaysTouchesEnded = YES;
660 self.delegate = self;
661 _shouldEndInNextTouchesEnded = NO;
662 _touchedEndedWithoutBlocking = NO;
663 _forwardingRecognizer = forwardingRecognizer;
664 }
665 return self;
666 }
667
668 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
669 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
670 // The forwarding gesture recognizer should always get all touch events, so it should not be
671 // required to fail by any other gesture recognizer.
672 return otherGestureRecognizer != _forwardingRecognizer && otherGestureRecognizer != self;
673 }
674
675 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
676 shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
677 return otherGestureRecognizer == self;
678 }
679
680 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
681 self.touchedEndedWithoutBlocking = NO;
682 [super touchesBegan:touches withEvent:event];
683 }
684
685 - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
686 if (self.shouldEndInNextTouchesEnded) {
687 self.state = UIGestureRecognizerStateEnded;
688 self.shouldEndInNextTouchesEnded = NO;
689 } else {
690 self.touchedEndedWithoutBlocking = YES;
691 }
692 [super touchesEnded:touches withEvent:event];
693 }
694
695 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
696 self.state = UIGestureRecognizerStateFailed;
697 }
698 @end
699
700 @implementation ForwardingGestureRecognizer {
701 // Weak reference to PlatformViewsController. The PlatformViewsController has
702 // a reference to the FlutterViewController, where we can dispatch pointer events to.
703 //
704 // The lifecycle of PlatformViewsController is bind to FlutterEngine, which should always
705 // outlives the FlutterViewController. And ForwardingGestureRecognizer is owned by a subview of
706 // FlutterView, so the ForwardingGestureRecognizer never out lives FlutterViewController.
707 // Therefore, `_platformViewsController` should never be nullptr.
708 __weak FlutterPlatformViewsController* _platformViewsController;
709 // Counting the pointers that has started in one touch sequence.
710 NSInteger _currentTouchPointersCount;
711 // We can't dispatch events to the framework without this back pointer.
712 // This gesture recognizer retains the `FlutterViewController` until the
713 // end of a gesture sequence, that is all the touches in touchesBegan are concluded
714 // with |touchesCancelled| or |touchesEnded|.
715 UIViewController<FlutterViewResponder>* _flutterViewController;
716 }
717
718 - (instancetype)initWithTarget:(id)target
719 platformViewsController:(FlutterPlatformViewsController*)platformViewsController {
720 self = [super initWithTarget:target action:nil];
721 if (self) {
722 self.delegate = self;
723 FML_DCHECK(platformViewsController);
724 _platformViewsController = platformViewsController;
725 _currentTouchPointersCount = 0;
726 }
727 return self;
728 }
729
730 - (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target {
731 return [[ForwardingGestureRecognizer alloc] initWithTarget:target
732 platformViewsController:_platformViewsController];
733 }
734
735 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
736 FML_DCHECK(_currentTouchPointersCount >= 0);
737 if (_currentTouchPointersCount == 0) {
738 // At the start of each gesture sequence, we reset the `_flutterViewController`,
739 // so that all the touch events in the same sequence are forwarded to the same
740 // `_flutterViewController`.
741 _flutterViewController = _platformViewsController.flutterViewController;
742 }
743 [_flutterViewController touchesBegan:touches withEvent:event];
744 _currentTouchPointersCount += touches.count;
745 }
746
747 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
748 [_flutterViewController touchesMoved:touches withEvent:event];
749 }
750
751 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
752 [_flutterViewController touchesEnded:touches withEvent:event];
753 _currentTouchPointersCount -= touches.count;
754 // Touches in one touch sequence are sent to the touchesEnded method separately if different
755 // fingers stop touching the screen at different time. So one touchesEnded method triggering does
756 // not necessarially mean the touch sequence has ended. We Only set the state to
757 // UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended.
758 if (_currentTouchPointersCount == 0) {
759 self.state = UIGestureRecognizerStateFailed;
760 _flutterViewController = nil;
761 [self forceResetStateIfNeeded];
762 }
763 }
764
765 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
766 // In the event of platform view is removed, iOS generates a "stationary" change type instead of
767 // "cancelled" change type.
768 // Flutter needs all the cancelled touches to be "cancelled" change types in order to correctly
769 // handle gesture sequence.
770 // We always override the change type to "cancelled".
771 [_flutterViewController forceTouchesCancelled:touches];
772 _currentTouchPointersCount -= touches.count;
773 if (_currentTouchPointersCount == 0) {
774 self.state = UIGestureRecognizerStateFailed;
775 _flutterViewController = nil;
776 [self forceResetStateIfNeeded];
777 }
778 }
779
780 - (void)forceResetStateIfNeeded {
781 __weak ForwardingGestureRecognizer* weakSelf = self;
782 dispatch_async(dispatch_get_main_queue(), ^{
783 ForwardingGestureRecognizer* strongSelf = weakSelf;
784 if (!strongSelf) {
785 return;
786 }
787 if (strongSelf.state != UIGestureRecognizerStatePossible) {
788 [(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
789 }
790 });
791 }
792
793 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
794 shouldRecognizeSimultaneouslyWithGestureRecognizer:
795 (UIGestureRecognizer*)otherGestureRecognizer {
796 return YES;
797 }
798 @end
UIView(FirstResponder)::flt_hasFirstResponderInViewHierarchySubtree
BOOL flt_hasFirstResponderInViewHierarchySubtree
Definition: FlutterPlatformViews_Internal.h:156
FlutterDelayingGestureRecognizer::forwardingRecognizer
UIGestureRecognizer * forwardingRecognizer
Definition: FlutterPlatformViews_Internal.h:175
PlatformViewFilter::backdropFilterView
UIVisualEffectView * backdropFilterView
Definition: FlutterPlatformViews_Internal.h:96
FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded
@ FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded
Definition: FlutterPlugin.h:269
rectSoFar_
CGRect rectSoFar_
Definition: FlutterPlatformViews.mm:209
ChildClippingView
Definition: FlutterPlatformViews.mm:143
-[FlutterTouchInterceptingView blockGesture]
void blockGesture()
Definition: FlutterPlatformViews.mm:584
-[FlutterClippingMaskView reset]
void reset()
Definition: FlutterPlatformViews.mm:234
-[FlutterTouchInterceptingView releaseGesture]
void releaseGesture()
Definition: FlutterPlatformViews.mm:565
FlutterClippingMaskView
Definition: FlutterPlatformViews.mm:206
_currentTouchPointersCount
NSInteger _currentTouchPointersCount
Definition: FlutterPlatformViews.mm:700
PlatformViewFilter
Definition: FlutterPlatformViews.mm:51
GetCATransform3DFromSkMatrix
static CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix &matrix)
Definition: FlutterPlatformViewsController.mm:46
_flutterViewController
UIViewController< FlutterViewResponder > * _flutterViewController
Definition: FlutterPlatformViews.mm:715
-[FlutterTouchInterceptingView(Tests) accessibilityContainer]
id accessibilityContainer()
-[FlutterClippingMaskView clipRect:matrix:]
void clipRect:matrix:(const SkRect &clipSkRect,[matrix] const SkMatrix &matrix)
Definition: FlutterPlatformViews.mm:285
initWithFrame
instancetype initWithFrame
Definition: FlutterTextInputPlugin.h:172
kMaxPointsInVerb
static constexpr FLUTTER_ASSERT_ARC int kMaxPointsInVerb
Definition: FlutterPlatformViews.mm:15
+[PlatformViewFilter resetPreparation]
void resetPreparation()
Definition: FlutterPlatformViews.mm:78
PlatformViewFilter::frame
CGRect frame
Definition: FlutterPlatformViews_Internal.h:85
FlutterDelayingGestureRecognizer::shouldEndInNextTouchesEnded
BOOL shouldEndInNextTouchesEnded
Definition: FlutterPlatformViews_Internal.h:169
FlutterClippingMaskViewPool
Definition: FlutterPlatformViews.mm:464
ios_surface.h
-[ChildClippingView backdropFilterSubviews]
NSMutableArray * backdropFilterSubviews()
Definition: FlutterPlatformViews.mm:181
_indexOfVisualEffectSubview
static NSInteger _indexOfVisualEffectSubview
Definition: FlutterPlatformViews.mm:57
ForwardingGestureRecognizer
Definition: FlutterPlatformViews.mm:700
FlutterDelayingGestureRecognizer
Definition: FlutterPlatformViews.mm:651
FlutterPlatformViewGestureRecognizersBlockingPolicyEager
@ FlutterPlatformViewGestureRecognizersBlockingPolicyEager
Definition: FlutterPlugin.h:261
containsNonRectPath_
BOOL containsNonRectPath_
Definition: FlutterPlatformViews.mm:206
FlutterPlatformViews_Internal.h
fml
Definition: profiler_metrics_ios.mm:41
FlutterPlatformViewGestureRecognizersBlockingPolicy
FlutterPlatformViewGestureRecognizersBlockingPolicy
Definition: FlutterPlugin.h:252
PlatformViewFilter::blurRadius
CGFloat blurRadius
Definition: FlutterPlatformViews_Internal.h:90
UIView(FirstResponder)
Definition: FlutterPlatformViews.mm:501
FlutterPlatformViewsController
Definition: FlutterPlatformViewsController.h:31
FlutterTouchInterceptingView
Definition: FlutterPlatformViews.mm:521
_gaussianBlurFilter
static NSObject * _gaussianBlurFilter
Definition: FlutterPlatformViews.mm:53
-[FlutterTouchInterceptingView embeddedView]
UIView * embeddedView()
_indexOfBackdropView
static NSInteger _indexOfBackdropView
Definition: FlutterPlatformViews.mm:55
_preparedOnce
static BOOL _preparedOnce
Definition: FlutterPlatformViews.mm:58
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
GetCGRectFromSkRect
static CGRect GetCGRectFromSkRect(const SkRect &clipSkRect)
Definition: FlutterPlatformViewsController.mm:70