ios - pwa - Cortar la forma con animación
pwa safari icon (5)
(ACTUALIZACIÓN: vea también mi otra respuesta que describe cómo configurar múltiples orificios independientes y superpuestos)
UIView
una UIView
vieja y UIView
con un color de backgroundColor
de color negro translúcido, y demos a su capa una máscara que corte 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 que la vista del agujero sea muy grande, de modo que cubra el resto del contenido, independientemente de dónde se encuentre. Lo haremos 10000x10000. (Esto no ocupa más memoria porque iOS no asigna automáticamente un mapa de bits 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 necesitamos agregar la máscara que corta un agujero fuera de la vista del agujero. Haremos esto creando un camino compuesto que consiste en un gran rectángulo con un círculo más pequeño en su centro. Rellenaremos el camino con negro, dejando el círculo vacío y por lo tanto transparente. La parte negra tiene alfa = 1.0 y, por lo tanto, hace que se vea 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;
}
Observe que he puesto el círculo en el centro de la vista 10000x10000. Esto significa que solo podemos configurar holeView.center
para establecer el centro del círculo en relación con el otro contenido. Entonces, por ejemplo, podemos animarlo fácilmente hacia arriba y hacia 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:
Pero es más suave en la vida real.
Puede encontrar un proyecto de prueba de trabajo completo en este repositorio de github .
Quiero hacer algo similar a lo siguiente:
¿Cómo enmascarar una imagen en IOS SDK?
Quiero cubrir toda la pantalla con negro translúcido. Luego, quiero cortar un círculo de la cubierta negra translúcida para que pueda ver con claridad. 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.
Aquí hay una respuesta que funciona con múltiples focos independientes, posiblemente superpuestos.
Configuraré mi jerarquía de vistas de esta manera:
SpotlightsView with black background
UIImageView with `alpha`=.5 (“dim view”)
UIImageView with shape layer mask (“bright view”)
La vista oscura aparecerá atenuada porque su alfa mezcla su imagen con el negro de la vista de nivel superior.
La vista brillante no está atenuada, pero solo muestra dónde la máscara lo permite. Así que acabo de configurar la máscara para contener las áreas de los focos y en ningún otro lugar.
Esto es lo que parece:
Lo implementaré como una subclase de UIView
con esta interfaz:
// SpotlightsView.h
#import <UIKit/UIKit.h>
@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 <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
Necesitaré variables de instancia para las subvistas, la capa de máscara y una matriz de rutas de Spotlight 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 foco de luz que se puede arrastrar, creo una ruta que describe el foco de luz, la agrego a la matriz y me doy cuenta de que necesito 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 manejar la inicialización y el diseño. Manejaré la creación programática o en un xib o storyboard delegando el código de inicialización común a 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, debo anular algunos métodos UIResponder
. Quiero manejar cada toque por separado, así que simplemente paso por los toques actualizados, pasando cada uno a un método auxiliar:
#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é los métodos privados de apariencia y diseño.
#pragma mark - Implementation details - appearance/layout
Primero haré el código de inicialización común. Quiero configurar el color de fondo en negro, ya que eso es parte de la atenuación de 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 casi de la misma manera, así que llamaré a otro método privado para crear la vista de imagen tenue y luego modificarla para que sea realmente tenue:
- (void)initDimImageView {
_dimImageView = [self newImageSubview];
_dimImageView.alpha = 0.5;
}
Llamaré al mismo método auxiliar para crear la vista brillante, luego agregaré su subcapa de máscara:
- (void)initBrightImageView {
_brightImageView = [self newImageSubview];
_mask = [CAShapeLayer layer];
_brightImageView.layer.mask = _mask;
}
El método auxiliar que crea ambas vistas de imagen 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 imagen oscura, solo necesito establecer su marco en mis límites:
- (void)layoutDimImageView {
_dimImageView.frame = self.bounds;
}
Para diseñar la vista de imagen brillante, necesito establecer su marco en mis límites, y debo actualizar la ruta de su capa de máscara para que sea la unión de las rutas de los focos 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 relleno (el valor predeterminado, kCAFillRuleNonZero
) para garantizar que los puntos incluidos repetidamente se incluyan en la máscara.
A continuación, toque manejo.
#pragma mark - Implementation details - touch handling
Cuando UIKit me envíe un nuevo toque, buscaré la ruta del foco individual que contiene el toque y adjuntaré la ruta al toque como un objeto asociado. Eso significa que necesito una clave de objeto asociada, que solo necesita ser una cosa privada de la que pueda tomar la dirección:
static char kSpotlightPathAssociatedObjectKey;
Aquí realmente encuentro el camino y lo adjunto al toque. Si el toque está fuera de cualquiera de mis caminos 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 un toque se ha movido, veo si el toque tiene un camino adjunto. Si es así, traduzco (deslizo) la ruta en la cantidad que el toque se ha movido desde la última vez que lo vi. Entonces me marqué para 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 se desvinculará automáticamente de la ruta adjunta (si la hay):
- (void)touchEnded:(UITouch *)touch {
// Nothing to do
}
Para encontrar la ruta que contiene un toque, simplemente recorro las rutas de los focos, preguntando a cada uno 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 demo completa a github .
Esto no es simple. Puedo conseguirte una buena parte del camino allí. Es la animación lo que es complicado. Aquí está la salida de algunos códigos que tiré juntos:
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 base a la entrada del usuario, pero será un poco desafiante. Observe las líneas donde agrego una ruta rectangular a la ruta del círculo y luego establezco 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 fuera, mostrarás el negro translúcido en el centro del círculo y luego nada en la parte exterior (si eso tiene sentido).
Tal vez intente jugar con este código un poco y vea si puede animarlo. Jugaré 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: Así que 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 la ventaja es que el círculo se puede animar con bastante facilidad.
Este construye una capa compuesta con la capa translúcida y la capa de círculo es hermanos dentro de una capa principal que se usa como máscara.
Agregué una animación básica a esta para que pudiéramos ver la capa de círculo 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"];
}
He estado luchando con este mismo problema y encontré 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 tuviera un efecto de degradado. El beneficio adicional de esta solución es que funciona con cualquier UIView y no solo con imágenes.
La primera subclase UIView
para UIView
todo excepto los marcos 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 reunir a estos dos y pasar los 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 agujeros rectangulares / circulares, lo que permite al usuario interactuar con las vistas detrás de la superposición. Es una implementación rápida de estrategias similares utilizadas en otras respuestas. Lo usé para crear este tutorial para una de nuestras aplicaciones:
La biblioteca se llama TAOverlayView y es de código abierto bajo Apache 2.0.
Nota: aún no he implementado agujeros móviles (a menos que mueva la superposición completa como en otras respuestas).