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