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