Cortar forma con animación

Quiero hacer algo similar a lo siguiente:

Cómo enmascarar una imagen en IOS sdk?

Quiero cubrir toda la pantalla con negro translúcido. Entonces, quiero cortar un círculo de la cubierta negra translúcida para que puedas ver claramente. Estoy haciendo esto para resaltar partes de la pantalla para un tutorial.

Luego quiero animar el círculo recortado a otras partes de la pantalla. También quiero poder estirar el círculo recortado horizontal y verticalmente, como lo haría con una imagen de fondo de botón genérico.

(ACTUALIZACIÓN: vea también mi otra respuesta que describe cómo configurar múltiples orificios superpuestos e independientes).

UIView un viejo UIView con un color de backgroundColor de negro translúcido, y le damos a su capa una máscara que corta un agujero en el medio. Necesitaremos una variable de instancia para hacer referencia a la vista de agujero:

 @implementation ViewController { UIView *holeView; } 

Después de cargar la vista principal, queremos agregar la vista de agujero como una subvista:

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

Como queremos mover el agujero, será conveniente hacer que la vista del agujero sea muy grande, de modo que cubra el rest del contenido, independientemente de dónde esté colocado. Lo haremos 10000×10000. (Esto no ocupa más memoria porque iOS no asigna automáticamente un bitmap para la vista).

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

Ahora tenemos que agregar la máscara que corta un agujero en la vista del agujero. Haremos esto creando una ruta compuesta que consista en un gran rectángulo con un círculo más pequeño en su centro. Llenaremos el camino con negro, dejando el círculo sin llenar y, por lo tanto, transparente. La parte negra tiene alfa = 1.0, por lo que muestra el color de fondo de la vista del agujero. La parte transparente tiene alfa = 0.0, por lo que parte de la vista del agujero también es transparente.

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

Tenga en cuenta que he puesto el círculo en el centro de la vista 10000×10000. Esto significa que podemos establecer holeView.center para establecer el centro del círculo en relación con el otro contenido. Entonces, por ejemplo, podemos animarlo fácilmente arriba y abajo sobre la vista principal:

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

Esto es lo que parece:

animación de agujero

Pero es más suave en la vida real.

Puede encontrar un proyecto de prueba de trabajo completo en este repository github .

Esto no es simple. Puedo llevarte un buen trecho hasta allí. Es la animación que es difícil. Aquí está el resultado de algún código que arrojé:

Capa de máscara invertida

El código es así:

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

Tendría que animar la capa de máscara que podría construir en función de la entrada del usuario, pero será un poco desafiante. Observe las líneas donde anexo una ruta rectangular a la ruta circular y luego establezca la regla de relleno unas líneas más adelante en la capa de forma. Esto es lo que hace posible la máscara invertida. Si los dejas afuera, en cambio, mostrarás el negro translúcido en el centro del círculo y luego nada en la parte exterior (si tiene sentido).

Tal vez intente jugar un poco con este código y vea si puede obtenerlo animándolo. Jugaré con eso un poco más ya que tengo tiempo, pero este es un problema bastante interesante. Me encantaría ver una solución completa.


ACTUALIZACIÓN: Aquí hay otra puñalada. El problema aquí es que esta hace que la máscara translúcida se vea blanca en lugar de negra, pero lo bueno es que ese círculo se puede animar con bastante facilidad.

Éste construye una capa compuesta con la capa translúcida y la capa circular siendo hermanos dentro de una capa principal que se usa como máscara.

Máscara compuesta

Agregué una animación básica a esta para que pudiéramos ver la capa circular animada.

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

Aquí hay una respuesta que funciona con múltiples focos independientes, posiblemente superpuestos.

Voy a configurar mi jerarquía de vista de esta manera:

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

La vista tenue aparecerá atenuada porque su alfa mezcla su imagen con el negro de la vista de nivel superior.

La vista shiny no está atenuada, pero solo muestra dónde la deja su máscara. Así que acabo de configurar la máscara para que contenga las áreas de foco y en ninguna otra parte.

Esto es lo que parece:

enter image description here

Lo implementaré como una subclase de UIView con esta interfaz:

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

Necesitaré QuartzCore (también llamado Core Animation) y el tiempo de ejecución de Objective-C para implementarlo:

 // SpotlightsView.m #import "SpotlightsView.h" #import  #import  

Necesitaré variables de instancia para las subvistas, la capa de máscara y una matriz de rutas de foco individuales:

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

Para implementar la propiedad de la image , simplemente la paso a sus subvistas de imágenes:

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

Para agregar un reflector que se puede arrastrar, creo una ruta que perfila el reflector, lo agrego a la matriz y me identifico como que necesito un diseño:

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

Necesito anular algunos métodos de UIView para gestionar la inicialización y el diseño. Me encargaré de crearlo mediante progtwigción o en un xib o storyboard delegando el código de inicialización común en un método privado:

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

Manejaré el diseño en métodos de ayuda separados para cada subvista:

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

Para arrastrar los focos cuando se tocan, necesito anular algunos de los métodos de UIResponder . Quiero manejar cada toque por separado, por lo que simplemente repito los toques actualizados, pasando cada uno a un método de ayuda:

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

Ahora implementaré la apariencia privada y los métodos de diseño.

 #pragma mark - Implementation details - appearance/layout 

Primero haré el código de inicialización común. Quiero establecer mi color de fondo en negro, ya que eso es parte de atenuar la vista de la imagen atenuada, y quiero admitir varios toques:

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

Mis dos subvistas de imágenes se configurarán principalmente de la misma manera, por lo que llamaré a otro método privado para crear la vista de la imagen tenue, y luego la modificaré para que sea realmente tenue:

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

Llamaré al mismo método de ayuda para crear la vista shiny, luego agregaré su máscara de subcapa:

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

El método auxiliar que crea ambas vistas de imágenes establece el modo de contenido y agrega la nueva vista como una subvista:

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

Para diseñar la vista de la imagen tenue, solo necesito establecer su marco a mis límites:

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

Para diseñar la vista de imagen shiny, necesito establecer su marco a mis límites, y necesito actualizar la ruta de la capa de máscara para que sea la unión de las rutas de foco individuales:

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

Tenga en cuenta que esta no es una verdadera unión que encierra cada punto una vez. Se basa en el modo de llenado (el valor predeterminado, kCAFillRuleNonZero ) para garantizar que se incluyan puntos encerrados repetidamente en la máscara.

A continuación, toque manejo.

 #pragma mark - Implementation details - touch handling 

Cuando UIKit me envíe un nuevo toque, encontraré la ruta de foco individual que contiene el toque y ato la ruta al toque como un objeto asociado. Eso significa que necesito una clave de objeto asociada, que solo tiene que ser algo privado en el que pueda tomar la dirección de:

 static char kSpotlightPathAssociatedObjectKey; 

Aquí realmente encuentro el camino y lo conecto al tacto. Si el toque está fuera de cualquiera de mis rutas de foco, lo ignoro:

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

Cuando UIKit me dice que se ha movido un toque, veo si el toque tiene una ruta adjunta. De ser así, traduzco (deslizo) la ruta por la cantidad que se ha movido desde la última vez que la vi. Luego me identifico por el diseño:

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

En realidad, no necesito hacer nada cuando el toque finaliza o se cancela. El tiempo de ejecución de Objective-C eliminará la asociación de la ruta adjunta (si hay una) automáticamente:

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

Para encontrar la ruta que contiene un toque, simplemente recorro las rutas de foco, preguntando a cada una de ellas si contiene el toque:

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

He subido una demostración completa a github .

He estado luchando con este mismo problema y he encontrado una gran ayuda aquí en SO, así que pensé en compartir mi solución combinando algunas ideas diferentes que encontré en línea. Una característica adicional que agregué fue que el recorte tenía un efecto degradado. El beneficio adicional de esta solución es que funciona con cualquier UIView y no solo con imágenes.

Primera subclase UIView para UIView todo, excepto los fotogtwigs que desea recortar:

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

Si no desea el efecto de desenfoque, puede cambiar las rutas al óvalo y omitir la máscara de desenfoque a continuación. De lo contrario, el recorte será cuadrado y se rellenará con un degradado circular.

Cree una forma de degradado con el centro transparente y desvaneciéndose lentamente en negro:

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

Ahora solo necesita llamar a estos dos juntos y pasar el UIView s que desea 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]; } } 

Si solo quieres algo que sea plug and play, agregué una biblioteca a CocoaPods que te permite crear superposiciones con orificios rectangulares / circulares, lo que permite al usuario interactuar con las vistas detrás de la superposición. Es una implementación Swift de estrategias similares usadas en otras respuestas. Lo usé para crear este tutorial para una de nuestras aplicaciones:

Tutorial utilizando TAOverlayView

La biblioteca se llama TAOverlayView y es de código abierto bajo Apache 2.0.

Nota: Aún no he implementado los agujeros móviles (a menos que mueva toda la superposición como en otras respuestas).