Introducere

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 afla pașii necesari pentru a lustrui pliul de pagină creat anterior și veți realiza o experiență de pliere interactivă mai interactivă utilizând gesturi de fixare.


Introducere

În prima parte a tutorialului din două părți, am luat o aplicație existentă de desenare liberă care a permis utilizatorului să schițeze pe ecran cu degetul și am implementat un efect de pliere 3D pe panza de desen care a dat impresia vizuală a unei cărți împăturită de-a lungul coloanei vertebrale. Am realizat acest lucru folosind CALayers și transformări. Am conectat efectul la un gest de robinet care a determinat ca plierea să se întâmple în trepte, animând fără probleme între o treaptă și următoarea.

În această parte a tutorialului, vom examina ameliorarea aspectelor vizuale ale efectului (prin adăugarea de colțuri curbate către paginile noastre și lăsarea paginilor noastre aruncă o umbră pe fundal), precum și făcând o pliere-desfășurare mai interactivă , permițându-i utilizatorului să-l controleze cu un gest de strângere. Pe parcurs, vom învăța despre câteva lucruri: a CALayer subclasa numită CAShapeLayer, cum să maschezi un strat pentru a da o formă nerectuală, cum să activezi un strat pentru a arunca o umbră, cum să configurezi proprietățile umbrei și un pic mai mult despre animația implicită a stratului și cum să faci acele animații să fie plăcute când adaugăm interacțiunea utilizatorului în mix.

Punctul de plecare pentru această parte va fi proiectul Xcode pe care l-am încheiat la sfârșitul primului tutorial. Să mergem!


Modelarea colțurilor paginii

Rețineți că fiecare din paginile stânga și dreapta a fost o instanță de CALayer. Ele aveau forma dreptunghiulară, așezate deasupra jumătăților drepte și drepte ale panzei, care se așezară de-a lungul liniei verticale care trece prin centru. Am desenat conținutul (adică schița liberă a utilizatorului) a jumătăților stângi și drepte ale panzei în aceste două pagini.

Chiar dacă a CAlayer când creația începe cu limite dreptunghiulare (cum ar fi UIViews), unul dintre lucrurile reale pe care le putem face straturilor este de a le forma forma conform unei "masti", astfel incat acestea sa nu mai fie limitate la a fi dreptunghiulare! Cum este definită această mască? CALayers au a masca proprietate care este a CALayer al cărui canal alfa al conținutului descrie masca care trebuie utilizată. Dacă folosim o mască "moale" (canalul alfa are valori fracționare), putem face stratul parțial transparent. Dacă folosim o mască "tare" (adică cu valori alfa zero sau una), putem "fixa" stratul astfel încât să atingă o formă bine definită a alegerii noastre.

Am putea folosi o imagine externă pentru a defini masca noastră. Cu toate acestea, deoarece masca noastră are o formă specifică (dreptunghi cu unele colțuri rotunjite), există o modalitate mai bună de ao face în cod. Pentru a specifica o formă pentru masca noastră, folosim o subclasă de CALayer denumit CAShapeLayer. CAShapeLayers sunt straturi care pot avea orice formă definită de o cale vectorială de tip Opac Core Graphics CGPathRef. Putem crea direct această cale utilizând Core API Core Graphics, sau - mai convenabil - putem crea un UIBezierPath obiecte cu cadrul UIKit Obiectiv-C. UIBezierPath expune subiacentul CGPathRef obiect prin intermediul lui CGPath proprietate care poate fi atribuită noastră CAShapeLayer„s cale proprietate, și acest strat de formă poate fi, la rândul său, atribuit să fie al nostru CALayero mască. Din fericire pentru noi, UIBezierPath pot fi inițializate cu multe forme predefinite interesante, incluzând un dreptunghi cu colțuri rotunjite (unde alegem colțul (coloanele) la care să rotundă).

Adăugați următorul cod, după, de exemplu, linia rightPage.transform = makePerspectiveTransform (); în ViewController.m viewDidAppear: metodă:

 // colțuri rotunjite UIBezierPath * leftPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: leftPage.bounds byRoundingCorners: UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii: CGSizeMake (25., 25.0)]; UIBezierPath * rightPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: rightPage.bounds byRoundingCorners: UIRectCornerTopRight | UIRectCornerBottomRight cornerRadii: CGSizeMake (25.0, 25.0)]; CAShapeLayer * leftPageRoundedCornersMask = [stratul CAShapeLayer]; CAShapeLayer * rightPageRoundedCornersMask = [stratul CAShapeLayer]; leftPageRoundedCornersMask.frame = leftPage.bounds; rightPageRoundedCornersMask.frame = rightPage.bounds; leftPageRoundedCornersMask.path = leftPageRoundedCornersPath.CGPath; rightPageRoundedCornersMask.path = rightPageRoundedCornersPath.CGPath; leftPage.mask = leftPageRoundedCornersMask; rightPage.mask = rightPageRoundedCornersMask;

Codul trebuie să fie explicativ. Traseul bezier este în formă de dreptunghi cu aceeași dimensiune ca stratul care este mascat și are colțurile corespunzătoare rotunjite (partea stângă sus și partea stângă jos pentru pagina stângă, dreapta sus și dreapta spre dreapta pentru pagina corectă).

Construiți proiectul și executați. Colțurile paginii ar trebui rotunjite acum ... cool!


Aplicarea unei umbre

Aplicarea unei umbre este, de asemenea, ușoară, dar există o problemă când vrem o umbră după ce aplicăm o mască (așa cum am făcut-o). Vom face acest lucru în timp util!

Un CALayer are a shadowPath care este o (ați ghicit-o) CGPathRef și definește forma umbrei. O umbră are mai multe proprietăți pe care le putem stabili: culoarea, decalajul (în esență, cât și cât de departe cade din strat), raza sa (specificând amploarea și neclaritatea) și opacitatea.

De fapt, trebuie menționat că nu este absolut esențial să setați shadowPath deoarece sistemul de desen va lucra la umbra din canalul alfa al compozit al stratului. Cu toate acestea, acest lucru este mai puțin eficient și va cauza, de obicei, performanțele să sufere, deci este întotdeauna recomandat să setați shadowPath dacă este posibil.

Introduceți următorul bloc de cod imediat după cel pe care tocmai l-am adăugat:

 leftPage.shadowPath = [UIBezierPath bezierPathWithRect: leftPage.bounds] .CGPath; rightPage.shadowPath = [UIBezierPath bezierPathWithRect: rightPage.bounds] .CGPath; leftPage.shadowRadius = 100.0; leftPage.shadowColor = [UICcolor negruColor] .CGColor; leftPage.shadowOpacity = 0.9; rightPage.shadowRadius = 100.0; rightPage.shadowColor = [UICcolor negruColor] .CGColor; rightPage.shadowOpacity = 0,9;

Construiți și executați codul. Din păcate, nu va fi aruncată nici o umbră, iar ieșirea va arăta exact așa cum a procedat anterior. Ce-i cu aia?!

Pentru a înțelege ce se întâmplă, scrieți primul bloc de cod pe care l-am scris în acest tutorial, dar lăsați codul de umbră în loc. Acum construiește și fugi. OK, am pierdut colțurile rotunjite, dar acum straturile noastre au aruncat o umbra nebuloasă pe vederea din spatele lor, mărind simțul profunzimii.

Ce se întâmplă este atunci când setăm a CALayerproprietatea mască, regiunea de redare a stratului este tăiată în regiunea mască. Prin urmare, umbrele (care sunt în mod natural îndepărtate de la strat) nu sunt redate și, prin urmare, nu apar.

Înainte de a încerca să rezolvăm această problemă, rețineți că umbra pentru pagina potrivită a fost difuzată în partea superioară a paginii din stânga. Acest lucru se datorează faptului că leftPage a fost adăugat la vizualizare înainte rightPage, prin urmare, primul este în mod efectiv "în spatele" acestuia din urmă în ordinea desenului (chiar dacă ambele sunt straturi de frate). Pe lângă schimbarea ordinii în care au fost adăugate cele două straturi super stratului, am putea schimba ordinea zPosition proprietatea straturilor pentru a specifica în mod explicit ordinea desenului, atribuind o valoare float mai mică stratului pe care am vrut să-l tragem mai întâi. Ne-am fi angajat într-o implementare mai complexă dacă vrem să evităm acest efect cu totul, dar din moment ce (cu curaj) îi dă un efect frumos umbrit paginii noastre, suntem mulțumiți de lucruri așa cum sunt!


Obținerea ambelor umbre și o formă mascată

Pentru a rezolva această problemă, vom folosi două straturi pentru a reprezenta fiecare pagină, una pentru a genera umbre și cealaltă pentru a afișa conținutul desenat într-o regiune profilată. În ceea ce privește heirarchia, vom adăuga straturile de umbră ca substrat direct în stratul de fundal. Apoi vom adăuga straturile de conținut în straturile de umbră. Prin urmare, straturile de umbră se vor dubla ca "containere" pentru stratul de conținut. Toate transformările noastre geometrice (de a face cu efectul de cotitură a paginii) vor fi aplicate straturilor de umbră. Deoarece straturile de conținut vor fi redate în raport cu containerele lor, nu va trebui să le aplicăm transformări.

După ce ați înțeles acest lucru, scrierea codului este relativ simplă. Dar din cauza tuturor schimbărilor pe care le facem, va fi dezordonat să modificăm codul anterior, așadar vă sugerăm să îl înlocuiți toate codul din viewDidAppear: cu urmatoarele:

 - (vid) viewDidAppear: (BOOL) animat [super viewDidAppear: animat]; self.view.backgroundColor = [UICcolor whiteColor]; leftPageShadowLayer = [stratul CAShapeLayer]; rightPageShadowLayer = [stratul CAShapeLayer]; leftPageShadowLayer.anchorPoint = CGPointMake (1.0, 0.5); rightPageShadowLayer.anchorPoint = CGPointMake (0,0, 0,5); leftPageShadowLayer.position = CGPointMake (auto.view.bounds.size.width / 2, self.view.bounds.size.height / 2); rightPageShadowLayer.position = CGPointMake (auto.view.bounds.size.width / 2, self.view.bounds.size.height / 2); leftPageShadowLayer.bounds = CGRectMake (0, 0, auto.view.bounds.size.width / 2, self.view.bounds.size.height); rightPageShadowLayer.bounds = CGRectMake (0, 0, auto.view.bounds.size.width / 2, self.view.bounds.size.height); UIBezierPath * leftPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: leftPageShadowLayer.bounds byRoundingCorners: UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii: CGSizeMake (25., 25.0)]; UIBezierPath * rightPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: rightPageShadowLayer.bounds deRoundingCorners: UIRectCornerTopRight | UIRectCornerBottomRight cornerRadii: CGSizeMake (25.0, 25.0)]; leftPageShadowLayer.shadowPath = leftPageRoundedCornersPath.CGPath; rightPageShadowLayer.shadowPath = rightPageRoundedCornersPath.CGPath; leftPageShadowLayer.shadowColor = [UICcolor negruColor] .CGColor; leftPageShadowLayer.shadowRadius = 100.0; leftPageShadowLayer.shadowOpacity = 0.9; rightPageShadowLayer.shadowColor = [UICcolor negruColor] .CGColor; rightPageShadowLayer.shadowRadius = 100; rightPageShadowLayer.shadowOpacity = 0,9; leftPage = [strat CALayer]; dreaptaPage = [strat CALayer]; leftPage.frame = leftPageShadowLayer.bounds; rightPage.frame = rightPageShadowLayer.bounds; leftPage.backgroundColor = [UICcolor alb Culoare] .CGColor; rightPage.backgroundColor = [Culoare UICcolor alb] .CGColor; leftPage.borderColor = [UICcolor închisGrayColor] .CGColor; rightPage.borderColor = [UICcolor închisGrayColor] .CGColor; leftPage.transform = makePerspectiveTransform (); rightPage.transform = makePerspectiveTransform (); CAShapeLayer * leftPageRoundedCornersMask = [stratul CAShapeLayer]; CAShapeLayer * rightPageRoundedCornersMask = [stratul CAShapeLayer]; leftPageRoundedCornersMask.frame = leftPage.bounds; rightPageRoundedCornersMask.frame = rightPage.bounds; leftPageRoundedCornersMask.path = leftPageRoundedCornersPath.CGPath; rightPageRoundedCornersMask.path = rightPageRoundedCornersPath.CGPath; leftPage.mask = leftPageRoundedCornersMask; rightPage.mask = rightPageRoundedCornersMask; leftPageShadowLayer.transform = makePerspectiveTransform (); rightPageShadowLayer.transform = makePerspectiveTransform (); curtainView = [[UIView alocare] initWithFrame: self.view.bounds]; curtainView.backgroundColor = [UICcolor scrollViewTexturedBackgroundColor]; [curtainView.layer addSublayer: leftPageShadowLayer]; [curtainView.layer addSublayer: rightPageShadowLayer]; [leftPageShadowLayer adăugațiSublayer: leftPage]; [dreaptaPageShadowLayer adăugaSublayer: 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]; 

De asemenea, trebuie să adăugați cele două variabile de instanță corespunzătoare celor două straturi de umbră. Modificați codul la începutul @implementation secțiune pentru a citi:

 @implementation ViewController CALayer * leftPage; CALayer * rightPage; UIView * curtainView; CAShapeLayer * leftPageShadowLayer; CAShapeLayer * rightPageShadowLayer; 

Pe baza discuției noastre anterioare, ar trebui să găsiți codul simplu de urmat.

Amintiți-vă că am menționat anterior că straturile care conțin conținutul desenat vor fi incluse ca substraturi în stratul generator de umbre. Prin urmare, trebuie să ne modificăm ori: și se desfășoară: metode pentru a efectua transformarea necesară pe straturile de umbră.

 - (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 leftPageShadowLayer.transform = CATransform3DScale (leftPageShadowLayer.transform, 0.95, 0.95, 0.95); rightPageShadowLayer.transform = CATransform3Scale (dreaptaPageShadowLayer.transform, 0,95, 0,95, 0,95); leftPageShadowLayer.transform = CATransform3DRotate (leftPageShadowLayer.transform, D2R (7,5), 0,0, 1,0, 0,0); rightPageShadowLayer.transform = CATransform3DRotate (dreaptaPageShadowLayer.transform, D2R (-7.5), 0.0, 1.0, 0.0); [self.view addSubview: curtainView];  - (void) desfășurați: (UITapGestureRecognizer *) gr leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; leftPageShadowLayer.transform = makePerspectiveTransform (); // deconectați mai târziu rightPageShadowLayer.transform = makePerspectiveTransform (); // deconectați mai târziu [curtainView removeFromSuperview]; 

Construiți și rulați aplicația pentru a verifica paginile noastre atât cu colțuri rotunjite, cât și cu umbre!

Ca și înainte, robinetele cu un singur clic determină creșterea cărților în incremente, în timp ce o restabilire prin atingerea a două butoane elimină efectul și restabilește aplicația în modul de desen normal.


Încorporează plierea bazată pe strângere

Lucrurile arată bine, din punct de vedere vizual, dar robinetul nu este cu adevărat un gest realist pentru o metaforă de pliere a cărților. Dacă ne gândim la aplicațiile iPad cum ar fi Hârtie, plierea și desfășurarea este condusă de un gest de strângere. Să implementăm asta acum!

Implementați următoarea metodă în ViewController.m:

 - (void) foldWithPinch: (UIPinchGestureRecognizer *) p (dacă (p.state == UIGestureRecognizerStateBegan) // ... (A) self.view.userInteractionEnabled = NO; CGImageRef imgRef = ((CanvasView *) auto-vizualizare) .incrementalImage.CGImage; leftPage.contents = (id id) imgRef; rightPage.contents = (id id) imgRef; leftPage.contentsRect = CGRectMake (0.0, 0.0, 0.5, 1.0); rightPage.contentsRect = CGRectMake (0,5, 0,0, 0,5, 1,0); leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; leftPageShadowLayer.transform = makePerspectiveTransform (); rightPageShadowLayer.transform = makePerspectiveTransform (); [self.view addSubview: curtainView];  float scale = p.scale> 0,48? p.scale: 0,48; // ... (B) scale = scale < 1.0 ? scale : 1.0; // SOME CODE WILL GO HERE (1) leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; leftPageShadowLayer.transform = makePerspectiveTransform(); rightPageShadowLayer.transform = makePerspectiveTransform(); leftPageShadowLayer.transform = CATransform3DScale(leftPageShadowLayer.transform, scale, scale, scale); // (C) rightPageShadowLayer.transform = CATransform3DScale(rightPageShadowLayer.transform, scale, scale, scale); leftPageShadowLayer.transform = CATransform3DRotate(leftPageShadowLayer.transform, (1.0 - scale) * M_PI, 0.0, 1.0, 0.0); rightPageShadowLayer.transform = CATransform3DRotate(rightPageShadowLayer.transform, -(1.0 - scale) * M_PI, 0.0, 1.0, 0.0); // SOME CODE WILL GO HERE (2) if (p.state == UIGestureRecognizerStateEnded) //… (C)  // SOME CODE CHANGES HERE LATER (3) self.view.userInteractionEnabled = YES; [curtainView removeFromSuperview];  

O scurtă explicație privind codul, în ceea ce privește etichetele A, B și C menționate în cod:

  1. Când gestul de prindere este recunoscut (indicat cu ajutorul lui stat proprietate care ia valoarea UIGestureRecognizerStateBegan) începem să ne pregătim pentru animația fold. Declaratia self.view.userInteractionEnabled = NU; se asigură că orice atingere suplimentară care are loc în timpul gestului de prindere nu va determina desenul să aibă loc pe panza. Codul rămas trebuie să vă fie cunoscut. Suntem doar resetarea stratului transformă.
  2. scară proprietatea pinchului determină raportul dintre distanța dintre degete față de începutul prinderii. Am decis să strângem valoarea pe care o vom folosi pentru a calcula scalarea și rotația paginilor noastre 0,48. Conditia scară este astfel încât o "reversie" (utilizatorul își deplasează degetele mai departe decât începutul vârfului, corespunzător p.scale> 1,0) nu are efect. Conditia p.scale> 0,48 este astfel încât, atunci când distanța dintre degete devine aproximativ jumătate din ceea ce a fost la începutul prinderii, animația noastră plată este finalizată și orice alunecare suplimentară nu are efect. Eu aleg 0,48 în loc de 0,50 din cauza modului în care calculez unghiul de cotitură al transformării rotative a stratului. Cu o valoare de 0,48, unghiul de rotație va fi ușor mai mic de 90 de grade, astfel încât cartea nu se va orienta complet și, prin urmare, nu va deveni invizibilă.
  3. După ce utilizatorul a terminat amprenta, vom elimina vizualizarea prezentând straturile noastre animate din vizualizarea canvas (ca mai înainte) și vom restabili interactivitatea panzei.

Adăugați codul pentru a adăuga un dispozitiv de recunoaștere a vârfului la sfârșitul paginii viewDidAppear:

 UIPinchGestureRecognizer * pinch = [[UIPinchGestureRecognizer alocare] initWithTarget: acțiune auto: @selector (foldWithPinch :)]; [auto.view addGestureRecognizer: pinch];

Puteți să scăpați de tot codul legat de cele două butoane de recunoaștere de la robinet ViewController.m pentru că nu mai avem nevoie de ele.

Construiți și alergați. Dacă testați simulatorul în loc de dispozitiv, amintiți-vă că trebuie să țineți apăsată tasta opțională pentru a simula două gesturi cu degetul, cum ar fi ciupirea.

În principiu, desfășurarea de pliere-desfășurare bazată pe strângere funcționează, dar sunteți obligat să observați (în special dacă testați pe un dispozitiv fizic) că animația este lentă și se află în spatele vârfului, slăbind astfel iluzia că prinderea conduce animația pliere-desfășurare. Deci ce se întâmplă?


Obținerea corectă a animațiilor

Amintiți-vă animația implicită a transformării pe care ne-am bucurat "gratuit" când animația a fost controlată de robinet? Se pare că aceeași animație implicită a devenit o piedică acum! În general, atunci când dorim să controlam o animație printr-un gest, cum ar fi o vedere care este trasă pe ecran cu un gest de tragere (sau cazul cu aplicația noastră aici), dorim să anulam animațiile implicite. Să ne asigurăm că înțelegem de ce, luând în considerare cazul unui utilizator care trage o viziune pe ecran cu degetul:

  1. vedere este în poziție p0 pentru a începe cu (adică. view.position = p0 unde p0 = (x0, y0) și este a CGPoint)
  2. Când UIKit urmărește apoi atingerea utilizatorului de pe ecran, el și-a târât degetul în altă poziție, să zicem p1.
  3. Animația animată implicită, cauzând sistemul de animație, începe să animeze schimbarea poziției vedere din p0 la p1 cu durata "relaxată" de 0,25 secunde. Cu toate acestea, animația abia a început, iar utilizatorul și-a târât deja degetul într-o nouă poziție, p2. Animația trebuie să fie anulată, iar cea nouă trebuie să înceapă spre poziția p2.
  4. ... și așa mai departe și așa mai departe!

Din moment ce gestul de panoramare al utilizatorului (în exemplul nostru ipotetic) este efectiv unul continuu, trebuie doar să schimbăm poziția vizuală în pas cu gestul utilizatorului de a păstra iluzia unei animații de răspuns! Exact același argument se aplică și în cazul paginii noastre, cu situația de strângere aici. Am menționat doar exemplul de tragere, deoarece părea mai simplu să explic ceea ce se întâmpla.

Există modalități diferite de a dezactiva animațiile implicite. Vom alege cea mai simplă, care implică împachetarea codului nostru într-un CATransaction blocarea și invocarea metodei de clasă [SetDisableActions: DA]. Deci, ce e CATransaction oricum? În termeni simpli, CATransaction "bundles up" modificările proprietăților care trebuie animate și actualizează succesiv valorile lor pe ecran, tratând toate aspectele legate de calendar. În realitate, face toată munca grea legată de redarea animațiilor noastre pe ecran pentru noi! Chiar dacă nu am utilizat încă o tranzacție de animație în mod explicit, una implicită este întotdeauna prezentă atunci când se execută orice cod de animație. Ceea ce trebuie să facem acum este să încheiem animația cu un obicei CATransaction bloc.

În pinchToFold: adăugați următoarele linii:

 [Începe tranzacția CAT]; [SetDisableActions: DA];

La locul comentariului // CUM SĂ URMEAZĂ UN ANUMIT COD (1),
adăugați linia:

 [Comitentul tranzacției CAT];

Dacă construiți și rulați aplicația acum, veți observa că plierea-derularea este mult mai fluidă și mai receptivă!

O altă problemă legată de animație pe care trebuie să o abordăm este aceea a noastră desfășurare: metoda nu animă restaurarea cărții în starea ei aplatizată atunci când se termină gestul de prindere. S-ar putea să vă plângeți că în codul nostru nu am deranjat de fapt să renunțăm transforma în blocul "apoi" al nostru dacă (p.state == UIGestureRecognizerStateEnded) afirmație. Dar sunteți binevenit să încercați să inserați instrucțiunile care reseta transformarea stratului înainte de instrucțiune [canvasView removeFromSuperview]; pentru a vedea dacă straturile animă această schimbare de proprietate (spoiler: nu vor!).

Motivul pentru care straturile nu vor anima modificarea proprietății este că orice schimbare de proprietate pe care am putea să o realizăm în acel bloc de cod va fi concentrată în același (implicit) CATransaction ca și codul de eliminare a vizualizării stratului de găzduire (canvasView) de pe ecran. Îndepărtarea vederii se va întâmpla imediat - nu este o schimbare animată, la urma urmei - și nu s-ar produce nici o animație în subdiviziuni (sau sub straturi adăugate la stratul său).

Din nou, un explicit CATransaction blocul vine la salvarea noastră! A CATransaction are un bloc de completare care se execută numai după modificările de proprietate care apar după ce au terminat animarea.

Modificați codul următor dacă (p.state == UIGestureRecognizerStateEnded) clauza, astfel încât declarația if să aibă următorul text:

 dacă (p.state == UIGestureRecognizerStateEnded) [Catransacția începe]; [CAT transactivare setCompletionBlock: ^ self.view.userInteractionEnabled = DA; [curtainView removeFromSuperview]; ]; [Set de tranzacționare pe bază de agregareDurație: 0,5 / scară]; leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; [Comitentul tranzacției CAT]; 

Rețineți că am decis să modificăm durata animației în mod invers cu privire la scară astfel încât cu cât este mai mare gradul de întoarcere la momentul în care gestul încheie cât mai mult timp ar trebui să ia animația care se întoarce.

Lucrul crucial pentru a înțelege aici este că numai după transforma schimbările au animat codul în completionBlock a executa. CATransaction clasa are alte proprietăți pe care le puteți utiliza pentru a configura o animație exact așa cum doriți. Vă sugerez să vă uitați la documentație pentru mai multe.

Construiți și alergați. În cele din urmă, animația noastră nu arată numai bine, ci răspunde în mod corespunzător interacțiunii utilizatorilor!


Concluzie

Sper că acest tutorial v-a convins că straturile principale de animație reprezintă o alegere realistă pentru a obține efecte 3D și animații destul de sofisticate, cu puțin efort. Cu o oarecare optimizare, ar trebui să puteți anima câteva sute de straturi pe ecran în același timp, dacă aveți nevoie. Un exemplu de utilizare excelentă pentru Core Animation este încorporarea unei tranziții trecute 3D la trecerea de la o vizualizare la alta în aplicația dvs. De asemenea, simt că Core Animation ar putea fi o opțiune viabilă pentru construirea de jocuri simple bazate pe cuvinte sau pe bază de cărți.

Chiar dacă tutorialul nostru a cuprins două părți, aproape că am zgâriat suprafața straturilor și a animațiilor (dar sperăm că este un lucru bun!). Există și alte tipuri de interesante CALayer subclase pe care nu am avut ocazia să le analizăm. Animația este un subiect imens în sine. Vă recomandăm să vizionați discuțiile despre aceste subiecte, precum "Core Animation in Practice" (Sesiuni 424 și 424, WWDC 2010), "Core Animation Essentials" (Sesiunea 421, WWDC 2011), "Performanța aplicației iOS - Grafică și animație" (Sesiunea 238, WWDC 2012) și "Optimizarea performanței de grafică și animație 2D" (Sesiunea 506, WWDC 2012) și apoi sapă în documentație. Învățarea fericită și scrierea aplicațiilor!

Cod