用动画剪出形状

时间:2013-03-13 02:04:08

标签: ios uikit core-animation core-graphics mask

我想做类似以下的事情:

How to mask an image in IOS sdk?

我想用半透明的黑色覆盖整个屏幕。然后,我想从半透明的黑色覆盖物上切下一个圆圈,这样你就可以清楚地看透。我这样做是为了突出显示教程的部分内容。

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

5 个答案:

答案 0 :(得分:58)

(更新:另请参阅my other answer,其中介绍了如何设置多个独立的重叠孔。)

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

@implementation ViewController {
    UIView *holeView;
}

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

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

由于我们想要移动孔,因此可以方便地使孔视图非常大,以便无论位于何处都覆盖其余内容。我们将它变成10000x10000。 (这不会占用更多内存,因为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];
}

现在我们需要添加从孔视图中切出一个孔的蒙版。我们将通过创建一个复合路径来实现这一点,该路径由一个巨大的矩形组成,其中心有一个较小的圆圈。我们将用黑色填充路径,使圆圈未填充,因此是透明的。黑色部分的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;
}

请注意,我已将圆圈放在10000x10000视图的中心。这意味着我们可以设置holeView.center来设置圆的中心相对于其他内容。因此,例如,我们可以轻松地在主视图上上下动画:

- (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];
    });
}

这是它的样子:

hole animation

但它在现实生活中更顺畅。

您可以找到完整的工作测试项目in this github repository

答案 1 :(得分:18)

这不是一个简单的问题。我可以在那里找到你的方式。这是一个很棘手的动画。这是我扔在一起的一些代码的输出:

Inverted Mask Layer

代码是这样的:

- (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];
}

您必须根据用户输入设置可以构建的遮罩层的动画,但这将有点挑战性。注意我在圆形路径上添加矩形路径然后在形状图层上稍后设置填充规则的行。这些使得倒置掩模成为可能。如果你把它们留下来,你会在圆圈的中心显示半透明的黑色,然后在外面部分没有任何东西(如果这是有道理的)。

也许尝试使用此代码,看看你是否可以让它动画。因为我有时间,我会更多地玩它,但这是一个非常有趣的问题。很想看到一个完整的解决方案。


更新:所以这是另一个尝试。这里的问题是这个让半透明的面具看起来是白色而不是黑色,但好处是圆圈可以很容易地动画。

这个构建了一个复合层,其中半透明层和圆形层是父层内的兄弟,它们被用作掩模。

Composite Mask

我为此添加了一个基本动画,因此我们可以看到圆形图层动画。

- (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"];

}

答案 2 :(得分:8)

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

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

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

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

明亮的视野不会变暗,但它只显示其面具所在的位置。所以我只是将面具设置为包含聚光灯区域而不是其他地方。

这是它的样子:

enter image description here

我将使用此接口将其实现为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>

我需要子视图的实例变量,遮罩层和各个聚光灯路径的数组:

@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;
}

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

- (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中创建:

#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];
}

我的两个图像子视图将以大致相同的方式配置,因此我将调用另一个私有方法来创建昏暗的图像视图,然后调整它实际上是暗淡的:

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

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

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

创建两个图像视图的辅助方法设置内容模式并将新视图添加为子视图:

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

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

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

要布置明亮的图像视图,我需要将其框架设置为我的边界,我需要将其遮罩层的路径更新为各个聚光灯路径的并集:

- (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向我发送新的触摸时,我会找到包含触摸的单独聚光灯路径,并将触摸路径作为关联对象附加。这意味着我需要一个关联的对象密钥,它只需要一些私有的东西,我可以采取以下地址:

static char kSpotlightPathAssociatedObjectKey;

在这里,我实际上找到了路径并将其附加到触摸上。如果触摸在我的任何聚光灯路径之外,我会忽略它:

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

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

- (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运行时将自动取消关联附加路径(如果有):

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

要查找包含触摸的路径,我只需在聚光灯路径上循环,询问每个路径是否包含触摸:

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

@end

我上传了完整的演示to github

答案 3 :(得分:0)

我一直在努力解决同样的问题,并在SO上找到了一些很好的帮助,所以我想我会在网上找到一些不同的想法,分享我的解决方案。我添加的另一个功能是剪切具有渐变效果。此解决方案的附加好处是它可以与任何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

如果您不想要模糊效果,则可以将路径交换为椭圆形路径并跳过下方的模糊遮罩。否则,切口将是方形的并且填充有圆形渐变。

创建一个渐变形状,中心透明,慢慢褪色为黑色:

// 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];
    }
}

答案 4 :(得分:0)

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

Tutorial using the TAOverlayView

该库名为TAOverlayView,在Apache 2.0下是开源的。

注意:我还没有实现移动孔(除非你像其他答案那样移动整个叠加层)。