用动画剪出形状

ma1*_*w28 28 core-animation core-graphics mask uikit ios

我想做类似以下的事情:

如何在IOS sdk中屏蔽图像?

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

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

rob*_*off 58

(更新:请参阅我的其他答案,其中介绍了如何设置多个独立的重叠孔.)

让我们用一个普通的旧UIViewbackgroundColor黑色半透明的,并给其层,削减一个洞出来,中间的面具.我们需要一个实例变量来引用孔视图:

@implementation ViewController {
    UIView *holeView;
}
Run Code Online (Sandbox Code Playgroud)

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

- (void)viewDidLoad {
    [super viewDidLoad];
    [self addHoleSubview];
}
Run Code Online (Sandbox Code Playgroud)

由于我们想要移动孔,因此可以方便地使孔视图非常大,以便覆盖其余内容,无论它位于何处.我们将它变成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];
}
Run Code Online (Sandbox Code Playgroud)

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

请注意,我已将圆圈放在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];
    });
}
Run Code Online (Sandbox Code Playgroud)

这是它的样子:

洞动画

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

您可以在此github存储库中找到完整的工作测试项目.


Mat*_*ong 18

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

倒置掩模层

代码是这样的:

- (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];
}
Run Code Online (Sandbox Code Playgroud)

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

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


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

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

复合面膜

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

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

}
Run Code Online (Sandbox Code Playgroud)


rob*_*off 8

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

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

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

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

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

这是它的样子:

在此输入图像描述

我将它作为UIView这个接口的子类实现:

// SpotlightsView.h

#import <UIKit/UIKit.h>

@interface SpotlightsView : UIView

@property (nonatomic, strong) UIImage *image;

- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius;

@end
Run Code Online (Sandbox Code Playgroud)

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

// SpotlightsView.m

#import "SpotlightsView.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
Run Code Online (Sandbox Code Playgroud)

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

@implementation SpotlightsView {
    UIImageView *_dimImageView;
    UIImageView *_brightImageView;
    CAShapeLayer *_mask;
    NSMutableArray *_spotlightPaths;
}
Run Code Online (Sandbox Code Playgroud)

要实现该image属性,我只需将其传递给您的图像子视图:

#pragma mark - Public API

- (void)setImage:(UIImage *)image {
    _dimImageView.image = image;
    _brightImageView.image = image;
}

- (UIImage *)image {
    return _dimImageView.image;
}
Run Code Online (Sandbox Code Playgroud)

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

- (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];
}
Run Code Online (Sandbox Code Playgroud)

我需要覆盖一些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;
}
Run Code Online (Sandbox Code Playgroud)

我将为每个子视图处理单独的辅助方法中的布局:

- (void)layoutSubviews {
    [super layoutSubviews];
    [self layoutDimImageView];
    [self layoutBrightImageView];
}
Run Code Online (Sandbox Code Playgroud)

要在触摸时拖动聚光灯,我需要覆盖一些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];
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我将实现私有外观和布局方法.

#pragma mark - Implementation details - appearance/layout
Run Code Online (Sandbox Code Playgroud)

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

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

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

- (void)initDimImageView {
    _dimImageView = [self newImageSubview];
    _dimImageView.alpha = 0.5;
}
Run Code Online (Sandbox Code Playgroud)

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

- (void)initBrightImageView {
    _brightImageView = [self newImageSubview];
    _mask = [CAShapeLayer layer];
    _brightImageView.layer.mask = _mask;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

- (void)layoutDimImageView {
    _dimImageView.frame = self.bounds;
}
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,这不是一个包含每个点一次的真正联合.它依赖于填充模式(默认值kCAFillRuleNonZero),以确保掩码中包含重复封闭的点.

接下来,触摸处理.

#pragma mark - Implementation details - touch handling
Run Code Online (Sandbox Code Playgroud)

当UIKit向我发送新的触摸时,我将找到包含触摸的单独聚光灯路径,并将触摸路径作为关联对象附加.这意味着我需要一个关联的对象密钥,只需要一些私有的东西,我可以采取以下地址:

static char kSpotlightPathAssociatedObjectKey;
Run Code Online (Sandbox Code Playgroud)

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

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

当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];
}
Run Code Online (Sandbox Code Playgroud)

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

- (void)touchEnded:(UITouch *)touch {
    // Nothing to do
}
Run Code Online (Sandbox Code Playgroud)

为了找到包含触摸的路径,我只是遍历聚光灯路径,询问每个路径是否包含触摸:

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

@end
Run Code Online (Sandbox Code Playgroud)

我已经向github上传了一个完整的演示版.