用animation剪出形状

我想要做类似于以下的事情:

如何掩盖IOS SDK中的图像?

我想用半透明的黑色覆盖整个屏幕。 然后,我想从半透明的黑色遮盖物中划出一个圆圈,以便清楚地看清楚。 我这样做是为了突出显示部分教程的屏幕。

然后,我想要将切出的圆圈设置为屏幕的其他部分。 我也希望能够水平和垂直拉伸切出的圆,就像使用通用button背景图像一样。

(更新:请参阅我的其他答案 ,描述如何build立多个独立的,重叠的孔。)

让我们使用一个普通的旧UIViewbackgroundColor半透明的黑色,并给它的图层面具,从中间切出一个洞。 我们将需要一个实例variables来引用孔视图:

 @implementation ViewController { UIView *holeView; } 

加载主视图后,我们要添加孔视图作为子视图:

 - (void)viewDidLoad { [super viewDidLoad]; [self addHoleSubview]; } 

由于我们想要移动洞,因此可以很方便地使洞视图变得非常大,以便覆盖剩下的内容而不pipe它的位置。 我们将它做成10000×10000。 (这不占用更多的内存,因为iOS不会自动为视图分配一个位图。)

 - (void)addHoleSubview { holeView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10000, 10000)]; holeView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5]; holeView.autoresizingMask = 0; [self.view addSubview:holeView]; [self addMaskToHoleView]; } 

现在我们需要添加从孔视图中切出一个孔的蒙版。 我们将通过创build一个由一个巨大的矩形组成的复合path来做到这一点,在它的中心有一个较小的圆圈。 我们用黑色填充path,留下未填充的圆,因此透明。 黑色部分有alpha = 1.0,所以它使孔视图的背景颜色显示。 透明部分具有alpha = 0.0,所以孔视图的一部分也是透明的。

 - (void)addMaskToHoleView { CGRect bounds = holeView.bounds; CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.frame = bounds; maskLayer.fillColor = [UIColor blackColor].CGColor; static CGFloat const kRadius = 100; CGRect const circleRect = CGRectMake(CGRectGetMidX(bounds) - kRadius, CGRectGetMidY(bounds) - kRadius, 2 * kRadius, 2 * kRadius); UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect]; [path appendPath:[UIBezierPath bezierPathWithRect:bounds]]; maskLayer.path = path.CGPath; maskLayer.fillRule = kCAFillRuleEvenOdd; holeView.layer.mask = maskLayer; } 

请注意,我已将圆圈放在10000×10000视图的中心。 这意味着我们可以设置holeView.center来设置相对于其他内容的圆的中心。 所以,例如,我们可以在主视图上轻松地进行animation处理:

 - (void)viewDidLayoutSubviews { CGRect const bounds = self.view.bounds; holeView.center = CGPointMake(CGRectGetMidX(bounds), 0); // Defer this because `viewDidLayoutSubviews` can happen inside an // autorotation animation block, which overrides the duration I set. dispatch_async(dispatch_get_main_queue(), ^{ [UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse animations:^{ holeView.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMaxY(bounds)); } completion:nil]; }); } 

以下是它的样子:

孔动画

但在现实生活中更平滑。

你可以在这个github仓库中find一个完整的工作testing项目。

这不是一个简单的问题。 我可以帮你解决问题。 这是棘手的animation。 下面是我扔在一起的一些代码的输出:

反掩模层

代码是这样的:

 - (void)viewDidLoad { [super viewDidLoad]; // Create a containing layer and set it contents with an image CALayer *containerLayer = [CALayer layer]; [containerLayer setBounds:CGRectMake(0.0f, 0.0f, 500.0f, 320.0f)]; [containerLayer setPosition:[[self view] center]]; UIImage *image = [UIImage imageNamed:@"cool"]; [containerLayer setContents:(id)[image CGImage]]; // Create your translucent black layer and set its opacity CALayer *translucentBlackLayer = [CALayer layer]; [translucentBlackLayer setBounds:[containerLayer bounds]]; [translucentBlackLayer setPosition: CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)]; [translucentBlackLayer setBackgroundColor:[[UIColor blackColor] CGColor]]; [translucentBlackLayer setOpacity:0.45]; [containerLayer addSublayer:translucentBlackLayer]; // Create a mask layer with a shape layer that has a circle path CAShapeLayer *maskLayer = [CAShapeLayer layer]; [maskLayer setBorderColor:[[UIColor purpleColor] CGColor]]; [maskLayer setBorderWidth:5.0f]; [maskLayer setBounds:[containerLayer bounds]]; // When you create a path, remember that origin is in upper left hand // corner, so you have to treat it as if it has an anchor point of 0.0, // 0.0 UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake([translucentBlackLayer bounds].size.width/2.0f - 100.0f, [translucentBlackLayer bounds].size.height/2.0f - 100.0f, 200.0f, 200.0f)]; // Append a rectangular path around the mask layer so that // we can use the even/odd fill rule to invert the mask [path appendPath:[UIBezierPath bezierPathWithRect:[maskLayer bounds]]]; // Set the path's fill color since layer masks depend on alpha [maskLayer setFillColor:[[UIColor blackColor] CGColor]]; [maskLayer setPath:[path CGPath]]; // Center the mask layer in the translucent black layer [maskLayer setPosition: CGPointMake([translucentBlackLayer bounds].size.width/2.0f, [translucentBlackLayer bounds].size.height/2.0f)]; // Set the fill rule to even odd [maskLayer setFillRule:kCAFillRuleEvenOdd]; // Set the translucent black layer's mask property [translucentBlackLayer setMask:maskLayer]; // Add the container layer to the view so we can see it [[[self view] layer] addSublayer:containerLayer]; } 

你将不得不根据用户input来创build你可以build立的遮罩层,但这将会有一定的难度。 注意我将一个矩形path追加到圆形path的行,然后在形状图层上稍后设置填充规则几行。 这些是使倒置的面具成为可能的原因。 如果你把这些放在外面,你将会在圆的中心显示半透明的黑色,然后在外面显示任何东西(如果有意义的话)。

也许尝试玩这个代码,看看你是否可以得到它的animation。 随着时间的推移,我会再玩一些,但这是一个非常有趣的问题。 很想看到一个完整的解决scheme。


更新:所以这里是另一个刺。 这里的麻烦在于,这个半透明的面膜看起来是白色的,而不是黑色的,但好处是这个圆形可以很容易地被激发。

这一个build立一个复合层与半透明层和圆层是兄弟姐妹内部父母层被用作面具。

复合面膜

我为这个添加了一个基本的animation,所以我们可以看到圆形图层的animation。

 - (void)viewDidLoad { [super viewDidLoad]; CGRect baseRect = CGRectMake(0.0f, 0.0f, 500.0f, 320.0f); CALayer *containerLayer = [CALayer layer]; [containerLayer setBounds:baseRect]; [containerLayer setPosition:[[self view] center]]; UIImage *image = [UIImage imageNamed:@"cool"]; [containerLayer setContents:(id)[image CGImage]]; CALayer *compositeMaskLayer = [CALayer layer]; [compositeMaskLayer setBounds:baseRect]; [compositeMaskLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)]; CALayer *translucentLayer = [CALayer layer]; [translucentLayer setBounds:baseRect]; [translucentLayer setBackgroundColor:[[UIColor blackColor] CGColor]]; [translucentLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)]; [translucentLayer setOpacity:0.35]; [compositeMaskLayer addSublayer:translucentLayer]; CAShapeLayer *circleLayer = [CAShapeLayer layer]; UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)]; [circleLayer setBounds:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)]; [circleLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)]; [circleLayer setPath:[circlePath CGPath]]; [circleLayer setFillColor:[[UIColor blackColor] CGColor]]; [compositeMaskLayer addSublayer:circleLayer]; [containerLayer setMask:compositeMaskLayer]; [[[self view] layer] addSublayer:containerLayer]; CABasicAnimation *posAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; [posAnimation setFromValue:[NSValue valueWithCGPoint:[circleLayer position]]]; [posAnimation setToValue:[NSValue valueWithCGPoint:CGPointMake([circleLayer position].x + 100.0f, [circleLayer position].y + 100)]]; [posAnimation setDuration:1.0f]; [posAnimation setRepeatCount:INFINITY]; [posAnimation setAutoreverses:YES]; [circleLayer addAnimation:posAnimation forKey:@"position"]; } 

这是一个适用于多个独立,可能重叠的聚光灯的答案。

我将像这样设置我的视图层次结构:

 SpotlightsView with black background UIImageView with `alpha`=.5 (“dim view”) UIImageView with shape layer mask (“bright view”) 

昏暗的视图将显示为灰色,因为它的alpha混合其图像与顶级视图的黑色。

明亮的景色不是暗淡的,但它只显示了它的面具让它的位置。 所以我只是设置面具包含聚光灯的地方和其他地方。

以下是它的样子:

在这里输入图像说明

我将使用此接口作为UIView的子类实现它:

 // SpotlightsView.h #import <UIKit/UIKit.h> @interface SpotlightsView : UIView @property (nonatomic, strong) UIImage *image; - (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius; @end 

我需要QuartzCore(也称为Core Animation)和Objective-C运行库来实现它:

 // SpotlightsView.m #import "SpotlightsView.h" #import <QuartzCore/QuartzCore.h> #import <objc/runtime.h> 

我将需要子视图,遮罩层和单个Spotlightpath的数组的实例variables:

 @implementation SpotlightsView { UIImageView *_dimImageView; UIImageView *_brightImageView; CAShapeLayer *_mask; NSMutableArray *_spotlightPaths; } 

为了实现image属性,我只是把它传递给你的图像子视图:

 #pragma mark - Public API - (void)setImage:(UIImage *)image { _dimImageView.image = image; _brightImageView.image = image; } - (UIImage *)image { return _dimImageView.image; } 

为了添加可拖动的聚光灯,我创build了一个概述聚光灯的path,将其添加到数组中,并将自己标记为需要布局:

 - (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius { UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius)]; [_spotlightPaths addObject:path]; [self setNeedsLayout]; } 

我需要重写UIView一些方法来处理初始化和布局。 我将通过将通用初始化代码委派给一个私有方法,来以编程方式或在xib或storyboard中创build:

 #pragma mark - UIView overrides - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self commonInit]; } return self; } 

我将在每个子视图的单独的帮助器方法中处理布局:

 - (void)layoutSubviews { [super layoutSubviews]; [self layoutDimImageView]; [self layoutBrightImageView]; } 

当他们被触摸时拖动聚光灯,我需要重写一些UIResponder方法。 我想单独处理每一个触摸,所以我只是循环遍历更新的触摸,传递给每个辅助方法:

 #pragma mark - UIResponder overrides - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches){ [self touchBegan:touch]; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches){ [self touchMoved:touch]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { [self touchEnded:touch]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { [self touchEnded:touch]; } } 

现在我将实现私人的外观和布局方法。

 #pragma mark - Implementation details - appearance/layout 

首先,我将执行常见的初始化代码。 我想设置我的背景颜色为黑色,因为这是使暗淡的图像视图昏暗的一部分,我想支持多个触摸:

 - (void)commonInit { self.backgroundColor = [UIColor blackColor]; self.multipleTouchEnabled = YES; [self initDimImageView]; [self initBrightImageView]; _spotlightPaths = [NSMutableArray array]; } 

我的两个图像子视图的configuration方式基本相同,所以我会调用另一个私有方法来创build暗淡的图像视图,然后调整它实际上是昏暗的:

 - (void)initDimImageView { _dimImageView = [self newImageSubview]; _dimImageView.alpha = 0.5; } 

我会调用相同的辅助方法来创build明亮的视图,然后添加其掩码子图层:

 - (void)initBrightImageView { _brightImageView = [self newImageSubview]; _mask = [CAShapeLayer layer]; _brightImageView.layer.mask = _mask; } 

同时创build图像视图的助手方法设置内容模式并将新视图添加为子视图:

 - (UIImageView *)newImageSubview { UIImageView *subview = [[UIImageView alloc] init]; subview.contentMode = UIViewContentModeScaleAspectFill; [self addSubview:subview]; return subview; } 

为了布置昏暗的图像视图,我只需要将其框架设置为我的界限:

 - (void)layoutDimImageView { _dimImageView.frame = self.bounds; } 

为了布置明亮的图像视图,我需要将其框架设置为我的界限,我需要将其遮罩层的path更新为单个聚光灯path的联合:

 - (void)layoutBrightImageView { _brightImageView.frame = self.bounds; UIBezierPath *unionPath = [UIBezierPath bezierPath]; for (UIBezierPath *path in _spotlightPaths) { [unionPath appendPath:path]; } _mask.path = unionPath.CGPath; } 

请注意,这不是一个包含每个点的真实联合。 它依靠填充模式(默认, kCAFillRuleNonZero )来确保重复封闭的点包含在掩码中。

接下来,触摸处理。

 #pragma mark - Implementation details - touch handling 

当UIKit给我一个新的触摸,我会find包含触摸的单个聚光灯path,并将触摸的path作为关联的对象。 这意味着我需要一个关联的对象键,这只需要一些私人的东西,我可以把地址:

 static char kSpotlightPathAssociatedObjectKey; 

在这里,我实际上find了path,并将其附加到触摸。 如果触摸在我的聚光灯path之外,我忽略它:

 - (void)touchBegan:(UITouch *)touch { UIBezierPath *path = [self firstSpotlightPathContainingTouch:touch]; if (path == nil) return; objc_setAssociatedObject(touch, &kSpotlightPathAssociatedObjectKey, path, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } 

当UIKit告诉我触摸已经移动时,我看看触摸是否有附加的path。 如果是这样,我翻译(滑动)的path,自从我上次看到触摸已经移动的量。 然后我标记自己的布局:

 - (void)touchMoved:(UITouch *)touch { UIBezierPath *path = objc_getAssociatedObject(touch, &kSpotlightPathAssociatedObjectKey); if (path == nil) return; CGPoint point = [touch locationInView:self]; CGPoint priorPoint = [touch previousLocationInView:self]; [path applyTransform:CGAffineTransformMakeTranslation( point.x - priorPoint.x, point.y - priorPoint.y)]; [self setNeedsLayout]; } 

当触摸结束或取消时,我实际上不需要做任何事情。 Objective-C运行时将自动关联附加的path(如果有的话):

 - (void)touchEnded:(UITouch *)touch { // Nothing to do } 

要find包含触摸的path,我只需循环聚焦path,询问每个path是否包含触摸:

 - (UIBezierPath *)firstSpotlightPathContainingTouch:(UITouch *)touch { CGPoint point = [touch locationInView:self]; for (UIBezierPath *path in _spotlightPaths) { if ([path containsPoint:point]) return path; } return nil; } @end 

我已经上传了一个完整的演示给github 。

我一直在努力解决这个问题,在这里find了一些很好的帮助,所以我想我会分享我的解决scheme,结合我在网上find的一些不同的想法。 我添加了一个额外的function是切出具有渐变效果。 这个解决scheme的附加好处是,它可以与任何UIView一起使用,而不仅仅适用于图像。

第一个子类UIView把除了你想要剪切的帧之外的所有东西都黑掉:

 // BlackOutView.h @interface BlackOutView : UIView @property (nonatomic, retain) UIColor *fillColor; @property (nonatomic, retain) NSArray *framesToCutOut; @end // BlackOutView.m @implementation BlackOutView - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetBlendMode(context, kCGBlendModeDestinationOut); for (NSValue *value in self.framesToCutOut) { CGRect pathRect = [value CGRectValue]; UIBezierPath *path = [UIBezierPath bezierPathWithRect:pathRect]; // change to this path for a circular cutout if you don't want a gradient // UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:pathRect]; [path fill]; } CGContextSetBlendMode(context, kCGBlendModeNormal); } @end 

如果您不想要模糊效果,那么您可以将path交换到椭圆形,并跳过下面的模糊遮罩。 否则,切口将是正方形,并以圆形渐变填充。

创build一个渐变形状,使中心透明并缓慢地褪去黑色:

 // BlurFilterMask.h @interface BlurFilterMask : CAShapeLayer @property (assign) CGPoint origin; @property (assign) CGFloat diameter; @property (assign) CGFloat gradient; @end // BlurFilterMask.m @implementation CRBlurFilterMask - (void)drawInContext:(CGContextRef)context { CGFloat gradientWidth = self.diameter * 0.5f; CGFloat clearRegionRadius = self.diameter * 0.25f; CGFloat blurRegionRadius = clearRegionRadius + gradientWidth; CGColorSpaceRef baseColorSpace = CGColorSpaceCreateDeviceRGB(); CGFloat colors[8] = { 0.0f, 0.0f, 0.0f, 0.0f, // Clear region colour. 0.0f, 0.0f, 0.0f, self.gradient }; // Blur region colour. CGFloat colorLocations[2] = { 0.0f, 0.4f }; CGGradientRef gradient = CGGradientCreateWithColorComponents (baseColorSpace, colors, colorLocations, 2); CGContextDrawRadialGradient(context, gradient, self.origin, clearRegionRadius, self.origin, blurRegionRadius, kCGGradientDrawsAfterEndLocation); CGColorSpaceRelease(baseColorSpace); CGGradientRelease(gradient); } @end 

现在你只需要把这两个调用一起传入你想要剪切的UIView

 - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self addMaskInViews:@[self.viewCutout1, self.viewCutout2]]; } - (void) addMaskInViews:(NSArray *)viewsToCutOut { NSMutableArray *frames = [NSMutableArray new]; for (UIView *view in viewsToCutOut) { view.hidden = YES; // hide the view since we only use their bounds [frames addObject:[NSValue valueWithCGRect:view.frame]]; } // Create the overlay passing in the frames we want to cut out BlackOutView *overlay = [[BlackOutView alloc] initWithFrame:self.view.frame]; overlay.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.8]; overlay.framesToCutOut = frames; [self.view insertSubview:overlay atIndex:0]; // add a circular gradients inside each view for (UIView *maskView in viewsToCutOut) { BlurFilterMask *blurFilterMask = [BlurFilterMask layer]; blurFilterMask.frame = maskView.frame; blurFilterMask.gradient = 0.8f; blurFilterMask.diameter = MIN(maskView.frame.size.width, maskView.frame.size.height); blurFilterMask.origin = CGPointMake(maskView.frame.size.width / 2, maskView.frame.size.height / 2); [self.view.layer addSublayer:blurFilterMask]; [blurFilterMask setNeedsDisplay]; } } 

如果你只是想要即插即用的东西,我给CocoaPods添加了一个库,允许你创build带有矩形/圆孔的覆盖层,允许用户与覆盖层后面的视图进行交互。 在其他答案中使用了类似策略的Swift实现。 我用它为我们的一个应用程序创build了这个教程:

教程使用TAOverlayView

这个库叫做TAOverlayView ,在Apache 2.0下是开源的。

注意:我还没有实现移动孔(除非像其他答案一样移动整个覆盖层)。