framework - uikit ios
Cambio de tamaño circular(redondo) de UIView con AutoLayout... ¿cómo animar a cornerRadius durante la animación de cambio de tamaño? (4)
ACTUALIZACIÓN: si su destino de despliegue es iOS 11 o posterior:
A partir de iOS 11, UIKit animará cornerRadius
si lo actualiza dentro de un bloque de animación. Simplemente configure layer.cornerRadius
su vista en un bloque de animación UIView
, o (para manejar cambios de orientación de interfaz), layoutSubviews
en layoutSubviews
o viewDidLayoutSubviews
.
ORIGINAL: si su destino de despliegue es anterior a iOS 11:
Entonces quieres esto:
(Activé Debug> Slow Animations para que la suavidad sea más fácil de ver).
Demonios, no dude en saltarse este párrafo: esto resulta ser mucho más difícil de lo que debería ser, porque el iOS SDK no hace que los parámetros (duración, curva de tiempo) de la animación de autorrotación estén disponibles de una manera conveniente. Puede (creo) obtenerlos anulando -viewWillTransitionToSize:withTransitionCoordinator:
en su controlador de vista para llamar a -animateAlongsideTransition:completion:
en el coordinador de transición, y en la devolución de llamada que pase, obtenga la transitionDuration
y completionCurve
del UIViewControllerTransitionCoordinatorContext
. Y luego necesita pasar esa información a su CircleView
, que tiene que guardarla (¡ya que no se ha redimensionado todavía!) Y más tarde cuando recibe layoutSubviews
, puede usarla para crear una CABasicAnimation
para cornerRadius
con esas animaciones guardadas parámetros. Y no cree accidentalmente una animación cuando no se trata de un cambio de tamaño animado ... Fin del rant lateral.
Wow, eso suena como una tonelada de trabajo, y tienes que involucrar al controlador de vista. Aquí hay otro enfoque que está completamente implementado dentro de CircleView
. Funciona ahora (en iOS 9) pero no puedo garantizar que siempre funcione en el futuro, ya que hace dos suposiciones que teóricamente podrían ser incorrectas en el futuro.
Este es el enfoque: anular -actionForLayer:forKey:
en CircleView
para devolver una acción que, cuando se ejecuta, instala una animación para cornerRadius
.
Estas son las dos suposiciones:
-
bounds.origin
ybounds.size
obtienen animaciones separadas. (Esto es cierto ahora, pero es de suponer que un futuro iOS podría usar una única animación para losbounds
. Sería bastante fácil comprobar si hay animación debounds
si no se encuentra la animación debounds.size
). - La animación
bounds.size
se agrega a la capa antes de que Core Animation solicite la accióncornerRadius
.
Dadas estas suposiciones, cuando Core Animation solicita la acción cornerRadius
, podemos obtener la animación bounds.size
de la capa, copiarla y modificar la copia para animar a cornerRadius
. La copia tiene los mismos parámetros de animación que el original (a menos que los modifiquemos), por lo que tiene la duración y la curva de tiempo correctas.
Aquí está el comienzo de CircleView
:
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
Tenga en cuenta que los límites de la vista se configuran antes de que la vista reciba layoutSubviews
, y por lo tanto, antes de que actualicemos cornerRadius
. Esta es la razón por la cual la animación bounds.size
se instala antes de que se cornerRadius
animación cornerRadius
. Las animaciones de cada propiedad están instaladas dentro del setter de la propiedad.
Cuando establecemos cornerRadius
, Core Animation nos pide una CAAction
para ejecutarlo:
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
En el código anterior, si se nos solicita una acción para cornerRadius
, buscamos una CABasicAnimation
en bounds.size
. Si encontramos uno, lo copiamos, cambiamos la ruta clave a cornerRadius
y lo CAAction
en una CAAction
personalizada (de la clase Action
, que mostraré a continuación). También cornerRadius
el valor actual de la propiedad cornerRadius
, porque Core Animation llama a actionForLayer:forKey:
antes de actualizar la propiedad.
Después de actionForLayer:forKey:
returns, Core Animation actualiza la propiedad cornerRadius
de la capa. Luego ejecuta la acción enviándola runActionForKey:object:arguments:
El trabajo de la acción es instalar las animaciones que sean apropiadas. Aquí está la subclase personalizada de CAAction
, que he anidado dentro de CircleView
:
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
El runActionForKey:object:arguments:
establece las propiedades fromValue
y toValue
de la animación y luego agrega la animación a la capa. Hay una complicación: UIKit usa animaciones "aditivas", porque funcionan mejor si se inicia otra animación en una propiedad mientras se está ejecutando una animación anterior. Entonces nuestra acción verifica eso.
Si la animación es aditiva, establece fromValue
a la diferencia entre los radios de esquina nuevos y antiguos, y establece toValue
en cero. Como la propiedad cornerRadius
la capa ya se ha actualizado en el momento en que se ejecuta la animación, agregar eso fromValue
al comienzo de la animación hace que se vea como el radio de la esquina anterior, y agregar el valor toValue
de cero al final de la animación hace que se vea como el nuevo radio de la esquina.
Si la animación no es aditiva (lo que no ocurre si UIKit creó la animación, hasta donde yo sé), simplemente establece fromValue
y toValue
de la manera obvia.
Aquí está el archivo completo para su conveniencia:
import UIKit
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
Mi respuesta fue inspirada por esta respuesta de Simon .
Tengo una UIView subclase a la que podemos llamar CircleView
. CircleView establece automáticamente un radio de esquina a la mitad de su ancho para que sea un círculo.
El problema es que cuando se cambia el tamaño de "CircleView" por una restricción de AutoLayout ... por ejemplo en una rotación de dispositivo ... se distorsiona mal hasta que el cambio de tamaño tiene lugar porque la propiedad "cornerRadius" tiene que ponerse al día, y el sistema operativo solo envía un único cambio de "límites" en el marco de la vista.
Me preguntaba si alguien tenía una buena y clara estrategia para implementar "CircleView" de una manera que no se distorsione en tales casos, pero aún así enmascara su contenido a la forma de un círculo y permite que exista un borde alrededor de dicha UIView .
Esta respuesta se basa en la respuesta anterior de rob mayoff . Básicamente, lo implementé para nuestro proyecto y funcionó bien en el iPhone (iOS 9 y 10), pero el problema permaneció en el iPad (iOS 9 o 10).
Depurando, encontré que la declaración if:
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
siempre falló en el iPad. Parece que las animaciones están compuestas en una secuencia diferente en iPad que en iPhone. Mirando la respuesta original de Simon , parece que la secuencia ha cambiado antes. Así que combiné ambas respuestas dándome algo como esto:
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
let buildAction: (CABasicAnimation) -> Action = { boundsAnimation in
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
return buildAction(boundsAnimation)
} else if let boundsAnimation = self.action(for: layer, forKey: "bounds") as? CABasicAnimation {
return buildAction(boundsAnimation)
}
}
return super.action(for: layer, forKey: event)
}
Al combinar ambas respuestas, parece funcionar correctamente tanto en iPhone como en iPad con iOS 9 y 10. En realidad, no he probado más y no conozco lo suficiente sobre CoreAnimation para comprender completamente este cambio.
Estas respuestas de traducción suelen ir a Objective-c ==> Swift, pero en caso de que haya más obstinados autores de Objective-c, esta es la respuesta de @ Rob traducida ...
// see https://.com/a/35714554/294949
#import "RoundView.h"
@interface Action : NSObject<CAAction>
@property(strong,nonatomic) CABasicAnimation *pendingAnimation;
@property(assign,nonatomic) CGFloat priorCornerRadius;
@end
@implementation Action
- (void)runActionForKey:(NSString *)event object:(id)anObject
arguments:(nullable NSDictionary *)dict {
if ([anObject isKindOfClass:[CALayer self]]) {
CALayer *layer = (CALayer *)anObject;
if (self.pendingAnimation.isAdditive) {
self.pendingAnimation.fromValue = @(self.priorCornerRadius - layer.cornerRadius);
self.pendingAnimation.toValue = @(0);
} else {
self.pendingAnimation.fromValue = @(self.priorCornerRadius);
self.pendingAnimation.toValue = @(layer.cornerRadius);
}
[layer addAnimation:self.pendingAnimation forKey:@"cornerRadius"];
}
}
@end
@interface RoundView ()
@property(weak,nonatomic) UIImageView *imageView;
@end
@implementation RoundView
- (void)layoutSubviews {
[super layoutSubviews];
[self updateCornerRadius];
}
- (void)updateCornerRadius {
self.layer.cornerRadius = MIN(self.bounds.size.width, self.bounds.size.height)/2.0;
}
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if ([event isEqualToString:@"cornerRadius"]) {
CABasicAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds.size"];
CABasicAnimation *animation = [boundsAnimation copy];
animation.keyPath = @"cornerRadius";
Action *action = [[Action alloc] init];
action.pendingAnimation = animation;
action.priorCornerRadius = layer.cornerRadius;
return action;
}
return [super actionForLayer:layer forKey:event];;
}
@end
Sugeriría no usar un radio de esquina, sino usar CAShapeLayer como máscara para la capa de contenido de su vista.
Instalaría un arco CGPath de 360 ° lleno como la forma de la capa de forma y lo establecería como la máscara de la vista de su capa.
A continuación, puede animar una nueva transformación de escala para la capa de máscara o animar un cambio en el radio de la ruta. Ambos métodos deben permanecer redondos, aunque la transformación de la escala puede no proporcionarle una forma limpia a tamaños de píxel más pequeños.
El momento sería la parte difícil (lograr que la animación de la capa de máscara se realice en bloque con la animación de límites).