7 #import <WebKit/WebKit.h>
9 #include "flutter/display_list/effects/dl_image_filter.h"
10 #include "flutter/fml/platform/darwin/cf_utils.h"
17 return CGRectMake(clipDlRect.GetX(),
19 clipDlRect.GetWidth(),
20 clipDlRect.GetHeight());
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];
30 transform.m21 = matrix.m[4];
31 transform.m22 = matrix.m[5];
32 transform.m23 = matrix.m[6];
33 transform.m24 = matrix.m[7];
35 transform.m31 = matrix.m[8];
36 transform.m32 = matrix.m[9];
37 transform.m33 = matrix.m[10];
38 transform.m34 = matrix.m[11];
40 transform.m41 = matrix.m[12];
41 transform.m42 = matrix.m[13];
42 transform.m43 = matrix.m[14];
43 transform.m44 = matrix.m[15];
49 void MoveTo(
const flutter::DlPoint& p2,
bool will_be_closed)
override {
50 CGPathMoveToPoint(path_ref_, nil, p2.x, p2.y);
52 void LineTo(
const flutter::DlPoint& p2)
override {
53 CGPathAddLineToPoint(path_ref_, nil, p2.x, p2.y);
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);
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);
65 void Close()
override { CGPathCloseSubpath(path_ref_); }
67 CGMutablePathRef
TakePath()
const {
return path_ref_; }
70 CGMutablePathRef path_ref_ = CGPathCreateMutable();
77 @property(nonatomic) BOOL backdropFilterViewConfigured;
82 - (void)updateVisualEffectView:(UIVisualEffectView*)visualEffectView;
96 blurRadius:(CGFloat)blurRadius
97 cornerRadius:(CGFloat)cornerRadius
98 isRoundedSuperellipse:(BOOL)isRoundedSuperellipse
99 visualEffectView:(UIVisualEffectView*)visualEffectView {
100 if (
self = [super init]) {
107 FML_DLOG(ERROR) <<
"Apple's API for UIVisualEffectView changed. Update the implementation to "
108 "access the gaussianBlur CAFilter.";
111 _backdropFilterView = visualEffectView;
112 _backdropFilterViewConfigured = NO;
124 + (void)prepareOnce:(UIVisualEffectView*)visualEffectView {
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;
139 }
else if ([NSStringFromClass([view
class]) hasSuffix:
@"VisualEffectSubview"]) {
146 + (BOOL)isUIVisualEffectViewImplementationValid {
151 FML_DCHECK(_backdropFilterView);
152 if (!
self.backdropFilterViewConfigured) {
153 [
self updateVisualEffectView:_backdropFilterView];
154 self.backdropFilterViewConfigured = YES;
156 return _backdropFilterView;
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 ];
166 UIView* visualEffectSubview = visualEffectView.subviews[_indexOfVisualEffectSubview];
167 visualEffectSubview.layer.backgroundColor = UIColor.clearColor.CGColor;
168 visualEffectView.frame = _frame;
170 visualEffectView.layer.cornerRadius = _cornerRadius;
171 visualEffectView.layer.cornerCurve =
172 _isRoundedSuperellipse ? kCACornerCurveContinuous : kCACornerCurveCircular;
173 visualEffectView.clipsToBounds = YES;
175 self.backdropFilterView = visualEffectView;
182 @property(nonatomic, copy) NSArray<PlatformViewFilter*>* filters;
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]) {
202 FML_DCHECK(
self.filters.count ==
self.backdropFilterSubviews.count);
203 if (
self.filters.count == 0 && filters.count == 0) {
206 self.filters = filters;
207 NSUInteger index = 0;
208 for (index = 0; index <
self.filters.count; index++) {
209 UIVisualEffectView* backdropFilterView;
212 backdropFilterView = filter.backdropFilterView;
213 [self addSubview:backdropFilterView];
214 [self.backdropFilterSubviews addObject:backdropFilterView];
216 [filter updateVisualEffectView:self.backdropFilterSubviews[index]];
220 [self.backdropFilterSubviews[i - 1] removeFromSuperview];
221 [self.backdropFilterSubviews removeLastObject];
226 if (!_backdropFilterSubviews) {
227 _backdropFilterSubviews = [[NSMutableArray alloc] init];
229 return _backdropFilterSubviews;
244 @property(nonatomic) CATransform3D reverseScreenScale;
246 - (
fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
251 std::vector<fml::CFRef<CGPathRef>> paths_;
257 return [
self initWithFrame:frame screenScale:[UIScreen mainScreen].scale];
260 - (instancetype)
initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale {
262 self.backgroundColor = UIColor.clearColor;
263 _reverseScreenScale = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);
270 + (Class)layerClass {
271 return [CAShapeLayer class];
274 - (CAShapeLayer*)shapeLayer {
275 return (CAShapeLayer*)
self.layer;
282 [
self shapeLayer].path = nil;
283 [
self setNeedsDisplay];
291 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
295 - (void)drawRect:(CGRect)rect {
299 CGContextRef context = UIGraphicsGetCurrentContext();
300 CGContextSaveGState(context);
303 CGContextSetAlpha(context, 1);
305 for (
size_t i = 0; i < paths_.size(); i++) {
306 CGContextAddPath(context, paths_.at(i));
307 CGContextClip(context);
309 CGContextFillRect(context, rect);
310 CGContextRestoreGState(context);
314 [
super drawRect:rect];
315 if (![
self shapeLayer].path) {
316 if (paths_.size() == 1) {
318 [
self shapeLayer].path = paths_.at(0);
321 CGPathRef pathSoFar = CGPathCreateWithRect(
rectSoFar_, nil);
322 [
self shapeLayer].path = pathSoFar;
323 CGPathRelease(pathSoFar);
329 - (void)clipRect:(const
flutter::DlRect&)clipDlRect matrix:(const
flutter::DlMatrix&)matrix {
331 CGPathRef path = CGPathCreateWithRect(clipRect, nil);
333 CATransform3D matrixInPoints =
335 paths_.push_back([
self getTransformedPath:path matrix:matrixInPoints]);
336 CGAffineTransform affine = [
self affineWithMatrix:matrixInPoints];
338 if (affine.b == 0 && affine.c == 0) {
345 - (void)clipRRect:(const
flutter::DlRoundRect&)clipDlRRect matrix:(const
flutter::DlMatrix&)matrix {
346 if (clipDlRRect.IsEmpty()) {
348 }
else if (clipDlRRect.IsRect()) {
349 [
self clipRect:clipDlRRect.GetBounds() matrix:matrix];
352 CGPathRef pathRef =
nullptr;
355 if (clipDlRRect.GetRadii().AreAllCornersSame()) {
357 auto radii = clipDlRRect.GetRadii();
359 CGPathCreateWithRoundedRect(clipRect, radii.top_left.width, radii.top_left.height, nil);
361 CGMutablePathRef mutablePathRef = CGPathCreateMutable();
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;
379 CGPathMoveToPoint(mutablePathRef, nil,
380 left + top_left.width, top);
382 CGPathAddLineToPoint(mutablePathRef, nil,
383 right - top_right.width, top);
384 CGPathAddCurveToPoint(mutablePathRef, nil,
386 right, top + top_right.height,
387 right, top + top_right.height);
389 CGPathAddLineToPoint(mutablePathRef, nil,
390 right, bottom - bottom_right.height);
391 CGPathAddCurveToPoint(mutablePathRef, nil,
393 right - bottom_right.width, bottom,
394 right - bottom_right.width, bottom);
396 CGPathAddLineToPoint(mutablePathRef, nil,
397 left + bottom_left.width, bottom);
398 CGPathAddCurveToPoint(mutablePathRef, nil,
400 left, bottom - bottom_left.height,
401 left, bottom - bottom_left.height);
403 CGPathAddLineToPoint(mutablePathRef, nil,
404 left, top + top_left.height);
405 CGPathAddCurveToPoint(mutablePathRef, nil,
407 left + top_left.width, top,
408 left + top_left.width, top);
409 CGPathCloseSubpath(mutablePathRef);
410 pathRef = mutablePathRef;
413 CATransform3D matrixInPoints =
418 paths_.push_back([
self getTransformedPath:pathRef matrix:matrixInPoints]);
422 - (void)clipPath:(const
flutter::DlPath&)dlPath matrix:(const
flutter::DlMatrix&)matrix {
425 CGPathReceiver receiver;
430 dlPath.Dispatch(receiver);
433 CATransform3D matrixInPoints =
435 paths_.push_back([
self getTransformedPath:receiver.TakePath() matrix:matrixInPoints]);
438 - (CGAffineTransform)affineWithMatrix:(CATransform3D)matrix {
439 return CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41,
443 - (
fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix {
444 CGAffineTransform affine = [
self affineWithMatrix:matrix];
445 CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine);
448 return fml::CFRef<CGPathRef>(transformedPath);
457 @property(nonatomic) NSUInteger capacity;
461 @property(nonatomic) NSMutableSet<FlutterClippingMaskView*>* pool;
467 - (instancetype)initWithCapacity:(NSInteger)capacity {
468 if (
self = [super init]) {
471 _pool = [[NSMutableSet alloc] initWithCapacity:1];
472 _capacity = capacity;
478 FML_DCHECK(
self.pool.count <=
self.capacity);
479 if (
self.pool.count == 0) {
482 screenScale:UIScreen.mainScreen.scale];
485 maskView.frame = frame;
487 [
self.pool removeObject:maskView];
492 FML_DCHECK(![
self.pool containsObject:maskView]);
493 FML_DCHECK(
self.pool.count <=
self.capacity);
494 if (
self.pool.count ==
self.capacity) {
497 [
self.pool addObject:maskView];
504 if (
self.isFirstResponder) {
507 for (UIView* subview in
self.subviews) {
508 if (subview.flt_hasFirstResponderInViewHierarchySubtree) {
518 @property(nonatomic, weak, readonly) UIViewController<FlutterViewResponder>* flutterViewController;
524 - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
526 gestureRecognizersBlockingPolicy:
528 self = [
super initWithFrame:embeddedView.frame];
530 self.multipleTouchEnabled = YES;
532 _platformViewsController = platformViewsController;
535 (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
537 [
self addSubview:embeddedView];
541 platformViewsController:platformViewsController];
543 _delayingRecognizer =
546 forwardingRecognizer:forwardingRecognizer];
553 [
self addGestureRecognizer:_delayingRecognizer];
555 [
self addGestureRecognizer:forwardingRecognizer];
560 - (void)forceResetForwardingGestureRecognizerState {
568 [oldForwardingRecognizer recreateRecognizerWithTarget:
self];
569 self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer;
570 [
self removeGestureRecognizer:oldForwardingRecognizer];
571 [
self addGestureRecognizer:newForwardingRecognizer];
575 self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
578 - (BOOL)containsWebView:(UIView*)view {
579 if ([view isKindOfClass:[WKWebView
class]]) {
582 for (UIView* subview in view.subviews) {
583 if ([
self containsWebView:subview]) {
590 - (void)searchAndFixWebView:(UIView*)view {
591 if ([view isKindOfClass:[WKWebView
class]]) {
592 return [
self searchAndFixWebViewGestureRecognzier:view];
594 for (UIView* subview in view.subviews) {
595 [
self searchAndFixWebView:subview];
600 - (void)searchAndFixWebViewGestureRecognzier:(UIView*)view {
601 for (UIGestureRecognizer* recognizer in view.gestureRecognizers) {
612 if (recognizer.enabled &&
613 [NSStringFromClass([recognizer
class]) hasSuffix:
@"TouchEventsGestureRecognizer"]) {
614 recognizer.enabled = NO;
615 recognizer.enabled = YES;
618 for (UIView* subview in view.subviews) {
619 [
self searchAndFixWebViewGestureRecognzier:subview];
623 - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
626 if (
self.flutterViewController == nil) {
629 CGPoint pointInFlutterView = [
self convertPoint:point toView:self.flutterViewController.view];
634 if (![
self.flutterViewController
635 platformViewShouldAcceptTouchAtTouchBeganLocation:pointInFlutterView]) {
639 return [
super hitTest:point withEvent:event];
643 switch (_blockingPolicy) {
649 self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
659 if (@available(iOS 26.4, *)) {
662 }
else if (@available(iOS 26.0, *)) {
667 NSNumber* isWorkaroundDisabled =
668 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTDisableWebViewGestureReset"];
669 if (!isWorkaroundDisabled.boolValue) {
670 [
self searchAndFixWebView:self.embeddedView];
672 }
else if (@available(iOS 18.2, *)) {
677 [
self removeGestureRecognizer:self.delayingRecognizer];
678 [
self addGestureRecognizer:self.delayingRecognizer];
684 if (
self.delayingRecognizer.touchedEndedWithoutBlocking) {
688 self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
693 self.delayingRecognizer.shouldEndInNextTouchesEnded = YES;
704 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
707 - (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
710 - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
713 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
717 return self.flutterAccessibilityContainer;
724 - (instancetype)initWithTarget:(
id)target
726 forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer {
727 self = [
super initWithTarget:target action:action];
729 self.delaysTouchesBegan = YES;
730 self.delaysTouchesEnded = YES;
731 self.delegate =
self;
732 _shouldEndInNextTouchesEnded = NO;
733 _touchedEndedWithoutBlocking = NO;
739 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
740 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
743 return otherGestureRecognizer != _forwardingRecognizer && otherGestureRecognizer !=
self;
746 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
747 shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
748 return otherGestureRecognizer ==
self;
751 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
752 self.touchedEndedWithoutBlocking = NO;
753 [
super touchesBegan:touches withEvent:event];
756 - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
758 self.state = UIGestureRecognizerStateEnded;
759 self.shouldEndInNextTouchesEnded = NO;
761 self.touchedEndedWithoutBlocking = YES;
763 [
super touchesEnded:touches withEvent:event];
766 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
767 self.state = UIGestureRecognizerStateFailed;
789 - (instancetype)initWithTarget:(
id)target
791 self = [
super initWithTarget:target action:nil];
793 self.delegate =
self;
794 FML_DCHECK(platformViewsController);
795 _platformViewsController = platformViewsController;
803 platformViewsController:_platformViewsController];
806 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
821 [_flutterViewController touchesBegan:touches withEvent:event];
825 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
826 [_flutterViewController touchesMoved:touches withEvent:event];
829 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
830 [_flutterViewController touchesEnded:touches withEvent:event];
837 self.state = UIGestureRecognizerStateFailed;
839 [
self forceResetStateIfNeeded];
843 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
849 [_flutterViewController forceTouchesCancelled:touches];
852 self.state = UIGestureRecognizerStateFailed;
854 [
self forceResetStateIfNeeded];
858 - (void)forceResetStateIfNeeded {
862 if (@available(iOS 26.0, *)) {
866 dispatch_async(dispatch_get_main_queue(), ^{
871 if (strongSelf.state != UIGestureRecognizerStatePossible) {
872 [(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
877 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
878 shouldRecognizeSimultaneouslyWithGestureRecognizer:
879 (UIGestureRecognizer*)otherGestureRecognizer {
FlutterPlatformViewGestureRecognizersBlockingPolicy
@ FlutterPlatformViewGestureRecognizersBlockingPolicyEager
@ FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded
@ FlutterPlatformViewGestureRecognizersBlockingPolicyDoNotBlockGesture
instancetype initWithFrame
id accessibilityContainer()
BOOL flt_hasFirstResponderInViewHierarchySubtree
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
CGMutablePathRef TakePath() const
UIGestureRecognizer * forwardingRecognizer
BOOL shouldEndInNextTouchesEnded
FlutterPlatformViewGestureRecognizersBlockingPolicy blockingPolicy