切出外形与动画(Cut Out Shape with Animation)

2019-07-21 11:24发布

我想要做类似下面的内容:

如何掩盖了IOS SDK中的图像?

我想覆盖黑色半透明整个屏幕。 然后,我要砍了一圈出来的黑色半透明覆盖的,这样就可以通过清楚地看到。 我这样做是为了突出画面的部分的教程。

那么我想动画截出圈到屏幕的其他部分。 我也希望能够在水平方向和垂直方向上拉伸切出圈,因为你将与一个通用按钮背景图片做。

Answer 1:

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

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

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

现在,我们需要补充的是裁减孔出洞视图的面具。 我们将通过创建由一个巨大的矩形,在其中心有较小的圆复合路径做到这一点。 我们要填补路径有黑色,留下圈填充,因此透明。 黑色部分具有α-= 1.0,因此它使孔视图的背景颜色显示。 透明部分具有α-= 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];
    });
}

这里是什么样子:

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

你可以找到一个完整的工作测试项目在此的github仓库 。



Answer 2:

这不是一个简单的。 我可以给你那里的路好位。 这是棘手的动画。 下面是一些代码,我扔在一起的输出:

该代码是这样的:

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

你将不得不动画,你可以根据用户的输入建立遮罩层,但是这将是一个挑战。 注意其中I追加的矩形路径圆路径,然后设置填充规则的几行以后的形状层上的线。 这是什么使反转掩码可能。 如果你离开了这些,你反而会显示在圆心的半透明的黑色,然后就没什么外部(如果是有道理的)。

也许尝试使用此代码玩了一下,看看你能得到它的动画。 我会用它玩多一些,因为我有时间,但是这是一个非常有趣的问题。 希望能看到一个完整的解决方案。


更新:所以这里的它的另一种刺。 这里的问题是,这个人让半透明屏呈现白色,而不是黑色的,但好处是,圈子可以很容易地制作动画。

这一个积聚与半透明层和所述的圆层是使其被用作掩模的母层的内部的兄弟姐妹的复合层。

我添加了一个基本的动画这一个,所以我们可以看到圈层动画。

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

}


Answer 3:

这里是与多个独立的,可能重叠聚光灯问题的解答。

我建立了我的视图层次是这样的:

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(也称为核心动画)和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处理初始化和布局。 我会处理或者正在创建编程或在厦门国际银行或故事情节通过委派共同初始化代码的私有方法:

#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的运行时将DE-相关联的连接路径(如果存在的话)会自动:

- (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

我上传了一个完整的演示到github上 。



Answer 4:

我一直在挣扎与此相同的问题,在这里发现了一些很大的帮助的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];
    }
}


Answer 5:

如果你只是想要的东西,是即插即用的,我加了一个图书馆的CocoaPods,让您创建矩形/圆形孔覆盖,使用户能够与覆盖背后的观点互动。 这是一个快速实施在其他的答案中使用类似的策略。 我用它来为我们的应用程序创建一个本教程:

该库被称为TAOverlayView ,并使用Apache 2.0的开源。

注:我还没有实现移动孔,但(除非你将整个覆盖在其他的答案)。



文章来源: Cut Out Shape with Animation