Cette mini-série en deux parties vous apprendra à créer un effet de pliage de page impressionnant avec Core Animation. Dans cet article, vous allez d'abord apprendre à créer une vue de carnet de croquis, puis à appliquer une animation de base de pliage d'épine sur cette vue. Continuer à lire!
Travailler avec UIView
La classe est au cœur du développement du SDK iOS. Les vues ont à la fois un aspect visuel (c'est-à-dire ce que vous voyez à l'écran) et généralement un aspect de contrôle (interaction de l'utilisateur via des touches et des gestes). L’aspect visuel est en fait géré par une classe appartenant à Core Animation Framework, appelée CALayer
(qui à son tour est implémenté via OpenGL, seulement nous ne nous soucions pas de descendre à ce niveau d'abstraction ici). Les objets de calque sont des instances de CALayer
classe ou l'une de ses sous-classes. Sans approfondir la théorie (après tout, ce tutoriel est supposé être pratique!), Nous devons garder à l’esprit les points suivants:
CALayer
Le contenu de consiste en un bitmap. Bien que la classe de base soit assez utile, elle contient également plusieurs sous-classes importantes dans Core Animation. Notamment, il y a CAShapeLayer
ce qui nous permet de représenter des formes à l'aide de chemins vectoriels.Alors, que pouvons-nous atteindre avec des couches que nous ne pouvons pas facilement faire avec des vues directement? La 3D, par exemple, est ce sur quoi nous allons nous concentrer. CALayer
Les capacités de ce logiciel mettent à votre portée des effets et des animations 3D assez sophistiqués, sans avoir à descendre au niveau OpenGL. C’est l’objectif de ce didacticiel: démontrer un effet 3D intéressant qui donne un petit aperçu de ce que nous pouvons réaliser avec CALayer
s.
Nous avons une application de dessin qui permet à un utilisateur de dessiner sur l'écran avec son doigt (pour lequel je vais réutiliser le code d'un de mes précédents tutoriels). Si l'utilisateur effectue ensuite un geste de pincement sur la zone de dessin, il se replie le long d'une ligne verticale longeant le centre de la zone de dessin (un peu comme le dos d'un livre). Le livre plié jette même une ombre sur le fond.
La partie dessin de l'application que j'emprunte n'a pas vraiment d'importance, et nous pourrions très bien utiliser n'importe quelle image pour démontrer l'effet de repliement. Cependant, l'effet constitue une très belle métaphore visuelle dans le contexte d'une application de dessin où l'action de pincement expose un livre à plusieurs feuilles (contenant nos dessins précédents) que nous pouvons parcourir. Cette métaphore est notamment visible dans l'application Paper. Bien que pour les besoins du tutoriel, notre implémentation soit plus simple et moins sophistiquée, mais ce n’est pas si loin… et bien sûr, vous pouvez prendre ce que vous avez appris dans ce tutoriel et le rendre encore meilleur!
Rappelez-vous que dans le système de coordonnées iOS, l'origine se situe dans le coin supérieur gauche de l'écran, l'axe des abscisses augmentant vers la droite et l'axe des ordonnées vers le bas. Le cadre d'une vue décrit son rectangle dans le système de coordonnées de son aperçu. Les couches peuvent également être interrogées pour leur cadre, mais il existe un autre moyen (préféré) de décrire l'emplacement et la taille d'une couche. Nous allons les motiver avec un exemple simple: imaginez deux couches, A et B, sous la forme de morceaux de papier rectangulaires. Vous souhaitez faire de la couche B une sous-couche de A, vous devez donc fixer B au sommet de A en utilisant une épingle, en maintenant les côtés droits parallèles. La broche passe par deux points, l'un en A et l'autre en B. Connaître l'emplacement de ces deux points nous donne un moyen efficace de décrire la position de B par rapport à A. Nous nommerons le point que la goupille perce dans A le " le point d'ancrage "et le point en B" position ". Regardez la figure suivante, pour laquelle nous ferons le calcul:
La figure semble avoir beaucoup à faire, mais ne vous inquiétez pas, nous l'examinerons petit à petit:
Le calcul est assez simple, alors assurez-vous de bien le comprendre. Cela vous donnera une bonne idée de la relation entre la position, le point d’ancrage et le cadre..
Le point d'ancrage est assez important car lorsque nous effectuons des transformations 3D sur le calque, ces transformations sont effectuées par rapport au point d'ancrage. Nous en reparlerons ensuite (et ensuite vous verrez du code, c'est promis!).
Je suis sûr que vous connaissez le concept de transformation tel que la mise à l'échelle, la traduction et la rotation. Dans l'application Photos sous iOS 6, si vous pincez avec deux doigts pour effectuer un zoom avant ou arrière sur une photo de votre album, vous effectuez une transformation de la taille. Si vous faites un mouvement de rotation avec vos deux doigts, la photo pivote et si vous la faites glisser tout en gardant les côtés parallèles, vous obtenez une translation. Animation de base et CALayer
atout UIView
en vous permettant d'effectuer des transformations en 3D au lieu de simplement 2D. Bien sûr, en 2013, nos écrans iDevice sont toujours en 2D. Par conséquent, les transformations 3D utilisent une astuce géométrique pour tromper nos yeux et les amener à interpréter une image plate comme un objet 3D (le processus n’est pas différent de la représentation d’un objet 3D crayon, vraiment). Pour traiter la 3ème dimension, nous devons utiliser un axe z que nous imaginons percer l'écran de notre appareil et perpendiculairement à celui-ci..
Le point d'ancrage est important car le résultat précis de la même transformation appliquée diffère généralement en fonction de celui-ci. Ceci est particulièrement important - et plus facile à comprendre - avec une transformation de rotation suivant le même angle appliqué par rapport à deux points d'ancrage différents dans le rectangle de la figure ci-dessous (le point rouge et le point bleu). Notez que la rotation est dans le plan de l'image (ou autour de l'axe des z, si vous préférez penser de cette façon).
Alors, comment pouvons-nous mettre en œuvre l'effet de pliage que nous recherchons? Bien que les couches soient vraiment cool, vous ne pouvez pas les plier au milieu! La solution consiste, comme vous l'avez sûrement compris, à utiliser deux calques, un pour chaque page de chaque côté du pli. Sur la base de ce que nous avons discuté précédemment, définissons à l'avance les propriétés géométriques de ces deux couches:
1.0, 0.5
et pour la deuxième couche est 0.0, 0.5
, dans leurs espaces de coordonnées respectifs. Assurez-vous de confirmer cela d'après la figure avant de continuer!largeur / 2, hauteur / 2
. Rappelez-vous que la propriété de position est en coordonnées standard et non normalisée.largeur / 2, hauteur
. Nous en savons maintenant assez pour écrire du code!
Créez un nouveau projet Xcode avec le modèle "Application vide" et appelez-le. LayerFunTut. Faites-en une application iPad et activez le comptage automatique des références (ARC), mais désactivez les options pour les tests de données de base et les tests unitaires. Sauvegarde le.
Dans la page Cible> Résumé qui apparaît, faites défiler jusqu'à "Orientations d'interface prises en charge" et choisissez les deux orientations de paysage..
Faites défiler la liste jusqu'à "Structures et bibliothèques liées", cliquez sur "+" et ajoutez le cadre principal QuartzCore, indispensable pour Core Animation et CALayers..
Nous allons commencer par incorporer notre application de dessin dans le projet. Créez une nouvelle classe Objective-C appelée CanvasView
, en faisant une sous-classe de UIView
. Collez le code suivant dans CanvasView.h:
// // CanvasView.h // #import@interface CanvasView: UIView @property (nonatomic, strong) UIImage * incrementalImage; @fin
Et puis dans CanvasView.m:
// // CanvasView.m // #import "CanvasView.h" @implementation CanvasView UIBezierPath * path; Points CGPoint [5]; uint ctr; - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) self.backgroundColor = [UIColor clearColor]; [self setMultipleTouchEnabled: NO]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 6.0]; retourner soi-même; - (id) initWithFrame: (CGRect) frame self = [super initWithFrame: frame]; if (self) self.backgroundColor = [UIColor clearColor]; [self setMultipleTouchEnabled: NO]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 6.0]; retourner soi-même; - (void) drawRect: (CGRect) rect [self.incrementalImage drawInRect: rect]; [[UIColor blueColor] setStroke]; [accident vasculaire cérébral]; - (void) touchBegan: (NSSet *) touche withEvent: (UIEvent *) événement ctr = 0; UITouch * touch = [touche anyObject]; pts [0] = [touch locationInView: self]; - (void) toucheMoved: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; ctr ++; pts [ctr] = p; si (ctr == 4) pts [3] = CGPointMake ((pts [2] .x + pts [4] .x) /2.0, (pts [2] .y + pts [4] .y) /2.0 ) [path moveToPoint: pts [0]]; [chemin addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; [self setNeedsDisplay]; pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1; - (void) touchesEnded: (NSSet *) touche withEvent: (UIEvent *) event [self drawBitmap]; [self setNeedsDisplay]; [chemin removeAllPoints]; ctr = 0; - (void) touchesCancelled: (NSSet *) touche withEvent: (UIEvent *) event [auto touchesEnded: touches withEvent: event]; - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, NO, 0.0); if (! self.incrementalImage) UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor clearColor] setFill]; [remplissage rectpath]; [self.incrementalImage drawAtPoint: CGPointZero]; [[UIColor blueColor] setStroke]; [accident vasculaire cérébral]; self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext (); @fin
Comme mentionné précédemment, il ne s'agit que du code d'un autre tutoriel que j'ai écrit (avec quelques modifications mineures). Assurez-vous de vérifier si vous n'êtes pas sûr du fonctionnement du code. Pour les besoins de ce tutoriel, l’important est que CanvasView
permet à l'utilisateur de dessiner des traits lisses sur l'écran. Nous avons déclaré une propriété appelée incrementalImage
qui stocke une version bitmap du dessin de l'utilisateur. C'est l'image avec laquelle nous allons "plier" CALayer
s.
Il est temps d’écrire le code du contrôleur de vue et de mettre en œuvre les idées que nous avons élaborées précédemment. Une chose dont nous n’avons pas parlé est de savoir comment nous obtenons l’image dessinée dans notre CALayer
de sorte que la moitié de l'image soit dessinée dans la page de gauche et l'autre moitié dans la page de droite. Heureusement, ce ne sont que quelques lignes de code, dont je parlerai plus tard.
Créez une nouvelle classe Objective-C appelée ViewController
, en faire une sous-classe de UIViewController
, et ne cochez aucune des options qui apparaissent.
Collez le code suivant dans ViewController.m
// // ViewController.m // #import "ViewController.h" #import "CanvasView.h" #import "QuartzCore / QuartzCore.h" #define D2R (x) (x * (M_PI / 180.0)) // macro convertir des degrés en radians @interface ViewController () @end @implementation ViewController CALayer * leftPage; CALayer * rightPage; UIView * curtainView; - (void) loadView self.view = [[CanvasView alloc]] initWithFrame: [[UIScreen mainScreen] applicationFrame]]; - (void) viewDidLoad [super viewDidLoad]; self.view.backgroundColor = [UIColor blackColor]; - (void) viewDidAppear: (BOOL) animated [super viewDidAppear: animated]; self.view.backgroundColor = [UIColor whiteColor]; CGSize size = self.view.bounds.size; leftPage = [couche calque]; rightPage = [couche calque]; leftPage.anchorPoint = (CGPoint) 1.0, 0.5; rightPage.anchorPoint = (CGPoint) 0.0, 0.5; leftPage.position = (CGPoint) size.width / 2.0, size.height / 2.0; rightPage.position = (CGPoint) size.width / 2.0, size.height / 2.0; leftPage.bounds = (CGRect) 0, 0, size.width / 2.0, size.height; rightPage.bounds = (CGRect) 0, 0, size.width / 2.0, size.height; leftPage.backgroundColor = [Couleur blanche UIColor] .CGColor; rightPage.backgroundColor = [UIColor whiteColor] .CGColor; leftPage.borderWidth = 2.0; // bordures ajoutées pour le moment, nous pouvons ainsi distinguer visuellement les pages gauche et droite rightPage.borderWidth = 2.0; leftPage.borderColor = [UIColor darkGrayColor] .CGColor; rightPage.borderColor = [UIColor darkGrayColor] .CGColor; //leftPage.transform = makePerspectiveTransform (); // décommenter plus tard //rightPage.transform = makePerspectiveTransform (); // ne commente pas plus tard curtainView = [[UIView alloc]] initWithFrame: self.view.bounds]; curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor]; [curtainView.layer addSublayer: leftPage]; [curtainView.layer addSublayer: rightPage]; UITapGestureRecognizer * foldTap = [[UITapGestureRecognizer alloc] initWithTarget: action autonome: @selector (fold :)]; [auto.view addGestureRecognizer: foldTap]; UITapGestureRecognizer * unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget: action autonome: @selector (unfold :)]; unfoldTap.numberOfTouchesRequired = 2; [auto.view addGestureRecognizer: unfoldTap]; - (void) fold: (UITapGestureRecognizer *) gr // dessin du bitmap "incrementalImage" dans nos calques CGImageRef imgRef = ((CanvasView *) self.view) .incrementalImage.CGImage; leftPage.contents = (__bridge id) imgRef; rightPage.contents = (__bridge id) imgRef; leftPage.contentsRect = CGRectMake (0,0, 0,0, 0,5, 1,0); // ce rectangle représente la moitié gauche de l'image rightPage.contentsRect = CGRectMake (0.5, 0.0, 0.5, 1.0); // ce rectangle représente la moitié droite de l'image leftPage.transform = CATransform3DScale (leftPage.transform, 0.95, 0.95, 0.95); rightPage.transform = CATransform3DScale (rightPage.transform, 0.95, 0.95, 0.95); leftPage.transform = CATransform3DRotate (leftPage.transform, D2R (7.5), 0,0, 1,0, 0,0); rightPage.transform = CATransform3DRotate (rightPage.transform, D2R (-7,5), 0,0, 1,0, 0,0); [auto.view addSubview: curtainView]; - (void) unfold: (UITapGestureRecognizer *) gr leftPage.transform = CATransform3DIdentity; rightPage.transform = CATransform3DIdentity; // leftPage.transform = makePerspectiveTransform (); // commente plus tard // rightPage.transform = makePerspectiveTransform (); // commenter plus tard [curtainView removeFromSuperview]; // UNCOMMENT LATER: / * CATransform3D makePerspectiveTransform () CATransform3D transform = CATransform3DIdentity; transform.m34 = 1,0 / -2000; transformation de retour; */ @fin
En ignorant le code commenté pour le moment, vous pouvez voir que la configuration de la couche est exactement comme nous l'avions prévu ci-dessus.
Discutons brièvement de ce code:
D2R ()
convertir des radians en degrés. Une observation importante est que les fonctions qui prennent une transformation dans leur argument (comme CATransform3DScale
et CATransform3DRotate
) "chaîne ensemble" une transformation avec une autre (la valeur actuelle de la propriété de transformation de couche). D'autres fonctions, telles que CATransform3DMakeRotation
, CATransform3DMakeScale
, CATransform3DIdentity
il suffit de construire la matrice de transformation appropriée. CATransform3DIdentity
est la "transformation d'identité" d'une couche lorsque vous en créez une. Il est analogue au nombre "1" dans une multiplication, car appliquer une transformation d'identité à un calque laisse sa transformation inchangée, un peu comme multiplier un nombre par un.. CGImage
ici - et aussi CGColor
plus tôt - au lieu de UIImage
et UIColor
. Ceci est dû au fait CALayer
fonctionne à un niveau inférieur à UIKit et fonctionne avec des types de données "opaques" (ce qui signifie approximativement, ne vous interrogez pas sur leur implémentation sous-jacente!) qui sont définis dans le cadre Core Graphics. Des classes Objective-C comme UIColor
et UIImage
peuvent être considérés comme des wrappers orientés objet autour de leurs versions CG plus primitives. Par souci de commodité, de nombreux objets UIKit exposent leur type de CG sous-jacent en tant que propriété.. dans le AppDelegate.m fichier, remplacez tout le code par le suivant (la seule chose que nous avons ajoutée est d’inclure le fichier d’en-tête ViewController et de créer une ViewController
exemple le contrôleur de vue racine):
// // AppDelegate.m // #import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate - (BOOL) application: (UIApplication *) application didFinishLaunchingWithOptions: (NSDictionary *) launchOptions (self.window). [UIWindow alloc] initWithFrame: [[UIScreen mainScreen]]]; self.window.rootViewController = [[ViewController alloc] init]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; retourner OUI; @fin
Générez le projet et exécutez-le sur le simulateur ou sur votre appareil. Gribouillez un peu sur la toile, puis appuyez sur l'écran avec un seul doigt pour déclencher l'action de reconnaissance des gestes (appuyer avec deux doigts provoque la fin de l'effet 3D et la réapparition de la toile de dessin)..
ne pas assez l'effet que nous recherchons! Que se passe-t-il?
Tout d'abord, notez que les pages deviennent plus petites à chaque pression, le problème ne réside donc pas dans la transformation de mise à l'échelle, mais uniquement dans la rotation. Le problème est que, même si la rotation se produit dans un espace 3D (mathématique), le résultat est projeté sur nos écrans plats de la même manière qu’un objet 3D projette son ombre sur un mur. Pour transmettre de la profondeur, nous devons utiliser une sorte de repère. Le repère le plus important est celui de la perspective: un objet plus proche de nos yeux apparaît plus grand qu’un objet plus éloigné. Les ombres sont une autre bonne idée, et nous y reviendrons dans un instant. Alors, comment pouvons-nous intégrer la perspective dans notre transformation?
Parlons un peu des transformations en premier. Qu'est-ce qu'ils sont vraiment? Mathématiquement, vous devez savoir que si nous représentons les points de notre forme sous forme de vecteurs mathématiques, les transformations géométriques telles que l'échelle, la rotation et la translation sont représentées sous forme de transformations matricielles. Cela signifie que si nous prenons une matrice représentant une transformation et que nous multiplions par un vecteur représentant un point de notre forme, le résultat de la multiplication (également un vecteur) représente le lieu où ce point se termine après avoir été transformé. Nous ne pouvons pas en dire beaucoup plus sans plonger dans la théorie (ce qui vaut la peine d'être étudié, si vous ne l'êtes pas déjà, surtout si vous avez l'intention d'incorporer des effets 3D sympas dans vos applications!).
Qu'en est-il du code? Auparavant, nous définissions la géométrie de la couche en définissant sa point d'ancrage
, position
, et bornes
. Ce que nous voyons à l’écran est la géométrie de la couche après qu’elle a été transformée par sa transformer
propriété. Notez les appels de fonction qui ressemblent à layer.transform = //…
. Voilà où nous établissons la transformation, ce qui en interne est juste un struct
représentant une matrice 4 x 4 de valeurs en virgule flottante. Notez également que les fonctions CATransform3DScale
et CATransform3DRotate
prenez la transformation actuelle de la couche en tant que paramètre. C'est parce que nous pouvons composer plusieurs transformations ensemble (ce qui signifie simplement que nous multiplions leurs matrices ensemble), le résultat final étant comme si vous aviez effectué ces transformations une par une. Notez que nous ne parlons que du résultat final de la transformation, pas de la manière dont Core Animation anime le calque.!
Pour en revenir au problème de la perspective, nous devons savoir que notre matrice de transformation présente une valeur que nous pouvons modifier pour obtenir l’effet de perspective recherché. Cette valeur est un membre de la structure de transformation, appelée m34 (les nombres indiquent sa position dans la matrice). Pour obtenir l’effet souhaité, nous devons le définir sur un petit nombre négatif..
Décommentez les deux sections commentées de la ViewController.m fichier (le CATransform3D makePerspectiveTransform ()
fonction et les lignes leftPage.transform = makePerspectiveTransform (); rightPage.transform = makePerspectiveTransform ();
et construire à nouveau. Cette fois, l'effet 3D semble plus crédible.
Notez également que lorsque nous changeons la propriété de transformation d'un CALayer
, le contrat vient avec une animation "gratuite". C’est ce que nous voulons ici - par opposition à la couche subissant sa transformation brutalement - mais parfois ce n’est pas.
Bien sûr, la perspective ne va pas plus loin que lorsque notre exemple devient plus sophistiqué, nous allons aussi utiliser des ombres! Nous pourrions aussi vouloir arrondir les coins de notre "livre" et masquer nos couches de page avec un CAShapeLayer
peut aider avec ça. De plus, nous aimerions utiliser un geste de pincement pour contrôler le pliage / dépliage afin qu'il soit plus interactif. Tout cela sera couvert dans la deuxième partie de cette mini-série de tutoriels.
Je vous encourage à expérimenter le code, en vous référant à la documentation de l'API, et à essayer de mettre en œuvre l'effet souhaité de manière indépendante (vous pourriez même le faire mieux!).
Amusez-vous avec le tutoriel et merci d'avoir lu!