Erstellen einer 3D-Animation zum Falten von Seiten: Seite skizzieren und Rückenfalteffekt
German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)
In dieser zweiteiligen Miniserie erfahren Sie, wie Sie mit Core Animation einen beeindruckenden Seitenfalteffekt erzielen. In diesem Teil erfahren Sie zunächst, wie Sie eine Skizzenblockansicht erstellen und dann eine grundlegende Animation zum Falten des Rückens auf diese Ansicht anwenden. Weiter lesen!
Endgültige Projektdemo
Tutorial Übersicht
Die Arbeit mit der UIView-Klasse ist für die Entwicklung des iOS SDK von zentraler Bedeutung. Ansichten haben sowohl einen visuellen Aspekt (d. h. was Sie auf dem Bildschirm sehen) als auch normalerweise einen Steuerungsaspekt (Benutzerinteraktion über Berührungen und Gesten). Der visuelle Aspekt wird tatsächlich von einer Klasse behandelt, die zum Core Animation Framework gehört und CALayer heißt (die wiederum über OpenGL implementiert wird, nur dass wir hier nicht auf diese Abstraktionsebene zurückgreifen möchten). Ebenenobjekte sind Instanzen der CALayer-Klasse oder einer ihrer Unterklassen. Ohne zu tief in die Theorie einzutauchen (dies soll schließlich ein praktisches Tutorial sein!), Sollten wir die folgenden Punkte beachten:
- Jede Ansicht in iOS wird von einer Ebene unterstützt, die für den visuellen Inhalt verantwortlich ist. Programmgesteuert wird darauf als Layereigenschaft in der Ansicht zugegriffen.
- Es gibt eine parallele Ebenenhierarchie, die der Ansichtshierarchie auf dem Bildschirm entspricht. Dies bedeutet, dass, wenn (sagen wir) ein Etikett eine Unteransicht einer Schaltfläche ist, die Ebene des Etiketts die Unterebene der Ebene der Schaltfläche ist. Genau genommen gilt diese Parallelität nur, solange wir der Mischung keine eigenen Unterschichten hinzufügen.
- Einige Eigenschaften, die wir für Ansichten festlegen (insbesondere die Ansichten für das Erscheinungsbild), sind in Wirklichkeit Eigenschaften für die darunter liegende Ebene. Die Ebene stellt jedoch einige Eigenschaften bereit, die auf Ansichtsebene nicht verfügbar sind. In diesem Sinne sind Ebenen leistungsfähiger als Ansichten.
- Im Vergleich zu Ansichten sind Ebenen "leichtere" Objekte. Wenn wir für einen Aspekt unserer App eine Visualisierung ohne Interaktivität benötigen, sind Ebenen wahrscheinlich die leistungsfähigere Option.
- Der Inhalt eines
CALayersbesteht aus einer Bitmap. Die Basisklasse ist zwar sehr nützlich, hat aber auch einige wichtige Unterklassen in Core Animation. Insbesondere gibt esCAShapeLayer, mit dem wir Formen mithilfe von Vektorpfaden darstellen können.
Was können wir also mit Ebenen erreichen, die wir mit Ansichten nicht einfach direkt erreichen können? Zum einen 3D, worauf wir uns hier konzentrieren werden. Mit den Funktionen von CALayer haben Sie ziemlich ausgefeilte 3D-Effekte und -Animationen in Reichweite, ohne auf die OpenGL-Ebene absteigen zu müssen. Dies ist das Ziel dieses Tutorials: einen interessanten 3D-Effekt zu demonstrieren, der einen kleinen Vorgeschmack darauf gibt, was wir mit CALayer erreichen können.
Unser Ziel
Wir haben eine Zeichen-App, mit der ein Benutzer mit dem Finger auf dem Bildschirm zeichnen kann (für die ich den Code aus einem früheren Tutorial von mir wiederverwenden werde). Wenn der Benutzer dann eine Quetschgeste auf der Zeichenfläche ausführt, wird diese entlang einer vertikalen Linie entlang der Mitte der Leinwand (ähnlich wie der Buchrücken) gefaltet. Das zusammengeklappte Buch wirft sogar einen Schatten auf den Hintergrund.
Der Zeichnungsteil der App, den ich ausleihe, ist hier nicht wirklich wichtig, und wir könnten sehr gut jedes Bild verwenden, um den Faltungseffekt zu demonstrieren. Der Effekt ergibt jedoch eine sehr schöne visuelle Metapher im Kontext einer Zeichen-App, bei der durch das Kneifen ein Buch mit mehreren Blättern (mit unseren vorherigen Zeichnungen) freigelegt wird, das wir durchsuchen können. Diese Metapher ist insbesondere in der Paper-App zu sehen. Für die Zwecke des Tutorials wird unsere Implementierung zwar einfacher und weniger ausgefeilt sein, aber es ist nicht zu weit entfernt... und natürlich können Sie das, was Sie in diesem Tutorial gelernt haben, noch besser machen!
Ebenengeometrie in Kürze
Denken Sie daran, dass im iOS-Koordinatensystem der Ursprung in der oberen linken Ecke des Bildschirms liegt, wobei die x-Achse nach rechts und die y-Achse nach unten zunimmt. Der Rahmen einer Ansicht beschreibt das Rechteck im Koordinatensystem der Übersicht. Ebenen können auch nach ihrem Rahmen abgefragt werden, es gibt jedoch eine andere(bevorzugte) Möglichkeit, die Position und Größe einer Ebene zu beschreiben. Wir werden diese mit einem einfachen Beispiel motivieren: Stellen Sie sich zwei Schichten, A und B, als rechteckige Papierstücke vor. Sie möchten die Schicht B zu einer Unterschicht von A machen, also befestigen Sie B mit einem Stift auf A, wobei Sie die geraden Seiten parallel halten. Der Stift durchläuft zwei Punkte, einen in A und einen in B. Wenn wir die Position dieser beiden Punkte kennen, können wir die Position von B relativ zu A effektiv beschreiben. Wir werden den Punkt, den der Stift in A durchbohrt, als "Ankerpunkt" und der Punkt in B "Position". Schauen Sie sich die folgende Abbildung an, für die wir rechnen werden:


Die Figur scheint viel los zu sein, aber keine Sorge, wir werden sie Stück für Stück untersuchen:
- Die obere Grafik zeigt die hierarchische Beziehung: Die violette Schicht (A, aus unserer vorherigen Diskussion) wird zu einer Unterschicht der blauen Schicht(B). Der grüne Kreis mit dem Plus ist der Punkt, an dem A in B fixiert ist. Die Position (im Koordinatensystem von A) wird als {32,5, 62,5} angegeben.
- Wenden Sie sich nun der unteren Grafik zu. Der Ankerpunkt wird anders angegeben. Es ist relative zur Größe von Ebene B, so dass die obere linke Ecke in {0.0, 0.0} und die untere rechte Ecke in {1.0, 1.0}. Da unser Stift ein Viertel des Abstands über die Breite von B und die Hälfte des Weges nach unten beträgt, beträgt der Ankerpunkt {0,25, 0,5}.
- Wenn wir die Größe von B (50 x 45) kennen, können wir jetzt die Koordinate der oberen linken Ecke berechnen. Bezogen auf die obere linke Ecke von B beträgt der Ankerpunkt 0,25 x 50 = 12,5 Punkte in x-Richtung und 0,50 x 45 = 22,5 Punkte in y-Richtung. Subtrahieren Sie diese von den Koordinaten der Position, und Sie erhalten die Koordinaten des Ursprungs von B im System von A: {32,5 - 12,5, 62,5 - 22,5} = {20, 40}. Der Rahmen von B ist eindeutig {20, 40, 50, 45}.
Die Berechnung ist recht einfach, stellen Sie also sicher, dass Sie sie gründlich verstehen. Sie erhalten ein gutes Gefühl für die Beziehung zwischen Position, Ankerpunkt und Rahmen.
Der Ankerpunkt ist sehr wichtig, da bei der Durchführung von 3D-Transformationen auf der Ebene diese Transformationen in Bezug auf den Ankerpunkt ausgeführt werden. Wir werden als nächstes darüber sprechen (und dann werden Sie einen Code sehen, das verspreche ich!).
Schichttransformationen
Ich bin sicher, dass Sie mit dem Konzept von Transformationen wie Skalierung, Übersetzung und Rotation vertraut sind. Wenn Sie in der Foto-App in iOS 6 mit zwei Fingern ein Foto in Ihrem Album vergrößern oder verkleinern, führen Sie eine Skalentransformation durch. Wenn Sie mit beiden Fingern eine Drehbewegung ausführen, dreht sich das Foto, und wenn Sie es ziehen, während die Seiten parallel bleiben, ist dies eine Übersetzung. Kernanimation und CALayer trumpfen mit UIView auf, indem Sie Transformationen in 3D anstatt nur in 2D durchführen können. Natürlich sind unsere iDevice-Bildschirme im Jahr 2013 immer noch 2D, daher verwenden 3D-Transformationen einige geometrische Tricks, um unsere Augen dazu zu bringen, ein flaches Bild als 3D-Objekt zu interpretieren (der Vorgang unterscheidet sich nicht von der Darstellung eines 3D-Objekts in einer mit a erstellten Strichzeichnung Bleistift, wirklich). Um mit der 3. Dimension fertig zu werden, müssen wir eine Z-Achse verwenden, von der wir uns vorstellen, dass sie unseren Gerätebildschirm durchbohrt und senkrecht dazu steht.
Der Ankerpunkt ist wichtig, da das genaue Ergebnis derselben angewendeten Transformation normalerweise davon abhängt. Dies ist besonders wichtig - und am einfachsten zu verstehen - bei einer Rotationstransformation um denselben Winkel, die in Bezug auf zwei verschiedene Ankerpunkte im Rechteck in der folgenden Abbildung angewendet wird (der rote Punkt und der blaue Punkt). Beachten Sie, dass sich die Drehung in der Bildebene befindet (oder um die Z-Achse, wenn Sie dies lieber so sehen möchten).


Die Seitenfaltenanimation
Wie implementieren wir den Faltungseffekt, den wir suchen? Während Ebenen wirklich cool sind, können Sie sie nicht über die Mitte falten! Die Lösung besteht - wie Sie sicher herausgefunden haben - darin, zwei Ebenen zu verwenden, eine für jede Seite auf beiden Seiten der Falte. Lassen Sie uns auf der Grundlage dessen, was wir zuvor besprochen haben, die geometrischen Eigenschaften dieser beiden Schichten im Voraus herausarbeiten:


- Wir haben einen Punkt entlang des "Faltenrückens" als Ankerpunkt für beide Ebenen ausgewählt, da dort unsere Faltung (d. h. die Rotationstransformation) stattfindet. Die Drehung erfolgt um eine vertikale Linie (d. h. die y-Achse) - stellen Sie sicher, dass Sie dies visualisieren. Das ist in Ordnung, könnte man sagen, aber warum habe ich den Mittelpunkt der Wirbelsäule gewählt (anstatt zu sagen, einen Punkt unten oder oben)? Tatsächlich macht es in diesem speziellen Fall keinen Unterschied, was die Rotation betrifft. Wir möchten aber auch eine Skalentransformation durchführen (wodurch die Ebenen beim Falten etwas kleiner werden). Wenn Sie den Ankerpunkt in der Mitte halten, bleibt das Buch beim Falten schön zentriert. Dies liegt daran, dass für die Skalierung der mit dem Ankerpunkt übereinstimmende Punkt in seiner Position fixiert bleibt.
- Der Ankerpunkt für die erste Ebene ist
{1.0, 0.5}und für die zweite Ebene ist{0.0, 0.5}in ihren jeweiligen Koordinatenräumen. Stellen Sie sicher, dass Sie dies anhand der Abbildung bestätigen, bevor Sie fortfahren! - Der Punkt, der unter dem Ankerpunkt in der Superschicht liegt (d. h. die "position"), ist der Mittelpunkt, daher sind seine Koordinaten
{width/2, height/2}. Denken Sie daran, dass die Positionseigenschaft in Standardkoordinaten und nicht normalisiert ist. - Die Größe jeder der Ebenen beträgt
{width/2, height}.
Implementierung
Wir wissen jetzt genug, um Code zu schreiben!
Erstellen Sie ein neues Xcode-Projekt mit der Vorlage "Leere Anwendung" und nennen Sie es LayerFunTut. Machen Sie es zu einer iPad-App und aktivieren Sie die automatische Referenzzählung (ARC). Deaktivieren Sie jedoch die Optionen für Kerndaten und Komponententests. Speichern Sie es.


Scrollen Sie auf der angezeigten Seite Ziel > Zusammenfassung nach unten zu "Unterstützte Schnittstellenausrichtungen" und wählen Sie die beiden Querformatausrichtungen aus.


Scrollen Sie weiter nach unten, bis Sie zu "Verknüpfte Frameworks und Bibliotheken" gelangen, klicken Sie auf "+" und fügen Sie das QuartzCore-Kernframework hinzu, das für Core Animation und CALayers erforderlich ist.


Wir beginnen mit der Integration unserer Zeichen-App in das Projekt. Erstellen Sie eine neue Objective-C-Klasse mit dem Namen CanvasView und machen Sie sie zu einer Unterklasse von UIView. Fügen Sie den folgenden Code in CanvasView.h ein:
1 |
//
|
2 |
// CanvasView.h
|
3 |
//
|
4 |
|
5 |
#import <UIKit/UIKit.h>
|
6 |
|
7 |
@interface CanvasView : UIView |
8 |
|
9 |
@property (nonatomic, strong) UIImage *incrementalImage; |
10 |
|
11 |
@end
|
Und dann in CanvasView.m:
1 |
//
|
2 |
// CanvasView.m
|
3 |
//
|
4 |
|
5 |
#import "CanvasView.h"
|
6 |
|
7 |
@implementation CanvasView |
8 |
{
|
9 |
UIBezierPath *path; |
10 |
CGPoint pts[5]; |
11 |
uint ctr; |
12 |
}
|
13 |
|
14 |
- (id)initWithCoder:(NSCoder *)aDecoder |
15 |
{
|
16 |
if (self = [super initWithCoder:aDecoder]) |
17 |
{
|
18 |
self.backgroundColor = [UIColor clearColor]; |
19 |
[self setMultipleTouchEnabled:NO]; |
20 |
path = [UIBezierPath bezierPath]; |
21 |
[path setLineWidth:6.0]; |
22 |
}
|
23 |
return self; |
24 |
}
|
25 |
- (id)initWithFrame:(CGRect)frame |
26 |
{
|
27 |
self = [super initWithFrame:frame]; |
28 |
if (self) { |
29 |
self.backgroundColor = [UIColor clearColor]; |
30 |
[self setMultipleTouchEnabled:NO]; |
31 |
path = [UIBezierPath bezierPath]; |
32 |
[path setLineWidth:6.0]; |
33 |
}
|
34 |
return self; |
35 |
}
|
36 |
|
37 |
- (void)drawRect:(CGRect)rect |
38 |
{
|
39 |
[self.incrementalImage drawInRect:rect]; |
40 |
[[UIColor blueColor] setStroke]; |
41 |
[path stroke]; |
42 |
}
|
43 |
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event |
44 |
{
|
45 |
ctr = 0; |
46 |
UITouch *touch = [touches anyObject]; |
47 |
pts[0] = [touch locationInView:self]; |
48 |
}
|
49 |
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event |
50 |
{
|
51 |
UITouch *touch = [touches anyObject]; |
52 |
CGPoint p = [touch locationInView:self]; |
53 |
ctr++; |
54 |
pts[ctr] = p; |
55 |
if (ctr == 4) |
56 |
{
|
57 |
pts[3] = CGPointMake((pts[2].x + pts[4].x)/2.0, (pts[2].y + pts[4].y)/2.0); |
58 |
[path moveToPoint:pts[0]]; |
59 |
[path addCurveToPoint:pts[3] controlPoint1:pts[1] controlPoint2:pts[2]]; |
60 |
[self setNeedsDisplay]; |
61 |
pts[0] = pts[3]; |
62 |
pts[1] = pts[4]; |
63 |
ctr = 1; |
64 |
}
|
65 |
}
|
66 |
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event |
67 |
{
|
68 |
[self drawBitmap]; |
69 |
[self setNeedsDisplay]; |
70 |
[path removeAllPoints]; |
71 |
ctr = 0; |
72 |
}
|
73 |
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event |
74 |
{
|
75 |
[self touchesEnded:touches withEvent:event]; |
76 |
}
|
77 |
- (void)drawBitmap |
78 |
{
|
79 |
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0); |
80 |
if (!self.incrementalImage) |
81 |
{
|
82 |
UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds]; |
83 |
[[UIColor clearColor] setFill]; |
84 |
[rectpath fill]; |
85 |
}
|
86 |
[self.incrementalImage drawAtPoint:CGPointZero]; |
87 |
[[UIColor blueColor] setStroke]; |
88 |
[path stroke]; |
89 |
self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext(); |
90 |
UIGraphicsEndImageContext(); |
91 |
}
|
92 |
@end
|
Wie bereits erwähnt, ist dies nur der Code aus einem anderen Tutorial, das ich geschrieben habe (mit einigen geringfügigen Änderungen). Probieren Sie es aus, wenn Sie nicht sicher sind, wie der Code funktioniert. Für die Zwecke dieses Tutorials ist es jedoch wichtig, dass der Benutzer mit CanvasView sanfte Striche auf dem Bildschirm zeichnen kann. Wir haben eine Eigenschaft namens incrementalImage deklariert, in der eine Bitmap-Version der Benutzerzeichnung gespeichert ist. Dies ist das Bild, das wir mit CALayer "zusammenfalten" werden.
Zeit, den View-Controller-Code zu schreiben und die zuvor ausgearbeiteten Ideen umzusetzen. Eine Sache, die wir nicht besprochen haben, ist, wie wir das gezeichnete Bild in unseren CALayer bekommen, so dass die Hälfte des Bildes auf die linke Seite und die andere Hälfte auf die rechte Seite gezeichnet wird. Zum Glück sind das nur ein paar Codezeilen, über die ich später sprechen werde.
Erstellen Sie eine neue Objective-C-Klasse mit dem Namen ViewController, machen Sie sie zu einer Unterklasse von UIViewController und überprüfen Sie keine der angezeigten Optionen.
Fügen Sie den folgenden Code in ViewController.m ein
1 |
//
|
2 |
// ViewController.m
|
3 |
//
|
4 |
|
5 |
#import "ViewController.h"
|
6 |
#import "CanvasView.h"
|
7 |
#import "QuartzCore/QuartzCore.h"
|
8 |
|
9 |
#define D2R(x) (x * (M_PI/180.0)) // macro to convert degrees to radians
|
10 |
|
11 |
@interface ViewController () |
12 |
|
13 |
@end
|
14 |
|
15 |
@implementation ViewController |
16 |
{
|
17 |
CALayer *leftPage; |
18 |
CALayer *rightPage; |
19 |
UIView *curtainView; |
20 |
}
|
21 |
|
22 |
|
23 |
- (void)loadView |
24 |
{
|
25 |
self.view = [[CanvasView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]]; |
26 |
}
|
27 |
|
28 |
- (void)viewDidLoad |
29 |
{
|
30 |
[super viewDidLoad]; |
31 |
self.view.backgroundColor = [UIColor blackColor]; |
32 |
}
|
33 |
- (void)viewDidAppear:(BOOL)animated |
34 |
{
|
35 |
[super viewDidAppear:animated]; |
36 |
self.view.backgroundColor = [UIColor whiteColor]; |
37 |
|
38 |
CGSize size = self.view.bounds.size; |
39 |
leftPage = [CALayer layer]; |
40 |
rightPage = [CALayer layer]; |
41 |
leftPage.anchorPoint = (CGPoint){1.0, 0.5}; |
42 |
rightPage.anchorPoint = (CGPoint){0.0, 0.5}; |
43 |
leftPage.position = (CGPoint){size.width/2.0, size.height/2.0}; |
44 |
rightPage.position = (CGPoint){size.width/2.0, size.height/2.0}; |
45 |
leftPage.bounds = (CGRect){0, 0, size.width/2.0, size.height}; |
46 |
rightPage.bounds = (CGRect){0, 0, size.width/2.0, size.height}; |
47 |
leftPage.backgroundColor = [UIColor whiteColor].CGColor; |
48 |
rightPage.backgroundColor = [UIColor whiteColor].CGColor; |
49 |
leftPage.borderWidth = 2.0; // borders added for now, so we can visually distinguish between the left and right pages |
50 |
rightPage.borderWidth = 2.0; |
51 |
leftPage.borderColor = [UIColor darkGrayColor].CGColor; |
52 |
rightPage.borderColor = [UIColor darkGrayColor].CGColor; |
53 |
|
54 |
//leftPage.transform = makePerspectiveTransform(); // uncomment later
|
55 |
//rightPage.transform = makePerspectiveTransform(); // uncomment later
|
56 |
curtainView = [[UIView alloc] initWithFrame:self.view.bounds]; |
57 |
curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor]; |
58 |
|
59 |
[curtainView.layer addSublayer:leftPage]; |
60 |
[curtainView.layer addSublayer:rightPage]; |
61 |
|
62 |
|
63 |
UITapGestureRecognizer *foldTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fold:)]; |
64 |
[self.view addGestureRecognizer:foldTap]; |
65 |
|
66 |
UITapGestureRecognizer *unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(unfold:)]; |
67 |
unfoldTap.numberOfTouchesRequired = 2; |
68 |
[self.view addGestureRecognizer:unfoldTap]; |
69 |
}
|
70 |
|
71 |
- (void)fold:(UITapGestureRecognizer *)gr |
72 |
{
|
73 |
// drawing the "incrementalImage" bitmap into our layers
|
74 |
CGImageRef imgRef = ((CanvasView *)self.view).incrementalImage.CGImage; |
75 |
leftPage.contents = (__bridge id)imgRef; |
76 |
rightPage.contents = (__bridge id)imgRef; |
77 |
leftPage.contentsRect = CGRectMake(0.0, 0.0, 0.5, 1.0); // this rectangle represents the left half of the image |
78 |
rightPage.contentsRect = CGRectMake(0.5, 0.0, 0.5, 1.0); // this rectangle represents the right half of the image |
79 |
|
80 |
leftPage.transform = CATransform3DScale(leftPage.transform, 0.95, 0.95, 0.95); |
81 |
rightPage.transform = CATransform3DScale(rightPage.transform, 0.95, 0.95, 0.95); |
82 |
leftPage.transform = CATransform3DRotate(leftPage.transform, D2R(7.5), 0.0, 1.0, 0.0); |
83 |
rightPage.transform = CATransform3DRotate(rightPage.transform, D2R(-7.5), 0.0, 1.0, 0.0); |
84 |
|
85 |
[self.view addSubview:curtainView]; |
86 |
}
|
87 |
|
88 |
- (void)unfold:(UITapGestureRecognizer *)gr |
89 |
{
|
90 |
leftPage.transform = CATransform3DIdentity; |
91 |
rightPage.transform = CATransform3DIdentity; |
92 |
// leftPage.transform = makePerspectiveTransform(); // uncomment later
|
93 |
// rightPage.transform = makePerspectiveTransform(); // uncomment later
|
94 |
[curtainView removeFromSuperview]; |
95 |
}
|
96 |
|
97 |
// UNCOMMENT LATER:
|
98 |
/*
|
99 |
|
100 |
CATransform3D makePerspectiveTransform()
|
101 |
{
|
102 |
CATransform3D transform = CATransform3DIdentity;
|
103 |
transform.m34 = 1.0 / -2000;
|
104 |
return transform;
|
105 |
}
|
106 |
|
107 |
*/
|
108 |
|
109 |
@end
|
Wenn Sie den auskommentierten Code vorerst ignorieren, können Sie sehen, dass die eingerichtete Ebene genau so ist, wie wir es oben geplant haben.
Lassen Sie uns diesen Code kurz diskutieren:
- Wir beschließen, die Methode -viewDidAppear: anstelle von -viewDidLoad (an die Sie vielleicht eher gewöhnt sind) zu überschreiben, da beim Aufrufen der letzteren Methode die Grenzen der Ansicht weiterhin für den Hochformatmodus gelten. Unsere App wird jedoch im Querformat ausgeführt. Zum Zeitpunkt des Aufrufs von viewDidAppear: wurden die Grenzen korrekt festgelegt und wir haben unseren Code dort abgelegt (wir haben vorübergehend dicke Ränder hinzugefügt, damit wir die linke und rechte Ebene erkennen können, wenn wir Transformationen auf sie anwenden).
- Wir haben einen Gestenerkenner hinzugefügt, der ein Tippen registriert. Bei jedem Tippen werden die Seiten etwas kleiner (95% ihrer vorherigen Größe) und um 7.5 Grad gedreht. Die Zeichen sind unterschiedlich, da sich eine der Seiten im Uhrzeigersinn und die andere gegen den Uhrzeigersinn dreht. Wir müssten in die Mathematik gehen, um zu sehen, welches Vorzeichen welcher Richtung entspricht, aber da es nur zwei Optionen gibt, ist es einfacher, einfach den Code zu schreiben und zu überprüfen! Übrigens akzeptieren die Transformationsfunktionen Winkel im Bogenmaß, daher verwenden wir das Makro
D2R(), um vom Bogenmaß in Grad umzuwandeln. Eine wichtige Beobachtung ist, dass die Funktionen, die eine Transformation in ihrem Argument annehmen (wieCATransform3DScaleundCATransform3DRotate), eine Transformation mit einer anderen "verketten" (der aktuelle Wert der Layer-Transformationseigenschaft). Andere Funktionen wieCATransform3DMakeRotation,CATransform3DMakeScaleundCATransform3DIdentityerstellen lediglich die entsprechende Transformationsmatrix.CATransform3DIdentityist die "Identitätstransformation", die eine Ebene beim Erstellen einer Ebene hat. Es ist analog zur Zahl "1" in einer Multiplikation, dass das Anwenden einer Identitätstransformation auf eine Ebene ihre Transformation unverändert lässt, ähnlich wie das Multiplizieren einer Zahl mit einer. - In Bezug auf die Zeichnung legen wir die Inhaltseigenschaft unserer Ebenen als Bild fest. Es ist sehr wichtig zu beachten, dass wir das Inhaltsrechteck (normalisiert zwischen 0 und 1 entlang jeder Dimension) so einstellen, dass auf jeder Seite nur die Hälfte des entsprechenden Bildes angezeigt wird. Dieses normalisierte Koordinatensystem ist das gleiche wie das, das wir zuvor beim Sprechen über den Ankerpunkt besprochen haben. Sie sollten also in der Lage sein, die Werte zu berechnen, die wir für jede Bildhälfte verwendet haben.
- Das curtainView-Objekt fungiert einfach als Container für die Seitenebenen (genauer gesagt, sie werden als Unterebenen für die zugrunde liegende Ebene von CurtainView erstellt). Denken Sie daran, dass wir die Platzierung und Geometrie der Ebenen bereits berechnet haben. Diese beziehen sich auf die Ebene von curtainView. Durch einmaliges Tippen wird diese Ansicht über unserer Leinwandansicht hinzugefügt und die Transformation auf die Ebene angewendet. Durch zweimaliges Tippen wird es entfernt, um die Leinwand erneut anzuzeigen, und die Transformation der Ebenen wird in die Identitätstransformation zurückgesetzt.
- Beachten Sie die Verwendung von
CGImagehier - und auchCGColorfrüher - anstelle vonUIImageundUIColor. Dies liegt daran, dassCALayerauf einer Ebene unterhalb von UIKit arbeitet und mit "undurchsichtigen" Datentypen arbeitet (was ungefähr bedeutet, fragen Sie nicht nach der zugrunde liegenden Implementierung!), Die im Core Graphics-Framework definiert sind. Objective-C-Klassen wieUIColorundUIImagekönnen als objektorientierte Wrapper um ihre primitiveren CG-Versionen betrachtet werden. Der Einfachheit halber machen viele UIKit-Objekte ihren zugrunde liegenden CG-Typ als Eigenschaft verfügbar.
Ersetzen Sie in der Datei AppDelegate.m den gesamten Code durch den folgenden Code (das einzige, was wir hinzugefügt haben, ist, die ViewController-Headerdatei einzuschließen und eine ViewController-Instanz zum Root-View-Controller zu machen):
1 |
//
|
2 |
// AppDelegate.m
|
3 |
//
|
4 |
|
5 |
#import "AppDelegate.h"
|
6 |
#import "ViewController.h"
|
7 |
|
8 |
@implementation AppDelegate |
9 |
|
10 |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
11 |
{
|
12 |
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; |
13 |
self.window.rootViewController = [[ViewController alloc] init]; |
14 |
self.window.backgroundColor = [UIColor whiteColor]; |
15 |
[self.window makeKeyAndVisible]; |
16 |
return YES; |
17 |
}
|
18 |
|
19 |
@end
|
Erstellen Sie das Projekt und führen Sie es auf dem Simulator oder auf Ihrem Gerät aus. Kritzeln Sie ein wenig auf die Leinwand und tippen Sie dann mit einem Finger auf den Bildschirm, um die Gestenerkennungsaktion auszulösen (durch Tippen mit zwei Fingern wird der 3D-Effekt beendet und die Zeichenfläche wird wieder angezeigt).


Nicht ganz der Effekt, den wir anstreben! Was ist los?
Unsere Perspektive richtig machen
Beachten Sie zunächst, dass die Seiten mit jedem einzelnen Tippen kleiner werden, sodass das Problem nicht bei der Skalierungstransformation liegt, sondern nur bei der Drehung. Das Problem ist, dass das Ergebnis, obwohl die Rotation in einem (mathematischen) 3D-Raum stattfindet, auf unsere Flachbildschirme projiziert wird, so wie ein 3D-Objekt seinen Schatten auf eine Wand wirft. Um Tiefe zu vermitteln, müssen wir eine Art Stichwort verwenden. Der wichtigste Hinweis ist der der Perspektive: Ein Objekt, das näher an unseren Augen liegt, erscheint größer als eines weiter entfernt. Schatten sind ein weiteres großartiges Stichwort, und wir werden sie in Kürze erreichen. Wie integrieren wir die Perspektive in unsere Transformation?
Lassen Sie uns zuerst ein wenig über Transformationen sprechen. Was sind sie wirklich? Mathematisch gesehen sollten Sie wissen, dass geometrische Transformationen wie Skalierung, Rotation und Translation als Matrixtransformationen dargestellt werden, wenn wir die Punkte in unserer Form als mathematische Vektoren darstellen. Dies bedeutet, dass, wenn wir eine Matrix nehmen, die eine Transformation darstellt, und mit einem Vektor multiplizieren, der einen Punkt in unserer Form darstellt, das Ergebnis der Multiplikation (auch ein Vektor) darstellt, wo dieser Punkt nach der Transformation endet. Mehr können wir hier nicht sagen, ohne auf die Theorie einzugehen (was es wirklich wert ist, gelernt zu werden, wenn Sie noch nicht damit vertraut sind - insbesondere, wenn Sie coole 3D-Effekte in Ihre Apps integrieren möchten!).
Was ist mit Code? Zuvor haben wir die Ebenengeometrie festgelegt, indem wir den anchorPoint, die position und die bounds festgelegt haben. Was wir auf dem Bildschirm sehen, ist die Geometrie der Ebene, nachdem sie durch ihre transform eigenschaft transformiert wurde. Beachten Sie die Funktionsaufrufe, die wie layer.transform = //... aussehen. Hier setzen wir die Transformation, die intern nur eine struct ist, die eine 4 x 4-Matrix von Gleitkommawerten darstellt. Beachten Sie auch, dass die Funktionen CATransform3DScale und CATransform3DRotate die aktuelle Transformation der Ebene als Parameter verwenden. Das liegt daran, dass wir mehrere Transformationen zusammensetzen können (was nur bedeutet, dass wir ihre Matrizen miteinander multiplizieren), wobei das Endergebnis so ist, als hätten Sie diese Transformationen einzeln durchgeführt. Beachten Sie, dass wir nur über das Endergebnis der Transformation sprechen, nicht darüber, wie Core Animation die Ebene animiert!
Um auf das Perspektivproblem zurückzukommen, müssen wir wissen, dass unsere Transformationsmatrix einen Wert enthält, den wir optimieren können, um den gewünschten Perspektiveneffekt zu erzielen. Dieser Wert ist ein Mitglied der Transformationsstruktur mit der Bezeichnung m34 (die Zahlen geben die Position in der Matrix an). Um den gewünschten Effekt zu erzielen, müssen wir ihn auf eine kleine negative Zahl setzen.
Kommentieren Sie die beiden kommentierten Abschnitte in der Datei ViewController.m (die Funktion CATransform3D makePerspectiveTransform() und die Zeilen leftPage.transform = makePerspectiveTransform(); rightPage.transform = makePerspectiveTransform(); und erstellen Sie sie erneut. Diesmal sieht der 3D-Effekt glaubwürdiger aus.


Beachten Sie auch, dass der Deal eine "kostenlose" Animation enthält, wenn wir die Transformationseigenschaft eines CALayer ändern. Dies ist, was wir hier wollen - im Gegensatz zu der Ebene, die ihre Transformation abrupt durchläuft -, aber manchmal ist es nicht so.
Natürlich geht die Perspektive nur so weit, wenn unser Beispiel komplexer wird, werden wir auch Schatten verwenden! Vielleicht möchten wir auch die Ecken unseres "Buches" abrunden, und das Maskieren unserer Seitenebenen mit einem CAShapeLayer kann dabei helfen. Außerdem möchten wir eine Prise Geste verwenden, um das Falten / Entfalten so zu steuern, dass es sich interaktiver anfühlt. All dies wird im zweiten Teil dieser Tutorial-Miniserie behandelt.
Ich empfehle Ihnen, unter Bezugnahme auf die API-Dokumentation mit dem Code zu experimentieren und zu versuchen, unseren gewünschten Effekt unabhängig zu implementieren (möglicherweise machen Sie es sogar besser!).
Viel Spaß mit dem Tutorial und vielen Dank fürs Lesen!



