Вырезать форму с анимацией

Я хочу сделать что-то похожее на следующее:

Как замаскировать изображение в IOS sdk?

Я хочу покрыть весь экран полупрозрачным черным. Затем я хочу вырезать круг из полупрозрачного черного покрытия, чтобы вы могли ясно видеть. Я делаю это, чтобы выделить части экрана для учебника.

Затем я хочу анимировать круг выреза на другие части экрана. Я также хочу, чтобы вы могли растягивать круг выреза по горизонтали и вертикали, так же как и с общим фоновым изображением кнопки.

Ответ 1

(ОБНОВЛЕНИЕ: см. также мой другой ответ, в котором описывается, как настроить несколько независимых перекрывающихся отверстий.)

Позвольте использовать простой старый 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];
}

Теперь нам нужно добавить маску, которая вырезает отверстие из отверстия. Мы сделаем это, создав сложный путь, состоящий из огромного прямоугольника с меньшим кругом в его центре. Мы заполним путь черным, оставив круг незаполненным и, следовательно, прозрачным. Черная часть имеет альфа = 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];
    });
}

Вот как это выглядит:

hole animation

Но он более плавный в реальной жизни.

В этом репозитории github вы можете найти полный рабочий тестовый проект .

Ответ 2

Это не просто. Я могу вам немного поработать. Это анимация, которая сложна. Здесь вывод некоторого кода, который я сбросил вместе:

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 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 mask property
  [translucentBlackLayer setMask:maskLayer];

  // Add the container layer to the view so we can see it
  [[[self view] layer] addSublayer:containerLayer];
}

Вам нужно было бы оживить слой маски, который вы могли бы создать на основе пользовательского ввода, но это будет немного сложно. Обратите внимание на строки, где я добавляю прямоугольный путь к пути окружности, а затем устанавливаю правило заливки несколькими строками позже на слое формы. Это то, что делает перевернутую маску возможной. Если вы оставите их, вместо этого вы увидите полупрозрачный черный цвет в центре круга, а затем ничего на внешней части (если это имеет смысл).

Возможно, попробуйте немного сыграть с этим кодом и посмотреть, сможете ли вы его оживить. Я буду играть с ним еще, поскольку у меня есть время, но это довольно интересная проблема. Хотелось бы увидеть полное решение.


UPDATE: Итак, вот еще один удар. Проблема заключается в том, что эта делает полупрозрачную маску белой, а не черной, но вверх - это то, что круг может быть легко анимирован.

Это создает сложный слой с полупрозрачным слоем, а слой окружности - братья и сестры внутри родительского слоя, который используется в качестве маски.

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

}

Ответ 3

Вот ответ, который работает с несколькими независимыми, возможно перекрывающимися прожекторами.

Я настрою свою иерархию просмотров следующим образом:

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

Тусклый вид будет тусклым, потому что его альфа смешивает его изображение с черным видом верхнего уровня.

Яркий вид не затемнен, но он показывает только, где его маска позволяет. Поэтому я просто установил маску, содержащую области прожектора, и нигде больше.

Вот как это выглядит:

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>

Мне понадобятся переменные экземпляра для subviews, слоя маски и массив отдельных прожекторов:

@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 или раскадровке, делегируя общий код инициализации частному методу:

#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

Я загрузил полное демо в github.

Ответ 4

Я боролся с этой же проблемой и нашел здесь большую помощь здесь, поэтому я решил поделиться своим решением, сочетающим несколько разных идей, которые я нашел в Интернете. Еще одна особенность, которую я добавил, заключалась в том, чтобы вырезать эффект градиента. Дополнительным преимуществом этого решения является то, что он работает с любым 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];
    }
}

Ответ 5

Если вы просто хотите что-то подключи и играй, я добавил библиотеку в CocoaPods, которая позволяет создавать наложения с прямоугольными/круглыми отверстиями, позволяя пользователю взаимодействовать с представлениями за наложением. Это быстрая реализация аналогичных стратегий, используемых в других ответах. Я использовал его для создания этого учебника для одного из наших приложений:

Tutorial using the TAOverlayView

Библиотека называется TAOverlayView и является открытым исходным кодом в Apache 2.0.

Примечание. Я еще не реализовал движущиеся дыры (если вы не перемещаете весь оверлей, как в других ответах).