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