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