web-dev-qa-db-fra.com

Animer le chemin CAShapeLayer sur les limites animées change

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

Des questions

Cela fonctionne principalement, mais j'ai deux problèmes avec cela:

  1. Parfois, j'obtiens un plantage sur (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. Edit: tant pis. J'ai oublié de convertir UIBezierPath en CGPathRef. Merci @antonioviero !

  2. 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.

  3. 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.

D'autres choses que j'ai essayées

  • Remplacez le 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?

Projet

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.

Screenshot

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é.)

Mettre à jour

Jose Luis Piedrahita m'a pointé du doigt à cet article de Nick Lockwood , ce qui suggère l'approche suivante:

  1. 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").

  2. 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.

  3. 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. "

40
Ole Begemann

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)
    }

}
6
Andy

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

3
Oleksii Nezhyborets

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
2
k06a

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.

1