| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" |
| |
| #include "flutter/display_list/display_list_image_filter.h" |
| #include "flutter/fml/platform/darwin/cf_utils.h" |
| #import "flutter/shell/platform/darwin/ios/ios_surface.h" |
| |
| static int kMaxPointsInVerb = 4; |
| |
| namespace flutter { |
| |
| FlutterPlatformViewLayer::FlutterPlatformViewLayer( |
| fml::scoped_nsobject<UIView> overlay_view, |
| fml::scoped_nsobject<UIView> overlay_view_wrapper, |
| std::unique_ptr<IOSSurface> ios_surface, |
| std::unique_ptr<Surface> surface) |
| : overlay_view(std::move(overlay_view)), |
| overlay_view_wrapper(std::move(overlay_view_wrapper)), |
| ios_surface(std::move(ios_surface)), |
| surface(std::move(surface)){}; |
| |
| FlutterPlatformViewLayer::~FlutterPlatformViewLayer() = default; |
| |
| FlutterPlatformViewsController::FlutterPlatformViewsController() |
| : layer_pool_(std::make_unique<FlutterPlatformViewLayerPool>()), |
| weak_factory_(std::make_unique<fml::WeakPtrFactory<FlutterPlatformViewsController>>(this)){}; |
| |
| FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; |
| |
| fml::WeakPtr<flutter::FlutterPlatformViewsController> FlutterPlatformViewsController::GetWeakPtr() { |
| return weak_factory_->GetWeakPtr(); |
| } |
| |
| CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix& matrix) { |
| // Skia only supports 2D transform so we don't map z. |
| CATransform3D transform = CATransform3DIdentity; |
| transform.m11 = matrix.getScaleX(); |
| transform.m21 = matrix.getSkewX(); |
| transform.m41 = matrix.getTranslateX(); |
| transform.m14 = matrix.getPerspX(); |
| |
| transform.m12 = matrix.getSkewY(); |
| transform.m22 = matrix.getScaleY(); |
| transform.m42 = matrix.getTranslateY(); |
| transform.m24 = matrix.getPerspY(); |
| return transform; |
| } |
| |
| void ResetAnchor(CALayer* layer) { |
| // Flow uses (0, 0) to apply transform matrix so we need to match that in Quartz. |
| layer.anchorPoint = CGPointZero; |
| layer.position = CGPointZero; |
| } |
| |
| } // namespace flutter |
| |
| @implementation ChildClippingView { |
| // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. |
| NSObject* _gaussianFilter; |
| } |
| |
| // Lazy initializes blurEffectView as the expected UIVisualEffectView. The backdropFilter blur |
| // requires this UIVisualEffectView initialization. The lazy initalization is only used to allow |
| // custom unit tests. |
| - (UIView*)blurEffectView { |
| if (!_blurEffectView) { |
| // blurEffectView is only needed to extract its gaussianBlur filter. It is released after |
| // searching its subviews and extracting the filter. |
| _blurEffectView = [[[UIVisualEffectView alloc] |
| initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; |
| } |
| return _blurEffectView; |
| } |
| |
| // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to |
| // be hit tested and consumed by this view if they are inside the embedded platform view which could |
| // be smaller the embedded platform view is rotated. |
| - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { |
| for (UIView* view in self.subviews) { |
| if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) { |
| return YES; |
| } |
| } |
| return NO; |
| } |
| |
| // Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its |
| // gaussianFilter. Returns nil if Apple's API has changed and the filter cannot be extracted. |
| - (NSObject*)extractGaussianFilter { |
| NSObject* gaussianFilter = nil; |
| |
| for (UIView* view in self.blurEffectView.subviews) { |
| if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { |
| for (CIFilter* filter in view.layer.filters) { |
| if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { |
| if ([[filter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { |
| gaussianFilter = filter; |
| } |
| // No need to look at other CIFilters. If the API structure has not changed, the |
| // gaussianBlur filter was succesfully saved. Otherwise, still exit the loop because the |
| // filter cannot be extracted. |
| break; |
| } |
| } |
| // No need to look at other UIViews. If the API structure has not changed, the gaussianBlur |
| // filter was succesfully saved. Otherwise, still exit the loop because the filter cannot |
| // be extracted. |
| break; |
| } |
| } |
| |
| return gaussianFilter; |
| } |
| |
| - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { |
| // The outer if-statement checks for the first time this method is called and _gaussianFilter is |
| // not initialized. The inner if-statement checks if extracting the gaussianBlur was successful. |
| // If it was not successful, this method will not be called again. Thus the if-statements check |
| // for different conditions. |
| if (!_gaussianFilter) { |
| _gaussianFilter = [self extractGaussianFilter]; |
| |
| if (!_gaussianFilter) { |
| FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " |
| "access the gaussianBlur CAFilter."; |
| return NO; |
| } |
| } |
| |
| BOOL newRadiusValues = NO; |
| |
| if ([blurRadii count] != [self.layer.filters count]) { |
| newRadiusValues = YES; |
| } else { |
| for (NSUInteger i = 0; i < [blurRadii count]; i++) { |
| if ([self.layer.filters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { |
| newRadiusValues = YES; |
| break; |
| } |
| } |
| } |
| |
| if (newRadiusValues) { |
| NSMutableArray* newGaussianFilters = [[[NSMutableArray alloc] init] autorelease]; |
| |
| for (NSUInteger i = 0; i < [blurRadii count]; i++) { |
| NSObject* newGaussianFilter = [[_gaussianFilter copy] autorelease]; |
| [newGaussianFilter setValue:blurRadii[i] forKey:@"inputRadius"]; |
| [newGaussianFilters addObject:newGaussianFilter]; |
| } |
| |
| self.layer.filters = newGaussianFilters; |
| } |
| |
| return YES; |
| } |
| |
| - (void)dealloc { |
| [_blurEffectView release]; |
| _blurEffectView = nil; |
| |
| [_gaussianFilter release]; |
| _gaussianFilter = nil; |
| [super dealloc]; |
| } |
| |
| @end |
| |
| @interface FlutterClippingMaskView () |
| |
| - (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix; |
| - (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect; |
| |
| @end |
| |
| @implementation FlutterClippingMaskView { |
| std::vector<fml::CFRef<CGPathRef>> paths_; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| if (self = [super initWithFrame:frame]) { |
| self.backgroundColor = UIColor.clearColor; |
| } |
| return self; |
| } |
| |
| // In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added |
| // this view as a subview of the ChildClippingView. |
| // This results this view blocking touch events on the ChildClippingView. |
| // So we should always ignore any touch events sent to this view. |
| // See https://github.com/flutter/flutter/issues/66044 |
| - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { |
| return NO; |
| } |
| |
| - (void)drawRect:(CGRect)rect { |
| CGContextRef context = UIGraphicsGetCurrentContext(); |
| CGContextSaveGState(context); |
| |
| // For mask view, only the alpha channel is used. |
| CGContextSetAlpha(context, 1); |
| |
| for (size_t i = 0; i < paths_.size(); i++) { |
| CGContextAddPath(context, paths_.at(i)); |
| CGContextClip(context); |
| } |
| CGContextFillRect(context, rect); |
| CGContextRestoreGState(context); |
| } |
| |
| - (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix { |
| CGRect clipRect = [self getCGRectFromSkRect:clipSkRect]; |
| CGPathRef path = CGPathCreateWithRect(clipRect, nil); |
| paths_.push_back([self getTransformedPath:path matrix:matrix]); |
| } |
| |
| - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix { |
| CGPathRef pathRef = nullptr; |
| switch (clipSkRRect.getType()) { |
| case SkRRect::kEmpty_Type: { |
| break; |
| } |
| case SkRRect::kRect_Type: { |
| [self clipRect:clipSkRRect.rect() matrix:matrix]; |
| return; |
| } |
| case SkRRect::kOval_Type: |
| case SkRRect::kSimple_Type: { |
| CGRect clipRect = [self getCGRectFromSkRect:clipSkRRect.rect()]; |
| pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(), |
| clipSkRRect.getSimpleRadii().y(), nil); |
| break; |
| } |
| case SkRRect::kNinePatch_Type: |
| case SkRRect::kComplex_Type: { |
| CGMutablePathRef mutablePathRef = CGPathCreateMutable(); |
| // Complex types, we manually add each corner. |
| SkRect clipSkRect = clipSkRRect.rect(); |
| SkVector topLeftRadii = clipSkRRect.radii(SkRRect::kUpperLeft_Corner); |
| SkVector topRightRadii = clipSkRRect.radii(SkRRect::kUpperRight_Corner); |
| SkVector bottomRightRadii = clipSkRRect.radii(SkRRect::kLowerRight_Corner); |
| SkVector bottomLeftRadii = clipSkRRect.radii(SkRRect::kLowerLeft_Corner); |
| |
| // Start drawing RRect |
| // Move point to the top left corner adding the top left radii's x. |
| CGPathMoveToPoint(mutablePathRef, nil, clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); |
| // Move point horizontally right to the top right corner and add the top right curve. |
| CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fRight - topRightRadii.x(), |
| clipSkRect.fTop); |
| CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fRight, clipSkRect.fTop, |
| clipSkRect.fRight, clipSkRect.fTop + topRightRadii.y(), |
| clipSkRect.fRight, clipSkRect.fTop + topRightRadii.y()); |
| // Move point vertically down to the bottom right corner and add the bottom right curve. |
| CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fRight, |
| clipSkRect.fBottom - bottomRightRadii.y()); |
| CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fRight, clipSkRect.fBottom, |
| clipSkRect.fRight - bottomRightRadii.x(), clipSkRect.fBottom, |
| clipSkRect.fRight - bottomRightRadii.x(), clipSkRect.fBottom); |
| // Move point horizontally left to the bottom left corner and add the bottom left curve. |
| CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fLeft + bottomLeftRadii.x(), |
| clipSkRect.fBottom); |
| CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fLeft, clipSkRect.fBottom, |
| clipSkRect.fLeft, clipSkRect.fBottom - bottomLeftRadii.y(), |
| clipSkRect.fLeft, clipSkRect.fBottom - bottomLeftRadii.y()); |
| // Move point vertically up to the top left corner and add the top left curve. |
| CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fLeft, |
| clipSkRect.fTop + topLeftRadii.y()); |
| CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fLeft, clipSkRect.fTop, |
| clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop, |
| clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); |
| CGPathCloseSubpath(mutablePathRef); |
| |
| pathRef = mutablePathRef; |
| break; |
| } |
| } |
| // TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that |
| // the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge |
| // clipping on iOS. |
| paths_.push_back([self getTransformedPath:pathRef matrix:matrix]); |
| } |
| |
| - (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { |
| if (!path.isValid()) { |
| return; |
| } |
| if (path.isEmpty()) { |
| return; |
| } |
| CGMutablePathRef pathRef = CGPathCreateMutable(); |
| |
| // Loop through all verbs and translate them into CGPath |
| SkPath::Iter iter(path, true); |
| SkPoint pts[kMaxPointsInVerb]; |
| SkPath::Verb verb = iter.next(pts); |
| SkPoint last_pt_from_last_verb; |
| while (verb != SkPath::kDone_Verb) { |
| if (verb == SkPath::kLine_Verb || verb == SkPath::kQuad_Verb || verb == SkPath::kConic_Verb || |
| verb == SkPath::kCubic_Verb) { |
| FML_DCHECK(last_pt_from_last_verb == pts[0]); |
| } |
| switch (verb) { |
| case SkPath::kMove_Verb: { |
| CGPathMoveToPoint(pathRef, nil, pts[0].x(), pts[0].y()); |
| last_pt_from_last_verb = pts[0]; |
| break; |
| } |
| case SkPath::kLine_Verb: { |
| CGPathAddLineToPoint(pathRef, nil, pts[1].x(), pts[1].y()); |
| last_pt_from_last_verb = pts[1]; |
| break; |
| } |
| case SkPath::kQuad_Verb: { |
| CGPathAddQuadCurveToPoint(pathRef, nil, pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y()); |
| last_pt_from_last_verb = pts[2]; |
| break; |
| } |
| case SkPath::kConic_Verb: { |
| // Conic is not available in quartz, we use quad to approximate. |
| // TODO(cyanglaz): Better approximate the conic path. |
| // https://github.com/flutter/flutter/issues/35062 |
| CGPathAddQuadCurveToPoint(pathRef, nil, pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y()); |
| last_pt_from_last_verb = pts[2]; |
| break; |
| } |
| case SkPath::kCubic_Verb: { |
| CGPathAddCurveToPoint(pathRef, nil, pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y(), |
| pts[3].x(), pts[3].y()); |
| last_pt_from_last_verb = pts[3]; |
| break; |
| } |
| case SkPath::kClose_Verb: { |
| CGPathCloseSubpath(pathRef); |
| break; |
| } |
| case SkPath::kDone_Verb: { |
| break; |
| } |
| } |
| verb = iter.next(pts); |
| } |
| paths_.push_back([self getTransformedPath:pathRef matrix:matrix]); |
| } |
| |
| - (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { |
| CGAffineTransform affine = |
| CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); |
| CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine); |
| CGPathRelease(path); |
| return fml::CFRef<CGPathRef>(transformedPath); |
| } |
| |
| - (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect { |
| return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft, |
| clipSkRect.fBottom - clipSkRect.fTop); |
| } |
| |
| @end |