Această mini-serie din două părți vă va învăța cum să creați un efect impresionant de pliere a paginii cu Core Animation. În această tranșă, veți învăța mai întâi cum să creați o vizualizare a schemei de schițe și apoi cum să aplicați o animație de pliere a coloanei vertebrale în acea vizualizare. Citește mai departe!
Lucrul cu UIView
clasa este esențială pentru dezvoltarea SDK-ului iOS. Vizionările au atât un aspect vizual (adică ceea ce vedeți pe ecran), cât și un aspect de control (interacțiunea utilizatorului prin atingeri și gesturi). Aspectul vizual este de fapt tratat de o clasă care aparține Core Animation Framework, numită CALayer
(care, la rândul său, este pusă în aplicare prin OpenGL, doar nu ne pasă să mergem la acel nivel de abstractizare aici). Obiectele Layer sunt instanțe ale CALayer
clasa sau una dintre subclasele sale. Fără a trece prea adânc în teorie (acesta este, la urma urmei, ar trebui să fie un tutorial practic!), Trebuie să ținem cont de următoarele aspecte:
CALayer
conținutul este format dintr-un bitmap. În timp ce clasa de bază este destul de utilă așa cum este, ea are, de asemenea, mai multe subclase importante în Core Animation. În special, există CAShapeLayer
care ne permite să reprezentăm forme prin intermediul căilor vectoriale.Deci, ce putem realiza cu straturi pe care nu le putem face cu ușurință cu vederile direct? 3D, pentru unul, la care ne vom concentra aici. CALayer
capabilitățile 3D dau efecte 3D și animații destul de sofisticate la îndemână, fără a fi nevoiți să coborâți la nivelul OpenGL. Acesta este obiectivul din spatele acestui tutorial: demonstrarea unui efect 3D interesant care oferă un gust mic de ceea ce putem realiza CALayer
s.
Avem o aplicație de desen care permite unui utilizator să deseneze pe ecran cu degetul (pentru care voi reutiliza codul dintr-un tutorial anterior al meu). Dacă utilizatorul execută apoi un gest de pe panza de desen, acesta se îndoaie de-a lungul unei linii verticale care rulează de-a lungul centrului pânzei (la fel ca și coloana vertebrală a unei cărți). Cartea pliată ridică chiar și o umbră în fundal.
Partea de desen al aplicației pe care o împrumut nu este cu adevărat importantă aici și am putea folosi foarte bine orice imagine pentru a demonstra efectul de pliere. Cu toate acestea, efectul face o metaforă vizuală foarte frumoasă în contextul unei aplicații de desenare în care acțiunea de ciupire expune o carte cu mai multe frunze (care conțin desenele noastre anterioare) pe care le putem naviga. Această metaforă este văzută în special în aplicația Hârtie. În timp ce pentru scopurile tutorialului implementarea noastră va fi mai simplă și mai puțin sofisticată, dar nu este prea departe ... și, bineînțeles, puteți să luați ceea ce învățați în acest tutorial și să îl faceți mai bine!
Amintiți-vă că în sistemul de coordonate iOS originea se află în colțul din stânga sus al ecranului, axa x crescând spre dreapta și axa y în jos. Cadrul unei imagini descrie dreptunghiul său în sistemul de coordonate al superviziei. Straturile pot fi interogate și pentru cadrul lor, dar există un alt mod (preferat) de a descrie locația și dimensiunea unui strat. Vom motiva acestea cu un exemplu simplu: imaginați două straturi, A și B, ca bucăți rectangulare de hârtie. Ați dori să transformați stratul B într-un substrat A, astfel încât să fixați B deasupra A folosind un bolț, menținând laturile drepte în paralel. Pinul trece prin două puncte, unul în A și unul în B. Știind locația acestor două puncte ne oferă o modalitate eficientă de a descrie poziția lui B în raport cu A. Vom eticheta punctul în care pinul pierde în A " punctul de ancorare "și punctul din poziția B". Uitați-vă la următoarea figură, pentru care vom face matematica:
Cifra pare să se întâmple multe, dar să nu ne îngrijorăm, o vom examina puțin câte puțin:
Calculul este destul de simplu, așa că asigurați-vă că îl înțelegeți bine. Acesta vă va da o senzație bună despre relația dintre poziție, punct de ancorare și cadru.
Punctul de ancorare este destul de semnificativ, deoarece atunci când efectuăm transformări 3D pe strat, aceste transformări sunt efectuate în raport cu punctul de ancorare. Vom vorbi despre asta în continuare (și apoi veți vedea un cod, vă promit!).
Sunt sigur că sunteți familiarizat cu conceptul de transformări, cum ar fi scalarea, traducerea și rotația. În aplicația Fotografii din iOS 6, dacă prindeți două degete pentru a mări sau a micșora o fotografie din album, efectuați o transformare a scalei. Dacă faceți o mișcare de mișcare cu cele două degete, fotografia se rotește și, dacă o trageți în timp ce țineți părțile paralele, aceasta este traducerea. Core animație și CALayer
atu UIView
permițându-vă să efectuați transformări în 3D în loc de doar 2D. Desigur, în 2013, ecranele noastre iDevice sunt încă 2D, astfel încât transformările 3D folosesc o șmecherie geometrică pentru a ne păcăli ochii pentru a interpreta o imagine plat ca obiect 3D (procesul nu se deosebește decât imaginea unui obiect 3D într-un desen liniar realizat cu creion, într-adevăr). Pentru a face față celei de-a treia dimensiuni, trebuie să folosim o axă z pe care ne imaginăm să ne străpungem ecranul dispozitivului și să îl perpendicularizăm.
Punctul de ancorare este important deoarece rezultatul precis al aceleiași transformări aplicate va diferi de obicei în funcție de acesta. Acest lucru este deosebit de important - și cel mai ușor de înțeles - cu o rotație transformată prin același unghi aplicat cu privire la două puncte de ancorare diferite în dreptunghiul din figura de mai jos (punctul roșu și punctul albastru). Rețineți că rotația se află în planul imaginii (sau în jurul axei z, dacă preferați să vă gândiți astfel).
Deci, cum implementăm efectul de pliere pe care îl urmăm? În timp ce straturile sunt foarte cool, nu le puteți împături în mijloc! Soluția este - așa cum sunt sigur că ați dat seama - să folosiți două straturi, câte una pentru fiecare pagină de pe ambele părți ale foliei. Pe baza celor discutate anterior, să analizăm în prealabil proprietățile geometrice ale acestor două straturi:
1,0, 0,5
iar pentru al doilea strat este 0,0, 0,5
, în spațiile lor de coordonate. Asigurați-vă că confirmați acest lucru din figura înainte de a continua!lățime / 2, înălțime / 2
. Amintiți-vă că proprietatea poziției este în coordonate standard, nu este normalizată.lățime / 2, înălțime
. Acum știm destule pentru a scrie un cod!
Creați un nou proiect Xcode cu șablonul "Empty Application" și apelați-l LayerFunTut. Faceți-o o aplicație iPad și activați numărarea automată a numerelor de referință (ARC), dar dezactivați opțiunile pentru testele principale de date și unități. Salvați-l.
În pagina Target> Sumar care se afișează, derulați în jos până la "Orientările interfeței suportate" și alegeți cele două orientări peisaj.
Parcurgeți mai jos până când ajungeți la "Cadre și Biblioteci conectate", faceți clic pe "+" și adăugați cadrul central QuartzCore, care este necesar pentru Core Animation și CALayers.
Vom începe prin includerea aplicației noastre de desen în proiect. Creați o nouă clasă Obiectiv-C numită CanvasView
, făcându-i o subclasă de UIView
. Lipiți următorul cod CanvasView.h:
// // CanvasView.h // #import@interface CanvasView: UIView @property (nonatomic, puternic) UIImage * incrementalImage; @Sfârșit
Și apoi înăuntru CanvasView.m:
// // CanvasView.m // #import "CanvasView.h" @implementation CanvasView cale UIBezierPath *; Punctele CGPoint [5]; uint ctr; - (id) initWithCoder: (NSCoder *) aDecoder dacă (self = [super initWithCoder: aDecoder]) auto.backgroundColor = [UICcolor clearColor]; [self setMultipleTouchEnabled: NU]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 6.0]; întoarce-te; - (id) initWithFrame: Cadrul (CGRect) auto = [super initWithFrame: cadru]; dacă (auto) auto.backgroundColor = [UICcolor clearColor]; [self setMultipleTouchEnabled: NU]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 6.0]; întoarce-te; - (void) DraRect: (CGRect) rect [auto.incrementalImage drawInRect: rect]; [[UICcolor blueColor] setStroke]; [accident vascular cerebral]; - (void) atingeBegan: (NSSet *) atinge cuEvent: (UIEvent *) eveniment ctr = 0; UITouch * touch = [atinge oriceObject]; pts [0] = [atingeți locațiaInView: auto]; - (void) atingeModed: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; ctr ++; pct [ctr] = p; dacă (ctr == 4) pts [3] = CGPointMake ((pct. [2] .x + pts [4] .x) /2.0. ); [calea moveToPoint: pts [0]]; [cale addCurveToPoint: pct. [3] controlPoint1: pts [1] controlPoint2: pts [2]]; [self setNeedsDisplay]; pts [0] = puncte [3]; pts [1] = puncte [4]; ctr = 1; - (void) atingeEnded: (NSSet *) atinge cuEvent: (UIEvent *) eveniment [self drawBitmap]; [self setNeedsDisplay]; [path removeAllPoints]; ctr = 0; - (void) touchesCancelled: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment [touch touchesEnded: touches withEvent: event]; - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (auto.bounds.size, NO, 0.0); dacă (! self.incrementalImage) UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UICcolor clearColor] setFill]; [umplutură de rectatură]; [auto.incrementalImage drawAtPoint: CGPointZero]; [[UICcolor blueColor] setStroke]; [accident vascular cerebral]; self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext (); @Sfârșit
După cum am menționat anterior, acesta este doar codul de la un alt tutorial pe care l-am scris (cu unele modificări minore). Asigurați-vă că verificați dacă nu sunteți sigur cum funcționează codul. În scopul acestui tutorial, totuși, este important acest lucru CanvasView
permite utilizatorului să deseneze curse netede pe ecran. Am declarat o proprietate numită incrementalImage
care stochează o versiune bitmap a desenului utilizatorului. Aceasta este imaginea pe care o vom "împăca" cu CALayer
s.
Este timpul să scriem codul de control al vizualizării și să implementăm ideile pe care le-am elaborat anterior. Un lucru pe care nu l-am discutat este cum obținem imaginea desenată în noi CALayer
astfel încât jumătate din imagine să fie atrasă în partea stângă, iar cealaltă jumătate în pagina dreaptă. Din fericire, sunt doar câteva linii de cod, despre care voi vorbi mai târziu.
Creați o nouă clasă Obiectiv-C numită ViewController
, faceți o subclasă de UIViewController
, și nu verificați niciuna dintre opțiunile care apar.
Lipiți următorul cod ViewController.m
// // ViewController.m // #import "ViewController.h" #import "CanvasView.h" #import "QuartzCore / QuartzCore.h" #define D2R (x) (x * (M_PI / 180.0) pentru a converti gradele la radiani @ interfață ViewController () @end @implementation ViewController CALayer * leftPage; CALayer * rightPage; UIView * curtainView; - (void) loadView auto.view = [[CanvasView alin] initWithFrame: [[UIScreen mainScreen] applicationFrame]]; - (void) viewDidLoad [vizualizare superDidLoad]; auto.view.backgroundColor = [UICcolor blackColor]; - (void) vizualizareDidAppear: (BOOL) animat [super viewDidAppear: animat]; self.view.backgroundColor = [UICcolor whiteColor]; Dimensiunea CGSize = auto.view.bounds.size; leftPage = [strat CALayer]; dreaptaPage = [strat CALayer]; 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 = [UICcolor alb Culoare] .CGColor; rightPage.backgroundColor = [Culoare UICcolor alb] .CGColor; leftPage.borderWidth = 2.0; // marginile adăugate pentru moment, astfel încât să putem distinge vizual între paginile stânga și dreapta rightPage.borderWidth = 2.0; leftPage.borderColor = [UICcolor închisGrayColor] .CGColor; rightPage.borderColor = [UICcolor închisGrayColor] .CGColor; //leftPage.transform = makePerspectiveTransform (); // deconectați mai târziu //rightPage.transform = makePerspectiveTransform (); // deconectați mai târziu perdeaView = [[UIView alocare] initWithFrame: self.view.bounds]; curtainView.backgroundColor = [UICcolor scrollViewTexturedBackgroundColor]; [curtainView.layer addSublayer: leftPage]; [curtainView.layer addSublayer: rightPage]; UITapGestureRecognizer * foldTap = [[UITapGestureRecognizer alocare] initWithTarget: acțiune auto: @selector (fold :)]; [auto.view addGestureRecognizer: foldTap]; UITapGestureRecognizer * unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget: actiune auto: @selector (desfasurare :); unfoldTap.numberOfTouchesRequired = 2; [auto.view addGestureRecognizer: unfoldTap]; - (void) ori: (UITapGestureRecognizer *) gr // desenarea bitmapului "incrementalImage" în straturile noastre CGImageRef imgRef = ((CanvasView *) self.view) .incrementalImage.CGImage; leftPage.contents = (id id) imgRef; rightPage.contents = (id id) imgRef; leftPage.contentsRect = CGRectMake (0.0, 0.0, 0.5, 1.0); // acest dreptunghi reprezintă jumătatea stângă a imaginii rightPage.contentsRect = CGRectMake (0.5, 0.0, 0.5, 1.0); // acest dreptunghi reprezintă jumătatea dreaptă a imaginii 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) desfășurați: (UITapGestureRecognizer *) gr leftPage.transform = CATransform3DIdentity; rightPage.transform = CATransform3DIdentity; // leftPage.transform = makePerspectiveTransform (); // deconectați mai târziu // rightPage.transform = makePerspectiveTransform (); // deconectați mai târziu [curtainView removeFromSuperview]; // UNCOMMENT LATER: / * CATransform3D makePerspectiveTransform () CATransform3D transform = CATransform3DIdentity; transform.m34 = 1,0 / -2000; transformare întoarcere; */ @Sfârșit
Dacă ignorați codul comentat afară, puteți observa că stratul setat este exact așa cum am planificat mai sus.
Să discutăm scurt acest cod:
D2R ()
pentru a converti de la radiani la grade. O observație importantă este că funcțiile care se transformă în argumentul lor (cum ar fi CATransform3DScale
și CATransform3DRotate
) "lanț împreună", o transformare cu alta (valoarea curentă a proprietății de transformare a stratului). Alte funcții, cum ar fi CATransform3DMakeRotation
, CATransform3DMakeScale
, CATransform3DIdentity
construi doar matricea de transformare adecvată. CATransform3DIdentity
este "transformarea identității" pe care un strat o are atunci când o creați. Este analoagă cu numărul "1" într-o multiplicare prin aceea că aplicarea unei transformări de identitate pe un strat lasă transformarea sa neschimbată, la fel ca înmulțirea unui număr cu unul. CGImage
aici - și, de asemenea CGColor
mai devreme - în loc de UIImage
și UIColor
. Asta pentru ca CALayer
funcționează la un nivel mai jos decât UIKit și funcționează cu tipuri de date "opace" (care înseamnă, în general, să nu întrebi implementarea lor de bază!) care sunt definite în cadrul Core Graphics. Obiective-C clase ca UIColor
și UIImage
pot fi considerate wrapper-uri orientate obiect în jurul versiunilor CG mai primitive. Din fericire, multe obiecte UIKit expun tipul lor de bază CG ca proprietate. În AppDelegate.m fișier, înlocuiți tot codul cu următoarele (singurul lucru pe care l-am adăugat este să includeți fișierul antet ViewController și să faceți o ViewController
exemplu controlerul de vizualizare rădăcină):
// // AppDelegate.m // #import "AppDelegate.h" #import "ViewController.h" @implementation Cerere AppDelegate - (BOOL): aplicație (UIApplication *) didFinishLaunchingWithOptions: (NSDictionary *) launchOptions self.window = [UIWindow alocare] initWithFrame: [[UIScreen mainScreen] limite]]; self.window.rootViewController = [[ViewController alloc] init]; auto.window.backgroundColor = [UICcolor whiteColor]; [auto.window makeKeyAndVisible]; reveniți DA; @Sfârșit
Construiți proiectul și executați-l pe simulator sau pe dispozitiv. Scribble pe pânză pentru un pic, apoi atingeți pe ecran cu un singur deget pentru a declanșa acțiunea recunoașterii gestului (atingerea cu două degete face ca efectul 3D să se încheie și panza de desen să reapară).
Nu destul de efectul pentru care mergem! Ce se întâmplă?
Mai întâi, observați că paginile devin mai mici cu fiecare robinet, astfel încât problema nu se află la transformarea de scalare, numai cu rotația. Problema este că, deși rotația se întâmplă într-un spațiu (matematic) 3D, rezultatul este proiectat pe ecrane plane aproape în același fel în care un obiect 3D își aruncă umbra pe un perete. Pentru a transmite profunzimea, trebuie să folosim un fel de tac. Cel mai important tac este cel al perspectivei: un obiect mai aproape de ochii noștri pare mai mare decât unul mai departe. Umbrele sunt un alt semn grozav, iar noi le vom ajunge la scurt timp. Deci, cum încorporăm perspectiva în transformarea noastră?
Să vorbim puțin despre transformări. Ce sunt ei, într-adevăr? Din punct de vedere matematic, trebuie să știți că dacă reprezentăm punctele în forma noastră ca vectori matematici, atunci transformările geometrice, cum ar fi scala, rotația și traducerea, sunt reprezentate ca transformări de matrice. Ce înseamnă acest lucru este faptul că dacă luăm o matrice reprezentând o transformare și se înmulțește cu un vector care reprezintă un punct în forma noastră, atunci rezultatul multiplicării (de asemenea, un vector) reprezintă locul în care punctul sfârșește după transformare. Nu putem spune mult mai mult aici fără să vă scufundăm în teorie (ceea ce merită să învățați, dacă nu sunteți deja familiarizat cu aceasta - mai ales dacă intenționați să includeți efecte 3D reci în aplicațiile dvs.).
Cum rămâne cu codul? Anterior, am setat geometria stratului prin setarea lui punct de ancorare
, poziţie
, și hotar
. Ceea ce vedem pe ecran este geometria stratului după ce a fost transformată de ea transforma
proprietate. Rețineți apelurile funcționale care arată ca acestea layer.transform = // ...
. Aici plasăm transformarea, care la interior este doar a struct
reprezentând o matrice de 4 x 4 de valori în virgulă mobilă. De asemenea, rețineți că funcțiile CATransform3DScale
și CATransform3DRotate
luați transformarea curentă a stratului ca parametru. Asta pentru că putem Compune mai multe se transformă împreună (ceea ce înseamnă doar că le multiplicăm matricile), rezultatul fiind ca și cum ați efectuat aceste transformări unul câte unul. Rețineți că vorbim numai despre rezultatul final al transformării, nu despre modul în care Core Animation animă stratul!
Revenind la problema de perspectivă, trebuie să știm că există o valoare în matricea noastră de transformare pe care o putem schimba pentru a obține efectul de perspectivă pe care îl urmăm. Această valoare este un membru al structurii transformate, numită m34 (numerele indică poziția sa în matrice). Pentru a obține efectul dorit, trebuie să-l setăm la un număr mic, negativ.
Dezactivați cele două secțiuni comentate din ViewController.m fișierul ( CATransform3D makePerspectiveTransform ()
funcția și liniile leftPage.transform = makePerspectiveTransform (); rightPage.transform = makePerspectiveTransform ();
și construi din nou. De data aceasta, efectul 3D pare mai credibil.
De asemenea, rețineți că atunci când schimbăm proprietatea de transformare a CALayer
, afacerea vine cu o animație "liberă". Aceasta este ceea ce vrem aici - spre deosebire de stratul care trece prin transformare brusc - dar uneori nu este.
Bineînțeles, perspectiva merge doar la fel de departe, când exemplul nostru devine mai sofisticat, vom folosi și umbre! Am putea dori, de asemenea, să rotunjim colțurile "cărții" noastre și să ne mascheze straturile de pagină cu un CAShapeLayer
poate ajuta cu asta. În plus, am dori să folosim un gest de control pentru a controla plierea / derularea, astfel încât să fie mai interactivă. Toate acestea vor fi acoperite în partea a doua a acestui mini-serie tutorial.
Vă încurajez să experimentați codul, referindu-vă la documentația API și încercați să implementați în mod independent efectul dorit (s-ar putea să ajungeți chiar să faceți mai bine!).
Distrează-te cu tutorialul și mulțumesc pentru lectură!