blob: e3c3389080a7dcb70544c63e061c0cc4993a5a40 [file] [log] [blame]
// 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