Core Animation sur iPhone
Article paru dans le magazine Programmez du mois de Février 2010.
Dans notre précédent article qui décrivait la plateforme de développement iPhone, nous avions survolé rapidement une des fonctionnalités du SDK iPhone : les animations. Je vous propose d’en faire cette fois un tour plus approfondi, cet article s’adresse donc à un public de développeurs avertis. Loin d’être un simple gadget elles apportent un réel plus pour l’utilisateur, mais attention les pièges sont nombreux et nous verrons comment s’en prémunir.
1. Qu’est-ce que Core Animation ?
Sur iPhone les animations font intégralement partie de l’expérience utilisateur, elles sont d’ailleurs très clairement mentionnées dans la référence Apple pour le design d’applications, à savoir le très complet « iPhone Human Interface Guidelines » (ce n’est pas pour rien qu’Apple est reconnu pour la qualité de ses interfaces graphiques).
Avant tout il s’agit d’un moyen pour communiquer des informations à l’utilisateur, en termes d’usabilité on parle de « feedback ». Par défaut dans un NavigationController l’écran s’anime de la gauche vers la droite pour montrer que l’on a navigué vers le détail. On peut l’observer par exemple dans les préférences. Pour autant ces effets graphiques doivent rester subtils et ne pas entraver le déroulement de l’application, il est en effet facile de succomber à l’effet « Star Wars » vu les nombreuses possibilités de cette bibliothèque.
Pour résumer simplement, Core Animation est une bibliothèque permettant d’effectuer des animations très simplement sans que le développeur ait à se préoccuper des détails techniques . Ainsi la gestion des threads ou bien la fréquence de rafraîchissement en fonction des ressources disponibles sont prises en charge de façon transparente.
C’est une technologie relativement récente, qui a fait son apparition avec Mac OS 10.5 et qui a été écrite à l’origine pour l’iPhone OS. Si l’on regarde d’un peu plus près on s’aperçoit d’ailleurs que le niveau d’intégration est très fort entre UIKit et Core Animation. UIView, la classe mère pour tous les contrôles graphiques contient une propriété layer de type CALayer et n’est donc qu’une surcouche à Core Animation. UIKit rajoute la gestion des interactions utilisateur, tandis que la composition des différentes couches qui composent une interface graphique et la possibilité de les animer les unes par rapport aux autres sont délégués à Core Animation.
2. Travaux pratiques : un effet génial
Core Animation permet de réaliser des animations complexes en seulement quelques lignes de code, pourtant l’API est très riche et la présenter sans exemple peut vite se révéler indigeste. Je vous propose donc de la mettre en œuvre et d’introduire les concepts au fur et à mesure.
Les exemples FrontRow ou CoverFlow étant déjà des classiques, intéressons nous à la nouvelle animation sur les listes Genius qui été introduite avec iTunes 9 et l’OS 3.1.2 : une mosaïque présente 4 illustrations d’albums qui se retournent successivement pendant que la pochette de l’album en cours de lecture s’agrandit pour occuper tout l’écran. L’animation ne dure même pas une seconde en tout mais l’effet est assez sympatique.
2.1 Bootstrap du projet
Pour commencer nous allons créer un nouveau projet « Genius » à partir du template « View based application » et modifier le ViewController pour créer les éléments graphiques que nous allons animer. Le but n’est pas de faire l’architecture projet la plus parfaite mais de faire court et se concentrer sur Core Animation.
Créez le projet, ajoutez quelques images dans les ressources(Img1.png, Img2.png etc.), puis modifiez la classe GeniusViewController comme suit :
GeniusViewController.h
#import <UIKit/UIKit.h>
@interface GeniusViewController : UIViewController {
UIButton *buttonView;
UIImageView *albumView;
}
@property (nonatomic, retain) UIButton *buttonView;
@property (nonatomic, retain) UIImageView *albumView;
@end
L’animation consiste à faire pivoter les différentes pochettes et en même temps faire apparaître au premier plan l’image principale. Nous avons donc défni cette image ainsi qu’un bouton qui va contenir la mosaïque de pochettes et qui répondra à l’événement UIControlEventTouchUpInside pour lancer l’animation.
(Note : pensez à rajouter la dépendance vers le framework QuartzCore dans votre projet Xcode)
GeniusViewController.m
#import "GeniusViewController.h"
#import <Quartzcore/QuartzCore.h>
@implementation GeniusViewController
@synthesize buttonView;
@synthesize albumView;
- (void)dealloc {
[buttonView release], buttonView = nil;
[albumView release], albumView = nil;
[super dealloc];
}
- (void)viewDidLoad {
// Create the main button
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 80, 300, 300)];
[button addTarget:self action:@selector(triggerGeniusAnimation) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
self.buttonView = button;
[button release];
// Add the mosaic in the button
CGFloat width = CGRectGetWidth(button.frame) / 2;
for (int i = 1; i < = 4; i++) {
CGRect imageFrame = CGRectMake(width * (i % 2), i < 2 ? 0 : width, width, width);
UIImageView *imageView = [[UIImageView alloc] initWithFrame:imageFrame];
[imageView setImage:[UIImage imageNamed:[NSString stringWithFormat:@"Img%d.png", i]]];
[buttonView addSubview:imageView];
[imageView release];
}
// Create the album view
albumView = [[UIImageView alloc] initWithFrame:CGRectZero];
[albumView setImage:[UIImage imageNamed:@"Imgfull.png"]];
albumView.contentMode = UIViewContentModeScaleAspectFit;
albumView.layer.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
[self.view addSubview:albumView];
}
- (void)triggerGeniusAnimation {
}
@end
Voici ce qui devrait être affiché si vous exécutez le projet, pour le moment il ne se passe pas grand chose mais nous avons tous les éléments en place pour la suite.
2.2 Un peu de code, beaucoup de théorie
Implémentons maintenant la méthode triggerGeniusAnimation comme suit pour créer la première animation, celle de l’image principale :
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"bounds"];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim.duration = 0.5;
anim.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 320, 460)];
anim.fillMode = kCAFillModeBoth;
anim.removedOnCompletion = NO;
[albumView.layer addAnimation:anim forKey:nil];
Intéressons nous au code précédent pour comprendre ce qui se passe exactement. Nous avons mentionné plus haut la propriété layer d’une vue, la classe CALayer est une des classes fondamentales de Core Animation. C’est ce qui définit une zone dessinée à l’écran ; elle possède en outre de nombreuses propriétés comme la position, la transparence, une bordure ou plus important : une liste de sous layers. L’interface graphique est ainsi composée d’un arbre de composants et chaque layer définit son système de coordonnées par rapport à son parent. Il existe plusieurs sous-classes spécialisées en fonction de leur contenu, par exemple CAEAGLLayer permet de rendre de l’OpenGL.
Ici nous avons défini une animation qui modifie la taille du layer de l’image principale pour lui faire occuper tout l’écran. La plupart des propriétés d’un layer sont susceptibles d’être animées, par exemple l’opacité ou la position mais il en existe plus d’une vingtaine. Il est également possible d’hériter d’une CALayer et de lui rajouter des propriétés animables.
Pour définir l’animation nous avons créé une instance de la classe CABasicAnimation qui permet de créer une animation où les étapes intermédiaires sont interpolées automatiquement. Il suffit donc de spécifier l’état de début et de fin et de laisser à Core Animation le soin de tout calculer à notre place. C’est exactement ce qu’il s’est passé ici :
- (1) L’animation porte sur la propriété bounds que nous avons spécifié à la création.
- La valeur de départ est la taille actuelle du layer.
- La valeur de fin d’animation est spécifiée via la propriété toValue (4), pour les connaisseurs on notera que Core Animation est basé sur la programmation KVC (Key Value Coding).
Deux propriétés sont un peu plus obscures et nécessitent quelques explications, à savoir fillMode et removedOnCompletion (5, 6). Dans Core Animation il faut distinguer les valeurs des propriétés d’un layer des valeurs qui sont affichées à l’écran, l’animation modifie l’apparence mais sans modifier réellement ces valeurs. Il est d’ailleurs possible de connaître l’état d’un layer via deux méthodes :
-(id)modelLayer
: renvoie une instance de layer représentant l’état de son modèle-(id)presentationLayer
: renvoie une approximation du layer tel qu’il est affiché
Une fois l’animation terminée le layer retourne à son état d’origine, ce qui peut surprendre. Comme nous voulons conserver l’état de fin nous avons donc défini que l’animation continue à être appliquée (removedOnCompletion) avec la valeur finale de l’animation (fillMode).
2.3 Animations en 3D
Implémentons le reste de l’animation : les quatre illustrations doivent se retourner les unes après les autres. Pour cela nous allons utiliser la propriété transform d’un CALayer qui permet d’appliquer une transformation de type rotation, translation ou échelle et ce sur les 3 axes X, Y, Z. Bien évidemment cette propriété est susceptible d’être animée.
Rajoutez le code ci-dessous à la fin de la méthode triggerGeniusAnimation :
// Flip albums
CGFloat timeOffset = 0;
for (CALayer *sublayer in buttonView.layer.sublayers) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
CATransform3D transform = CATransform3DMakeRotation(M_PI/2, 0, 1, 0);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.duration = 0.2;
animation.beginTime = CACurrentMediaTime() + timeOffset;
animation.fillMode = kCAFillModeBoth;
animation.removedOnCompletion = NO;
[sublayer addAnimation:animation forKey:nil];
timeOffset += 0.1;
}
Il suffit d’itérer sur les sous layers du bouton et d’appliquer une rotation de PI/2 sur l’axe Y, ce qui va donner la sensation que la pochette tourne jusqu’à être complètement masquée. Nous jouons également sur un délai d’exécution pour donner un effet « domino ».
2.4 Pour aller un peu plus loin
N’ayant pas la place de traiter complètement un si vaste sujet, voici deux pistes intéressantes à creuser si vous voulez en savoir plus sur Core Animation :
- Dans les deux exemple précédents nous avons toujours créé et ajouté un objet de type CAAnimation, ce mode déclaratif est appelé animations explicites. Core Animation permet un deuxième modèle de programmation appelé animations implicites où l’on associe une animation qui sera lancée pour toute modification d’une propriété (voir les méthodes
defaultActionForKey
etactionForKey
). - L’OS 3 a apporté à CALayer de nouvelles sous classes : CAGradientLayer, CAShapeLayer qui sont deux layers spécialisées dans le dessin de gradient et de path.
3. Ne pas griller son budget sur une animation
Les animations arrivent généralement tard sur un projet iPhone, il est courant de se concentrer d’abord sur le design et l’enchainement des écrans. Ne les négligez pas pour autant : c’est ce qui fait toute la richesse d’une application iPhone. Il vaut donc mieux anticiper le coût de développement et de débuggage.
Règle d’or : toujours tester sur le device (si possible les 3 générations) et le plus tôt possible. Le simulateur ne suffit absolument pas pour valider une animation et peut masquer un problème de performance ou pire un comportement totalement différent. Rien de pire que de se rendre compte après à la fin de la journée que l’animation ne passe pas du tout sur un iPhone.
Comment régler un problème de performance ? Tout d’abord le rendre visible, le mesurer et enfin le diagnostiquer. Les outils du SDK iPhone permettent heureusement tout cela.
Dans Xcode exécutez le projet que nous avons réalisé plus tôt avec les outils de diagnostic de performance activés en passant par l’option Run > Run with Performance Tool > Core Animation ce qui va nous permettre de mesurer précisément le nombre de FPS (images par seconde) que nous obtenons sur le device. Pour ce genre de test il est intéressant de répéter l’animation pour obtenir une mesure moyenne, on peut utiliser la propriété repeatCount d’une animation pour cela.
Sur l’iPhone 3GS de test, l’animation tourne déjà au maximum de FPS, c’est normal les transformations sont accélérées au niveau hardware et il y a peu d’éléments présents à l’écran. Même si en apparence tout est parfait, il se trouve que l’on peut quand même optimiser l’affichage. En cochant l’option « Color Blended Layers » l’iPhone se met à afficher des zones rouges et vertes :
Les zones rouges correspondent aux layers avec de la transparence et qui nécessitent un calcul plus complexe pour le rendu final. C’est évidemment à éviter au maximum pour avoir les meilleures performances possibles. Dans notre exemple, nous n’en avons pas besoin, il suffit donc de supprimer la composante alpha des images pour que tout devienne vert.
Une deuxième options d’Instruments permet de diagnostiquer une problème de performances. « Flash Updated Regions » montre en temps réel quelles sont les zones qui sont redessinées à l’écran en les affichant en jaune. Il est alors possible de détecter que certaines portions sont redessinées alors qu’elles ne devraient pas.
En résumé voici les trois conseils que nous avons présenté, ce n’est évidemment pas exhaustif mais devrait vous éviter quelques soucis :
- Tester au plus tôt sur le device
- Privilégier les vues opaques
- Minimiser les zones de redessin
Core Animation est une bibliothèque très complète et qui à permis à l’iPhone de se démarquer de la concurrence en proposant une expérience utilisateur riche et inédite. J’espère vous avoir fourni les notions nécessaires pour que vos applications en tirent également parti.
Ressources http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html http://developer.apple.com/iphone/library/documentation/userexperience/conceptual/mobilehig/Introduction/Introduction.html