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