J'ai un CAShapeLayer (qui est la couche d'une sous-classe UIView) dont path
doit être mis à jour chaque fois que la taille bounds
de la vue change. Pour cela, j'ai remplacé la méthode setBounds:
De la couche pour réinitialiser le chemin:
@interface CustomShapeLayer : CAShapeLayer
@end
@implementation CustomShapeLayer
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.path = [[self shapeForBounds:bounds] CGPath];
}
Cela fonctionne bien jusqu'à ce que j'anime le changement de limites. J'aimerais que le chemin soit animé à côté de tout changement de limites animées (dans un bloc d'animation UIView) afin que le calque de forme adopte toujours à sa taille.
Étant donné que la propriété path
ne s'anime pas par défaut, j'ai trouvé ceci:
Remplacez la méthode addAnimation:forKey:
Du calque. Dans celui-ci, déterminez si une animation de limites est ajoutée au calque. Si c'est le cas, créez une deuxième animation explicite pour animer le chemin le long des limites en copiant toutes les propriétés de l'animation des limites qui est passée à la méthode. Dans du code:
- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key
{
[super addAnimation:animation forKey:key];
if ([animation isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *basicAnimation = (CABasicAnimation *)animation;
if ([basicAnimation.keyPath isEqualToString:@"bounds.size"]) {
CABasicAnimation *pathAnimation = [basicAnimation copy];
pathAnimation.keyPath = @"path";
// The path property has not been updated to the new value yet
pathAnimation.fromValue = (id)self.path;
// Compute the new value for path
pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath];
[self addAnimation:pathAnimation forKey:@"path"];
}
}
}
J'ai eu cette idée en lisant --- de David Rönnqvist View-Layer Synergy article . (Ce code est pour iOS 8. Sur iOS 7, il semble que vous devez vérifier le keyPath
de l'animation par rapport à @"bounds"
Et non à "@" bounds.size ".)
Le code appelant qui déclenche la modification des limites animées de la vue ressemblerait à ceci:
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customShapeView.bounds = newBounds;
} completion:nil];
Cela fonctionne principalement, mais j'ai deux problèmes avec cela:
Parfois, j'obtiens un plantage sur ( Edit: tant pis. J'ai oublié de convertir EXC_BAD_ACCESS
) Dans CGPathApply()
lors du déclenchement de cette animation alors qu'une animation précédente est toujours en cours. Je ne sais pas si cela a quelque chose à voir avec ma mise en œuvre particulière.UIBezierPath
en CGPathRef
. Merci @antonioviero !
Lorsque vous utilisez une animation de ressort UIView standard, l'animation des limites de la vue et du chemin du calque est légèrement désynchronisée. Autrement dit, l'animation de chemin fonctionne également de manière élastique, mais elle ne suit pas exactement les limites de la vue.
Plus généralement, est-ce la meilleure approche? Il semble que d'avoir une couche de forme dont le chemin dépend de la taille de ses limites et qui devrait s'animer en synchronisation avec le changement de limites est quelque chose qui devrait fonctionner, mais j'ai du mal. Je pense qu'il doit y avoir une meilleure façon.
actionForKey:
Du calque ou le actionForLayer:forKey:
De la vue afin de renvoyer un objet d'animation personnalisé pour la propriété path
. Je pense que ce serait le moyen préféré mais je n'ai pas trouvé de moyen d'obtenir les propriétés de transaction qui devraient être définies par le bloc d'animation englobant. Même s'il est appelé depuis l'intérieur d'un bloc d'animation, [CATransaction animationDuration
] Etc. renvoie toujours les valeurs par défaut.Existe-t-il un moyen pour (a) déterminer que vous êtes actuellement dans un bloc d'animation, et (b) pour obtenir les propriétés d'animation (durée, courbe d'animation, etc.) qui ont été définies dans ce bloquer?
Voici l'animation: le triangle orange est le chemin du calque de forme. Le contour noir est le cadre de la vue hébergeant la couche de forme.
Jetez un œil à l'exemple de projet sur GitHub . (Ce projet est pour iOS 8 et nécessite Xcode 6 pour s'exécuter, désolé.)
Jose Luis Piedrahita m'a pointé du doigt à cet article de Nick Lockwood , ce qui suggère l'approche suivante:
Remplacez la méthode actionForLayer:forKey:
Ou actionForKey:
De la vue et vérifiez si la clé transmise à cette méthode est celle que vous souhaitez animer (@"path"
).
Si c'est le cas, appeler super
sur l'une des propriétés animables "normales" du calque (comme @"bounds"
) Vous dira implicitement si vous êtes dans un bloc d'animation. Si la vue (le délégué du calque) renvoie un objet valide (et non nil
ou NSNull
), nous le sommes.
Définissez les paramètres (durée, fonction de synchronisation, etc.) pour l'animation de chemin à partir de l'animation renvoyée par [super actionForKey:]
Et renvoyez l'animation de chemin.
Cela fonctionne en effet très bien sous iOS 7.x. Cependant, sous iOS 8, l'objet renvoyé de actionForLayer:forKey:
N'est pas un standard (et documenté) CA...Animation
Mais une instance de la classe privée _UIViewAdditiveAnimationAction
(Un NSObject
sous-classe). Étant donné que les propriétés de cette classe ne sont pas documentées, je ne peux pas les utiliser facilement pour créer l'animation du chemin.
_Edit: Ou cela pourrait bien fonctionner après tout. Comme Nick l'a mentionné dans son article, certaines propriétés comme backgroundColor
renvoient toujours un CA...Animation
Standard sur iOS 8. Je vais devoir l'essayer. "
Je sais que c'est une vieille question, mais je peux vous fournir une solution qui semble similaire à l'approche de M. Lockwoods. Malheureusement, le code source ici est Swift donc vous devrez le convertir en ObjC.
Comme mentionné précédemment, si le calque est un calque de support pour une vue, vous pouvez intercepter les CAAction dans la vue elle-même. Cela n'est cependant pas pratique par exemple si la couche de support est utilisée dans plusieurs vues.
La bonne nouvelle est actionForLayer: forKey: appelle en fait actionForKey: dans la couche de support.
C'est dans l'actionForKey: dans la couche de support où nous pouvons intercepter ces appels et fournir une animation lorsque le chemin est modifié.
Un exemple de couche écrit en Swift est le suivant:
class AnimatedBackingLayer: CAShapeLayer
{
override var bounds: CGRect
{
didSet
{
if !CGRectIsEmpty(bounds)
{
path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath
}
}
}
override func actionForKey(event: String) -> CAAction?
{
if event == "path"
{
if let action = super.actionForKey("backgroundColor") as? CABasicAnimation
{
let animation = CABasicAnimation(keyPath: event)
animation.fromValue = path
// Copy values from existing action
animation.autoreverses = action.autoreverses
animation.beginTime = action.beginTime
animation.delegate = action.delegate
animation.duration = action.duration
animation.fillMode = action.fillMode
animation.repeatCount = action.repeatCount
animation.repeatDuration = action.repeatDuration
animation.speed = action.speed
animation.timingFunction = action.timingFunction
animation.timeOffset = action.timeOffset
return animation
}
}
return super.actionForKey(event)
}
}
Je pense que vous avez des problèmes parce que vous jouez avec le cadre d'un calque et son chemin en même temps.
J'irais simplement avec CustomView qui a drawRect personnalisé: qui dessine ce dont vous avez besoin, puis faites simplement
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customView.bounds = newBounds;
} completion:nil];
Il n'est pas du tout nécessaire d'utiliser des chemins
Voici ce que j'ai en utilisant cette approche https://dl.dropboxusercontent.com/u/73912254/triangle.mov
Solution trouvée pour animer path
de CAShapeLayer
tandis que bounds
est animé:
typedef UIBezierPath *(^PathGeneratorBlock)();
@interface AnimatedPathShapeLayer : CAShapeLayer
@property (copy, nonatomic) PathGeneratorBlock pathGenerator;
@end
@implementation AnimatedPathShapeLayer
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
if ([key rangeOfString:@"bounds.size"].location == 0) {
CAShapeLayer *presentationLayer = self.presentationLayer;
CABasicAnimation *pathAnim = [anim copy];
pathAnim.keyPath = @"path";
pathAnim.fromValue = (id)[presentationLayer path];
pathAnim.toValue = (id)self.pathGenerator().CGPath;
self.path = [presentationLayer path];
[super addAnimation:pathAnim forKey:@"path"];
}
[super addAnimation:anim forKey:key];
}
- (void)removeAnimationForKey:(NSString *)key {
if ([key rangeOfString:@"bounds.size"].location == 0) {
[super removeAnimationForKey:@"path"];
}
[super removeAnimationForKey:key];
}
@end
//
@interface ShapeLayeredView : UIView
@property (strong, nonatomic) AnimatedPathShapeLayer *layer;
@end
@implementation ShapeLayeredView
@dynamic layer;
+ (Class)layerClass {
return [AnimatedPathShapeLayer class];
}
- (instancetype)initWithGenerator:(PathGeneratorBlock)pathGenerator {
self = [super init];
if (self) {
self.layer.pathGenerator = pathGenerator;
}
return self;
}
@end
Je pense qu'il est désynchronisé entre les limites et l'animation de chemin parce que la fonction de synchronisation différente entre UIVIew spring et CABasicAnimation.
Vous pouvez peut-être essayer d'animer la transformation à la place, il devrait également transformer le chemin (non testé), après avoir terminé l'animation, vous pouvez ensuite définir la limite.
Une autre façon possible est de prendre un instantané du chemin, de le définir comme contenu du calque, puis d'animer la limite, le contenu doit ensuite suivre l'animation.