| 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
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:
524 (fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController
525 gestureRecognizersBlockingPolicy:
526 (FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy {
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 - (void)blockGesture {
557 switch (_blockingPolicy) {
558 case FlutterPlatformViewGestureRecognizersBlockingPolicyEager:
559 // We block all other gesture recognizers immediately in this policy.
560 self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
561
562 // On iOS 18.2, WKWebView's internal recognizer likely caches the old state of its blocking
563 // recognizers (i.e. delaying recognizer), resulting in non-tappable links. See
564 // https://github.com/flutter/flutter/issues/158961. Removing and adding back the delaying
565 // recognizer solves the problem, possibly because UIKit notifies all the recognizers related
566 // to (blocking or blocked by) this recognizer. It is not possible to inject this workaround
567 // from the web view plugin level. Right now we only observe this issue for
568 // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar
569 // issue arises for the other policy.
570 if (@available(iOS 18.2, *)) {
571 if ([self.embeddedView isKindOfClass:[WKWebView class]]) {
572 [self removeGestureRecognizer:self.delayingRecognizer];
573 [self addGestureRecognizer:self.delayingRecognizer];
574 }
575 }
576
577 break;
578 case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
579 if (self.delayingRecognizer.touchedEndedWithoutBlocking) {
580 // If touchesEnded of the `DelayingGesureRecognizer` has been already invoked,
581 // we want to set the state of the `DelayingGesureRecognizer` to
582 // `UIGestureRecognizerStateEnded` as soon as possible.
583 self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
584 } else {
585 // If touchesEnded of the `DelayingGesureRecognizer` has not been invoked,
586 // We will set a flag to notify the `DelayingGesureRecognizer` to set the state to
587 // `UIGestureRecognizerStateEnded` when touchesEnded is called.
588 self.delayingRecognizer.shouldEndInNextTouchesEnded = YES;
589 }
590 break;
591 default:
592 break;
593 }
594 }
595
596 // We want the intercepting view to consume the touches and not pass the touches up to the parent
597 // view. Make the touch event method not call super will not pass the touches up to the parent view.
598 // Hence we overide the touch event methods and do nothing.
599 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
600 }
601
602 - (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
603 }
604
605 - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
606 }
607
608 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
609 }
610
611 - (id)accessibilityContainer {
612 return self.flutterAccessibilityContainer;
613 }
614
615 @end
616
617 @implementation FlutterDelayingGestureRecognizer
618
619 - (instancetype)initWithTarget:(id)target
620 action:(SEL)action
621 forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer {
622 self = [super initWithTarget:target action:action];
623 if (self) {
624 self.delaysTouchesBegan = YES;
625 self.delaysTouchesEnded = YES;
626 self.delegate = self;
627 _shouldEndInNextTouchesEnded = NO;
628 _touchedEndedWithoutBlocking = NO;
629 _forwardingRecognizer = forwardingRecognizer;
630 }
631 return self;
632 }
633
634 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
635 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
636 // The forwarding gesture recognizer should always get all touch events, so it should not be
637 // required to fail by any other gesture recognizer.
638 return otherGestureRecognizer != _forwardingRecognizer && otherGestureRecognizer != self;
639 }
640
641 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
642 shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
643 return otherGestureRecognizer == self;
644 }
645
646 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
647 self.touchedEndedWithoutBlocking = NO;
648 [super touchesBegan:touches withEvent:event];
649 }
650
651 - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
652 if (self.shouldEndInNextTouchesEnded) {
653 self.state = UIGestureRecognizerStateEnded;
654 self.shouldEndInNextTouchesEnded = NO;
655 } else {
656 self.touchedEndedWithoutBlocking = YES;
657 }
658 [super touchesEnded:touches withEvent:event];
659 }
660
661 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
662 self.state = UIGestureRecognizerStateFailed;
663 }
664 @end
665
666 @implementation ForwardingGestureRecognizer {
667 // Weak reference to PlatformViewsController. The PlatformViewsController has
668 // a reference to the FlutterViewController, where we can dispatch pointer events to.
669 //
670 // The lifecycle of PlatformViewsController is bind to FlutterEngine, which should always
671 // outlives the FlutterViewController. And ForwardingGestureRecognizer is owned by a subview of
672 // FlutterView, so the ForwardingGestureRecognizer never out lives FlutterViewController.
673 // Therefore, `_platformViewsController` should never be nullptr.
674 fml::WeakPtr<flutter::PlatformViewsController> _platformViewsController;
675 // Counting the pointers that has started in one touch sequence.
676 NSInteger _currentTouchPointersCount;
677 // We can't dispatch events to the framework without this back pointer.
678 // This gesture recognizer retains the `FlutterViewController` until the
679 // end of a gesture sequence, that is all the touches in touchesBegan are concluded
680 // with |touchesCancelled| or |touchesEnded|.
681 fml::scoped_nsobject<UIViewController<FlutterViewResponder>> _flutterViewController;
682 }
683
684 - (instancetype)initWithTarget:(id)target
685 platformViewsController:
686 (fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController {
687 self = [super initWithTarget:target action:nil];
688 if (self) {
689 self.delegate = self;
690 FML_DCHECK(platformViewsController.get() != nullptr);
691 _platformViewsController = std::move(platformViewsController);
692 _currentTouchPointersCount = 0;
693 }
694 return self;
695 }
696
697 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
698 FML_DCHECK(_currentTouchPointersCount >= 0);
699 if (_currentTouchPointersCount == 0) {
700 // At the start of each gesture sequence, we reset the `_flutterViewController`,
701 // so that all the touch events in the same sequence are forwarded to the same
702 // `_flutterViewController`.
703 _flutterViewController.reset(_platformViewsController->GetFlutterViewController());
704 }
705 [_flutterViewController.get() touchesBegan:touches withEvent:event];
706 _currentTouchPointersCount += touches.count;
707 }
708
709 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
710 [_flutterViewController.get() touchesMoved:touches withEvent:event];
711 }
712
713 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
714 [_flutterViewController.get() touchesEnded:touches withEvent:event];
715 _currentTouchPointersCount -= touches.count;
716 // Touches in one touch sequence are sent to the touchesEnded method separately if different
717 // fingers stop touching the screen at different time. So one touchesEnded method triggering does
718 // not necessarially mean the touch sequence has ended. We Only set the state to
719 // UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended.
720 if (_currentTouchPointersCount == 0) {
721 self.state = UIGestureRecognizerStateFailed;
722 _flutterViewController.reset(nil);
723 }
724 }
725
726 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
727 // In the event of platform view is removed, iOS generates a "stationary" change type instead of
728 // "cancelled" change type.
729 // Flutter needs all the cancelled touches to be "cancelled" change types in order to correctly
730 // handle gesture sequence.
731 // We always override the change type to "cancelled".
732 [_flutterViewController.get() forceTouchesCancelled:touches];
733 _currentTouchPointersCount -= touches.count;
734 if (_currentTouchPointersCount == 0) {
735 self.state = UIGestureRecognizerStateFailed;
736 _flutterViewController.reset(nil);
737 }
738 }
739
740 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
741 shouldRecognizeSimultaneouslyWithGestureRecognizer:
742 (UIGestureRecognizer*)otherGestureRecognizer {
743 return YES;
744 }
745 @end
-[FlutterTouchInterceptingView embeddedView]
UIView * embeddedView()
_flutterViewController
fml::scoped_nsobject< UIViewController< FlutterViewResponder > > _flutterViewController
Definition: FlutterPlatformViews_Internal.mm:681
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:666
-[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:556
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