1. Code
  2. Mobile Development
  3. iOS Development

Erstellen einer 3D-Animation zum Falten von Seiten: Seite skizzieren und Rückenfalteffekt

Scroll to top

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 CALayers besteht aus einer Bitmap. Die Basisklasse ist zwar sehr nützlich, hat aber auch einige wichtige Unterklassen in Core Animation. Insbesondere gibt es CAShapeLayer, 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:

Layer geometryLayer geometryLayer geometry

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).

Rotation wrt two different anchor pointsRotation wrt two different anchor pointsRotation wrt two different anchor points

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:

Determining our layers' geometriesDetermining our layers' geometriesDetermining our layers' geometries
  • 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.

New projectNew projectNew project

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

Supported OrientationsSupported OrientationsSupported Orientations

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.

Linking the QuartzCore frameworkLinking the QuartzCore frameworkLinking the QuartzCore framework

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 &quot;incrementalImage&quot; 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 (wie CATransform3DScale und CATransform3DRotate), eine Transformation mit einer anderen "verketten" (der aktuelle Wert der Layer-Transformationseigenschaft). Andere Funktionen wie CATransform3DMakeRotation, CATransform3DMakeScale und CATransform3DIdentity erstellen lediglich die entsprechende Transformationsmatrix. CATransform3DIdentity ist 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 CGImage hier - und auch CGColor früher - anstelle von UIImage und UIColor. Dies liegt daran, dass CALayer auf 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 wie UIColor und UIImage kö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).

FlatFlatFlat

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.

Fold with perspectiveFold with perspectiveFold with perspective

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!

Lesen Sie Teil 2