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