Questa miniserie in due parti ti insegnerà come creare un impressionante effetto di piegatura delle pagine con Core Animation. In questa puntata, imparerai innanzitutto come creare una vista del riquadro di schizzo e come applicare un'animazione basculante di base sulla vista. Continuare a leggere!
Lavorare con il UIView
la classe è fondamentale per lo sviluppo dell'SDK iOS. Le viste hanno sia un aspetto visivo (cioè quello che vedi sullo schermo) che di solito un aspetto di controllo (interazione dell'utente tramite tocchi e gesti). L'aspetto visivo è in realtà gestito da una classe appartenente al Core Animation Framework, chiamato CALayer
(che a sua volta è implementato tramite OpenGL, solo che non ci interessa passare al livello di astrazione qui). Gli oggetti di livello sono istanze di CALayer
classe o una delle sue sottoclassi. Senza scavare troppo in profondità nella teoria (questo, dopo tutto, dovrebbe essere un tutorial pratico!) Dovremmo tenere presenti i seguenti punti:
CALayer
Il contenuto è costituito da una bitmap. Mentre la classe base è abbastanza utile così com'è, ha anche diverse sottoclassi importanti in Core Animation. In particolare, c'è CAShapeLayer
che ci permette di rappresentare le forme attraverso percorsi vettoriali.Quindi, cosa possiamo ottenere con livelli che non possiamo facilmente fare con le viste direttamente? 3D, per esempio, su cui ci concentreremo qui. CALayer
Le sue capacità mettono a disposizione effetti 3D e animazioni piuttosto sofisticati, senza dover scendere al livello OpenGL. Questo è l'obiettivo alla base di questo tutorial: dimostrare un interessante effetto 3D che dà un piccolo assaggio di ciò che possiamo ottenere con CALayer
S.
Abbiamo un'app di disegno che consente all'utente di disegnare sullo schermo con il dito (per il quale riutilizzerò il codice di un precedente tutorial del mio). Se l'utente esegue un gesto di pizzicamento sulla tela del disegno, si ripiega lungo una linea verticale che corre lungo il centro della tela (molto simile al dorso di un libro). Il libro piegato getta persino un'ombra sullo sfondo.
La parte del disegno dell'app che sto prendendo in prestito non è molto importante qui, e potremmo benissimo usare qualsiasi immagine per dimostrare l'effetto pieghevole. Tuttavia, l'effetto rende una metafora visiva molto bella nel contesto di un'app di disegno in cui l'azione di pizzicamento espone un libro con diverse foglie (contenenti i nostri disegni precedenti) che possiamo sfogliare. Questa metafora è vista in modo particolare nell'app Carta. Mentre per gli scopi del tutorial la nostra implementazione sarà più semplice e meno sofisticata, ma non è troppo lontana ... e, naturalmente, puoi prendere ciò che impari in questo tutorial e renderlo ancora migliore!
Ricorda che nel sistema di coordinate iOS l'origine si trova nell'angolo in alto a sinistra dello schermo, con l'asse x crescente verso destra e l'asse y verso il basso. La cornice di una vista descrive il suo rettangolo nel sistema di coordinate della sua superview. I livelli possono essere interrogati anche per il loro frame, ma c'è un altro (preferito) modo di descrivere la posizione e le dimensioni di un layer. Li motiveremo con un semplice esempio: immagina due strati, A e B, come pezzi di carta rettangolari. Ti piacerebbe rendere il livello B un sottolivello di A, quindi aggiusti B in cima ad A usando un perno, mantenendo i lati diritti in parallelo. Il pin passa attraverso due punti, uno in A e uno in B. Conoscendo la posizione di questi due punti ci dà un modo efficace di descrivere la posizione di B rispetto a A. Etichetteremo il punto che il piercing punta in A " punto di ancoraggio "e il punto in B" posizione ". Dai un'occhiata alla figura seguente, per la quale faremo i conti:
Sembra che la figura abbia parecchie cose in corso, ma non preoccuparti, la esamineremo un po 'alla volta:
Il calcolo è abbastanza semplice, quindi assicurati di averlo compreso a fondo. Ti darà una buona sensazione della relazione tra posizione, punto di ancoraggio e cornice.
Il punto di ancoraggio è abbastanza significativo perché quando eseguiamo trasformazioni 3D sul livello, queste trasformazioni vengono eseguite rispetto al punto di ancoraggio. Ne parleremo dopo (e poi vedrai del codice, te lo prometto!).
Sono sicuro che tu abbia familiarità con il concetto di trasformazioni come ridimensionamento, traduzione e rotazione. Nell'app Foto in iOS 6, se pizzichi con due dita per ingrandire o ridurre una foto del tuo album, stai eseguendo una trasformazione di scala. Se fai un movimento con le tue due dita, la foto ruota e se la trascini tenendo i lati paralleli, questa è la traduzione. Animazione core e CALayer
briscola UIView
consentendo di eseguire trasformazioni in 3D anziché solo 2D. Naturalmente, nel 2013 i nostri schermi iDevice sono ancora in 2D, quindi le trasformazioni 3D impiegano alcuni trucchi geometrici per ingannare i nostri occhi nell'interpretazione di un'immagine piatta come un oggetto 3D (il processo non è diverso dalla raffigurazione di un oggetto 3D in un disegno a tratteggio realizzato con un matita, davvero). Per affrontare la 3a dimensione, dobbiamo usare un asse z che immaginiamo di perforare il nostro schermo del dispositivo e perpendicolare ad esso.
Il punto di ancoraggio è importante perché il risultato preciso della stessa trasformazione applicata di solito differisce a seconda di esso. Ciò è particolarmente importante - e più facilmente comprensibile - con una trasformazione di rotazione attraverso lo stesso angolo applicato rispetto a due diversi punti di ancoraggio nel rettangolo nella figura sottostante (il punto rosso e il punto blu). Nota che la rotazione è nel piano dell'immagine (o attorno all'asse z, se preferisci pensarla in questo modo).
Quindi, come possiamo implementare l'effetto di piegatura che stiamo cercando? Mentre i livelli sono davvero fantastici, non puoi piegarli a metà! La soluzione è - come sono sicuro tu abbia capito - di utilizzare due livelli, uno per ogni pagina su entrambi i lati della piega. Sulla base di ciò che abbiamo discusso in precedenza, analizziamo in anticipo le proprietà geometriche di questi due strati:
1.0, 0.5
e per il secondo strato lo è 0.0, 0.5
, nei loro rispettivi spazi di coordinate. Assicurati di confermarlo dalla figura prima di procedere!larghezza / 2, altezza / 2
. Ricorda che la proprietà position è in coordinate standard, non normalizzata.larghezza / 2, altezza
. Ora sappiamo abbastanza per scrivere del codice!
Crea un nuovo progetto Xcode con il modello "Empty Application" e chiamalo LayerFunTut. Falla diventare un'app per iPad e abilita il conteggio dei riferimenti automatico (ARC), ma disabilita le opzioni per i dati di base e i test delle unità. Salvarla.
Nella pagina Destinazione> Riepilogo visualizzata, scorri verso il basso fino a "Orientamenti dell'interfaccia supportati" e scegli i due orientamenti orizzontali.
Scorri verso il basso fino a "Linked Frameworks and Libraries", fai clic su "+" e aggiungi il framework QuartzCore, che è richiesto per Core Animation e CALayers.
Inizieremo incorporando la nostra app di disegno nel progetto. Crea una nuova classe Objective-C chiamata CanvasView
, rendendolo una sottoclasse di UIView
. Incolla il seguente codice in CanvasView.h:
// // CanvasView.h // #import@interface CanvasView: UIView @property (nonatomic, strong) UIImage * incrementalImage; @fine
E poi dentro CanvasView.m:
// // CanvasView.m // #import "CanvasView.h" @implementation CanvasView Percorso UIBezierPath *; Punti CGPoint [5]; uint ctr; - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) self.backgroundColor = [UIColor clearColor]; [self setMultipleTouchEnabled: NO]; percorso = [UIBezierPath bezierPath]; [percorso setLineWidth: 6.0]; return self; - (id) initWithFrame: (CGRect) frame self = [super initWithFrame: frame]; if (self) self.backgroundColor = [UIColor clearColor]; [self setMultipleTouchEnabled: NO]; percorso = [UIBezierPath bezierPath]; [percorso setLineWidth: 6.0]; return self; - (void) drawRect: (CGRect) rect [self.incrementalImage drawInRect: rect]; [[UIColor blueColor] setStroke]; [tratto del percorso]; - (void) touchesBegan: (NSSet *) tocca conEvent: (UIEvent *) event ctr = 0; UITouch * touch = [tocca anyObject]; pts [0] = [touch locationInView: self]; - (void) touchesMoved: (NSSet *) tocca conEvent: (UIEvent *) event UITouch * touch = [tocca anyObject]; CGPoint p = [touch locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 4) pts [3] = CGPointMake ((pts [2] .x + pts [4] .x) /2.0, (pts [2] .y + pts [4] .y) /2.0 ); [percorso moveToPoint: pts [0]]; [percorso addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; [self setNeedsDisplay]; pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1; - (void) touchesEnded: (NSSet *) tocca conEvent: (UIEvent *) event [self drawBitmap]; [self setNeedsDisplay]; [percorso removeAllPoints]; ctr = 0; - (void) touchesCancelled: (NSSet *) tocca conEvent: (UIEvent *) event [self touchesEnded: touch withEvent: event]; - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, NO, 0.0); if (! self.incrementalImage) UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor clearColor] setFill]; [riempimento rettale]; [self.incrementalImage drawAtPoint: CGPointZero]; [[UIColor blueColor] setStroke]; [tratto del percorso]; self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext (); @fine
Come accennato in precedenza, questo è solo il codice di un altro tutorial che ho scritto (con alcune modifiche minori). Assicurati di controllare se non sei sicuro di come funziona il codice. Per gli scopi di questo tutorial, tuttavia, ciò che è importante è quello CanvasView
consente all'utente di disegnare tratti morbidi sullo schermo. Abbiamo dichiarato una proprietà chiamata incrementalImage
che memorizza una versione bitmap del disegno dell'utente. Questa è l'immagine con cui "ci piegheremo" CALayer
S.
È tempo di scrivere il codice del controller di visualizzazione e implementare le idee che abbiamo elaborato in precedenza. Una cosa di cui non abbiamo discusso è come otteniamo l'immagine disegnata nella nostra CALayer
in modo tale che metà dell'immagine venga disegnata nella pagina sinistra e l'altra metà nella pagina destra. Fortunatamente, sono solo alcune righe di codice, di cui parlerò più tardi.
Crea una nuova classe Objective-C chiamata ViewController
, rendilo una sottoclasse di UIViewController
, e non selezionare nessuna delle opzioni visualizzate.
Incolla il seguente codice in ViewController.m
// // ViewController.m // #import "ViewController.h" #import "CanvasView.h" #import "QuartzCore / QuartzCore.h" #define D2R (x) (x * (M_PI / 180.0)) // macro per convertire i gradi in radianti @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) animato [super viewDidAppear: animato]; self.view.backgroundColor = [UIColor whiteColor]; CGSize size = self.view.bounds.size; leftPage = [CALayer layer]; rightPage = [CALayer layer]; 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 = [UIColor whiteColor] .CGColor; rightPage.backgroundColor = [UIColor whiteColor] .CGColor; leftPage.borderWidth = 2.0; // bordi aggiunti per ora, in modo che possiamo distinguere visivamente tra le pagine sinistra e destra rightPage.borderWidth = 2.0; leftPage.borderColor = [UIColor darkGrayColor] .CGColor; rightPage.borderColor = [UIColor darkGrayColor] .CGColor; //leftPage.transform = makePerspectiveTransform (); // uncomment tardi //rightPage.transform = makePerspectiveTransform (); // uncomment later curtainView = [[UIView alloc] initWithFrame: self.view.bounds]; curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor]; [curtainView.layer addSublayer: leftPage]; [curtainView.layer addSublayer: rightPage]; UITapGestureRecognizer * foldTap = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector (fold :)]; [self.view addGestureRecognizer: foldTap]; UITapGestureRecognizer * unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector (unfold :)]; unfoldTap.numberOfTouchesRequired = 2; [self.view addGestureRecognizer: unfoldTap]; - (void) fold: (UITapGestureRecognizer *) gr // disegna la bitmap "incrementalImage" nei nostri strati 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); // questo rettangolo rappresenta la metà sinistra dell'immagine rightPage.contentsRect = CGRectMake (0.5, 0.0, 0.5, 1.0); // questo rettangolo rappresenta la metà destra dell'immagine 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); [self.view addSubview: curtainView]; - (void) unfold: (UITapGestureRecognizer *) gr leftPage.transform = CATransform3DIdentity; rightPage.transform = CATransform3DIdentity; // leftPage.transform = makePerspectiveTransform (); // decomprimere più tardi // rightPage.transform = makePerspectiveTransform (); // decommenta più tardi [curtainView removeFromSuperview]; // UNCOMMENT LATER: / * CATransform3D makePerspectiveTransform () CATransform3D transform = CATransform3DIdentity; transform.m34 = 1.0 / -2000; ritorno alla trasformazione; */ @fine
Ignorando per ora il codice commentato, puoi vedere che il livello impostato è esattamente come pianificato in precedenza.
Discutiamo brevemente questo codice:
D2R ()
convertire da radianti in gradi. Un'osservazione importante è che le funzioni che prendono una trasformazione nel loro argomento (come CATransform3DScale
e CATransform3DRotate
) "concatena" una trasformazione con un'altra (il valore corrente della proprietà di trasformazione del livello). Altre funzioni, come CATransform3DMakeRotation
, CATransform3DMakeScale
, CATransform3DIdentity
basta costruire la matrice di trasformazione appropriata. CATransform3DIdentity
è la "trasformazione dell'identità" che ha un livello quando ne crei uno. È analogo al numero "1" in una moltiplicazione in cui applicare una trasformazione dell'identità a un livello lascia invariata la sua trasformazione, proprio come moltiplicare un numero di uno. CGImage
qui - e anche CGColor
prima - invece di UIImage
e UIColor
. Questo è perché CALayer
opera su un livello inferiore a UIKit e funziona con tipi di dati "opachi" (in parole povere, non chiedere informazioni sulla loro implementazione sottostante!) che sono definiti nel framework Core Graphics. Classi Objective-C come UIColor
e UIImage
possono essere pensati come involucri orientati agli oggetti attorno alle loro versioni CG più primitive. Per motivi di praticità, molti oggetti UIKit espongono il loro tipo CG sottostante come una proprietà. Nel AppDelegate.m file, sostituire tutto il codice con il seguente (l'unica cosa che abbiamo aggiunto è includere il file di intestazione ViewController e creare un ViewController
istanza il controller della vista radice):
// // AppDelegate.m // #import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate - (BOOL) application: (applicazione UIApplication *) didFinishLaunchingWithOptions: (NSDictionary *) launchOptions self.window = [ [UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bound]]; self.window.rootViewController = [[ViewController alloc] init]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; @fine
Costruisci il progetto ed eseguilo sul simulatore o sul tuo dispositivo. Scribble sulla tela per un po ', quindi tocca lo schermo con un dito per attivare l'azione di riconoscimento dei gesti (toccando con un dito l'effetto 3D termina e riappare la tela del disegno).
Non abbastanza l'effetto che stiamo facendo! Cosa sta succedendo?
Prima di tutto, nota che le pagine diventano più piccole con ogni singolo tocco, quindi il problema non dipende dalla trasformazione di ridimensionamento, ma solo dalla rotazione. Il problema è che anche se la rotazione avviene in uno spazio (matematico) 3D, il risultato viene proiettato sui nostri schermi piatti praticamente allo stesso modo in cui un oggetto 3D proietta la sua ombra su un muro. Per trasmettere profondità, abbiamo bisogno di usare una sorta di spunto. L'indizio più importante è quello della prospettiva: un oggetto più vicino ai nostri occhi appare più grande di uno più lontano. Le ombre sono un'altra grande stecca, e ci arriveremo presto. Quindi, come incorporiamo la prospettiva nella nostra trasformazione?
Parliamo un po 'delle trasformazioni prima. Cosa sono, davvero? Matematicamente parlando, dovresti sapere che se rappresentiamo i punti nella nostra forma come vettori matematici, le trasformazioni geometriche come la scala, la rotazione e la traduzione sono rappresentate come trasformazioni di matrice. Ciò significa che se prendiamo una matrice che rappresenta una trasformazione e moltiplichiamo con un vettore che rappresenta un punto nella nostra forma, allora il risultato della moltiplicazione (anche un vettore) rappresenta dove quel punto finisce dopo essere stato trasformato. Non possiamo dire molto di più qui senza immergerci nella teoria (che vale davvero la pena di conoscere, se non lo conosci già - soprattutto se intendi incorporare fantastici effetti 3D nelle tue app!).
E il codice? In precedenza, abbiamo impostato la geometria del livello impostandone la sua punto di ancoraggio
, posizione
, e misura
. Quello che vediamo sullo schermo è la geometria del livello dopo che è stata trasformata dal suo trasformare
proprietà. Nota le chiamate alla funzione che assomigliano layer.transform = // ...
. È lì che stiamo impostando la trasformazione, che internamente è solo una struct
che rappresenta una matrice 4 x 4 di valori in virgola mobile. Si noti inoltre che le funzioni CATransform3DScale
e CATransform3DRotate
prendere la trasformata corrente del livello come parametro. Questo perché possiamo comporre diverse trasformazioni insieme (il che significa solo che moltiplichiamo le loro matrici insieme), con il risultato finale di essere come se aveste eseguito queste trasformazioni una alla volta. Nota che stiamo parlando solo del risultato finale della trasformazione, non di come Core Animation anima il livello!
Tornando al problema della prospettiva, quello che dobbiamo sapere è che c'è un valore nella nostra matrice di trasformazione che possiamo modificare per ottenere l'effetto prospettico che stiamo cercando. Quel valore è un membro della struttura di trasformazione, chiamato m34 (i numeri indicano la sua posizione nella matrice). Per ottenere l'effetto che vogliamo, dobbiamo impostarlo su un numero piccolo e negativo.
Decommentare le due sezioni commentate in ViewController.m file (il CATransform3D makePerspectiveTransform ()
funzione e le linee leftPage.transform = makePerspectiveTransform (); rightPage.transform = makePerspectiveTransform ();
e costruisci di nuovo. Questa volta l'effetto 3D sembra più credibile.
Si noti inoltre che quando si modifica la proprietà di trasformazione di a CALayer
, l'accordo prevede un'animazione "gratuita". Questo è ciò che vogliamo qui - al contrario dello strato che subisce la sua trasformazione bruscamente - ma a volte non lo è.
Ovviamente, la prospettiva va solo oltre, quando il nostro esempio diventa più sofisticato, useremo anche le ombre! Potremmo anche voler girare gli angoli del nostro "libro" e mascherare i nostri livelli di pagina con a CAShapeLayer
può aiutare con quello. Inoltre, vorremmo usare un gesto di pizzicamento per controllare la piegatura / apertura in modo che si senta più interattivo. Tutto questo sarà trattato nella seconda parte di questa mini serie di tutorial.
Vi incoraggio a sperimentare con il codice, facendo riferimento alla documentazione dell'API, e cercare di implementare il nostro effetto desiderato in modo indipendente (potreste persino finire per farlo meglio!).
Divertiti con il tutorial e grazie per la lettura!