Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Grundlagen
3 Views und Viewcontroller
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Jahrmarkt der Nützlichkeiten
A Sicherer Entwicklungszyklus
Stichwort

Download:
- ZIP, ca. 49,9 MB
Buch bestellen
Ihre Meinung?

Spacer
Apps programmieren für iPhone und iPad von Klaus M. Rodewig, Clemens Wagner
Das umfassende Handbuch
Buch: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Galileo Computing
1000 S., geb., mit DVD
39,90 Euro, ISBN 978-3-8362-1915-0
Pfeil 6 Models, Layer, Animationen
Pfeil 6.1 Modell und Controller
Pfeil 6.1.1 iOS Next Topmodel
Pfeil 6.1.2 View an Controller
Pfeil 6.1.3 Modell an Controller
Pfeil 6.1.4 Undo und Redo
Pfeil 6.1.5 Unit-Tests
Pfeil 6.2 Als die Views das Laufen lernten
Pfeil 6.2.1 Animationen mit Blöcken
Pfeil 6.2.2 Transitionen
Pfeil 6.2.3 Zur Animation? Bitte jeder nur einen Block!
Pfeil 6.3 Core Animation
Pfeil 6.3.1 Layer
Pfeil 6.3.2 Vordefinierte Layer-Klassen
Pfeil 6.3.3 Unser Button soll schöner werden
Pfeil 6.3.4 Spieglein, Spieglein an der Wand
Pfeil 6.3.5 Der bewegte Layer
Pfeil 6.3.6 Daumenkino
Pfeil 6.3.7 Relativitätstheorie
Pfeil 6.3.8 Der View, der Layer, seine Animation und ihr Liebhaber
Pfeil 6.3.9 Die 3. Dimension
Pfeil 6.4 Scrollviews und gekachelte Layer
Pfeil 6.4.1 Scrollen und Zoomen
Pfeil 6.4.2 Die Eventverarbeitung
Pfeil 6.4.3 Scharfe Kurven
Pfeil 6.4.4 Ganz großes Kino
Pfeil 6.4.5 PDF-Dateien anzeigen
Pfeil 6.5 Über diese Brücke musst du gehen
Pfeil 6.5.1 Toll-Free Bridging und ARC
Pfeil 6.5.2 C-Frameworks und ARC
Pfeil 6.6 Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Pfeil 6.6.1 Spiel mir das Lied vom Leak
Pfeil 6.6.2 Ich folgte einem Zombie
Pfeil 6.6.3 Time Bandits

Galileo Computing - Zum Seitenanfang

6.3 Core AnimationZur nächsten Überschrift

Auf den ersten Blick wirken die Viewanimationen ein bisschen wie Magie. Durch einen zusätzlichen Methodenaufruf verwandeln Sie eine statische Veränderung des Views in einen eleganten Übergang, wobei Sie auch noch eine große Palette möglicher Animationen zur Verfügung haben. Da fragt sich doch der neugierige Programmierer, wie das funktioniert. Die Antwort darauf findet sich eine Ebene tiefer.

Core Animation ist eine Sammlung von Klassen zur Darstellung, Projektion und Animation von Grafiken. Der Name Core Animation ist dabei vielleicht etwas verwirrend, da Sie Core Animation auch ohne Animationen sinnvoll verwenden können.

Core Animation verwenden

Wenn Sie die Klassen aus Core Animation in Ihren Apps verwenden möchten, müssen Sie das QuartzCore-Framework einbinden. Dazu wählen Sie im Projektnavigator das Projekt aus und danach unter Targets das entsprechende Target [In den meisten Fällen hat Ihr Projekt nur ein Target.] . Dort öffnen Sie die Rubrik Link Binary With Libraries unter dem Reiter Build Phases. Wenn Sie den Plusknopf am unteren Rand der Liste anklicken, können Sie das Framework über den Eintrag QuartzCore.framework auswählen und zu dem Target hinzufügen. Außerdem müssen Sie den entsprechenden Header über

#import <QuartzCore/QuartzCore.h>

in Ihre Quelldateien importieren, damit der Compiler Ihren Programmcode übersetzen kann.


Galileo Computing - Zum Seitenanfang

6.3.1 LayerZur nächsten ÜberschriftZur vorigen Überschrift

Ein wichtiger Bestandteil von Core Animation sind Layer, die auch die Views des UIKits für ihre Darstellung verwenden. Ein Layer ist eine Zeichenebene, die Inhalte für die Anzeige bereitstellt, und seine Basisklasse ist CALayer. Dabei ist jedem View ein Layer zugeordnet, den Sie über die Property layer erhalten. Nach der Erzeugung eines Views ist dessen Layer fest, und Sie können dem View keinen neuen Layer zuweisen. Das ist im Allgemeinen auch nicht nötig, da Sie ja beliebige Grafiken über den View zeichnen können (siehe Kapitel 3, »Views und Viewcontroller«).

Die Klasse UIView hat allerdings die Klassenmethode layerClass, über die sie die Klasse ihres Layers bestimmt. Sie können in Ihren eigenen Unterklassen also diese Methode überschreiben, um so die Layer-Klasse des Views festlegen zu können. Das Beispielprojekt Pie enthält die Klasse PieView, die für die Darstellung die Layer-Klasse PieLayer verwendet, und Listing 6.30 enthält die Implementierung der Methode layerClass, um diese Zuordnung zu realisieren.

+ (Class)layerClass {
return [PieLayer class];
}

Listing 6.30 Festlegen der Layer-Klasse eines Views

Projektinformation

Den Quellcode des Beispielprojekts Pie finden Sie auf der DVD unter Code/Apps/Pie oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_2/Apps/Pie.

Layer können, analog zu Views, Sublayer haben. Wenn ein View Subviews hat, dann sind deren Layer die Sublayer des Layers des Views. Eine grafische Veranschaulichung dafür finden Sie in Abbildung 6.15.

Abbildung

Abbildung 6.15 View- und Layer-Hierarchie

Jeder View hat zwar einen Layer, nicht jeder Layer muss dagegen zu einem View gehören. Sie können also in einen Layer beliebig viele Sublayer einfügen. Die Verwaltung der Sublayer erfolgt dabei analog zu Views. Sie erhalten alle Sublayer eines Layers über die Property sublayers. Durch die Methode addSublayer: können Sie neue Sublayer zu einem Layer hinzufügen, und über die Property superlayer haben Sie Zugriff auf den darüber liegenden Layer.

Layer haben noch eine Reihe weiterer Eigenschaften. Sie finden im Layer viele Propertys, die Sie schon von der Klasse UIView kennen. Teilweise haben sie jedoch auch andere Namen. Beispielsweise heißt die analoge Methode zu clipsToBounds des Views im Layer masksToBounds, und das Gegenstück zu der View-Property alpha im Layer ist opacity.

Sehr praktisch ist beispielsweise die Property cornerRadius der Klasse CALayer. Damit können Sie die Ecken eines Layers abrunden. Dafür müssen Sie den Property-Wert größer als 0 setzen. Außerdem können Sie noch einen Schatten, eine Rahmenfarbe und eine Rahmenbreite wie beispielsweise in Listing 6.31 festlegen:

theLayer.cornerRadius = 10.0;
theLayer.borderColor = [UIColor blueColor].CGColor;
theLayer.borderWidth = 2.0;
theLayer.shadowColor = [UIColor blackColor].CGColor;
theLayer.shadowOffset = CGSizeMake(5.0, 5.0);
theLayer.shadowOpacity = 0.5;

Listing 6.31 Erzeugung eines Layer-Rahmens

Wenn Sie die Propertys wie dort setzen, erhalten Sie einen Rahmen um den Layer, wie ihn Abbildung 6.16 darstellt. Sie können diese Propertys natürlich auch bei dem Layer eines Views setzen, um die Ecken eines rechteckigen Views abzurunden. Das ist eine einfache Möglichkeit, um die Gestaltung der Oberflächen Ihrer Apps aufzulockern.

Ein Layer kann auch ein Delegate haben, das bei den Standardlayern der Views immer der View ist, und Sie dürfen diesen Layern kein anderes Delegate zuweisen. Bei allen anderen Layern können Sie hingegen das Delegate-Objekt frei wählen und über die Property delegate setzen.

Diplomatischer Skandal: Layer-Delegation hält sich an kein Protokoll

Es gibt kein Protokoll für das Layer-Delegate. Die Dokumentation der Delegate-Methoden finden Sie alle in der Klasse CALayer, die sie alle durch den Zusatz delegate method kennzeichnet. Die Deklaration befindet sich hingegen in der Kategorie NSObject(CALayerDelegate) in der Headerdatei der Layer-Klasse. Diese Art, Delegate-Methoden zu deklarieren, heißt auch informelles Protokoll.

Es gibt drei mögliche Wege, wie Sie den Inhalt eines Layers bereitstellen können. Über die Property contents können Sie dem Layer ein Core-Graphics-Bild (CGImageRef) zuweisen. Eine solche Referenz erhalten Sie beispielsweise über die Property CGImage eines Bildes der Klasse UIImage. In Listing 6.33 sehen Sie ein Beispiel, wie Sie den Inhalt über ein Bild bereitstellen können.

UIImage *theImage = [UIImage imageNamed:@"background.png"];
theLayer.contents = theImage.CGImage;

Listing 6.32 Setzen der »contents«-Property eines Layers

Der Layer-Inhalt lässt sich hingegen auch durch Zeichenoperationen erzeugen. Sie können dafür entweder eine Delegate-Methode verwenden oder eine Unterklasse erstellen. Für die Delegation implementieren Sie die Methode drawLayer:inContext:, wobei Sie den Kontextparameter für Ihre Zeichenoperationen verwenden:

- (void)drawLayer:(CALayer *)inLayer
inContext:(CGContextRef)inContext {
CGContextSaveGState(inContext);
// beliebige Zeichenoperationen
CGContextRestoreGState(inContext);
}

Listing 6.33 Inhalt eines Layers über Delegate-Methode zeichnen

Cocoa Touch ruft die Methode drawRect: über drawLayer:inContext: auf. Sie haben also bereits – ohne es zu ahnen – Layer-Inhalte über Delegation erzeugt.

UIView und drawLayer:inContext:

Wenn Sie den View-Inhalt über die Delegate-Methode anstatt mit drawRect: zeichnen möchten, dann müssen Sie trotzdem die Methode drawRect: überschreiben. Ansonsten ruft Cocoa Touch die Delegate-Methode nicht auf. Es reicht indes aus, wenn Sie die Methode leer lassen.

Der dritte Weg zur Bereitstellung des Layer-Inhalts ist das Überschreiben der Methode drawInContext: in einer Unterklasse von CALayer. Auch hier bekommen Sie den Kontext zum Zeichnen als Parameter übergeben.

Das Beispielprojekt Pie auf der beiliegenden DVD stellt ein Kreissegment dar, dessen Ausschnitt Sie über einen Schieberegler verändern können (siehe Abbildung 6.16). Die Darstellung des Segments basiert dabei auf einem Layer mit folgender Implementierung der Methode drawInContext:

- (void)drawInContext:(CGContextRef)inContext {
CGRect theBounds = self.bounds;
CGSize theSize = theBounds.size;
CGFloat thePart = self.part;
CGPoint theCenter = CGPointMake(CGRectGetMidX(theBounds), Zeilenumbruch
CGRectGetMidY(theBounds));
CGFloat theRadius = fminf(theSize.width, theSize.height) / Zeilenumbruch
2.0 – 5.0;
CGFloat theAngle = 2 * (thePart – 0.25) * M_PI;

CGContextSaveGState(inContext);
CGContextSetFillColorWithColor(inContext, Zeilenumbruch
[UIColor redColor].CGColor);
CGContextMoveToPoint(inContext, theCenter.x, theCenter.y);
CGContextAddArc(inContext, theCenter.x, theCenter.y, Zeilenumbruch
theRadius, -M_PI / 2.0, theAngle, NO);
CGContextAddLineToPoint(inContext, theCenter.x, Zeilenumbruch
theCenter.y);
CGContextFillPath(inContext);
CGContextRestoreGState(inContext);
}

Listing 6.34 Zeichnen des Kreissegments im Layer

Abbildung

Abbildung 6.16 Das »Pie«-Beispielprogramm

Die Property part enthält die Größe des Kreissegments als Wert zwischen 0 und 1. Ein Layer kann über die Methoden valueForKey: und setValue:forKey: beliebige Werte speichern, und die Klasse PieLayer speichert den Wert für die Property part auch über diese Methoden. Die Deklaration dieser Property in der Klasse PieLayer erfolgt zwar über ein gewohntes

@property (nonatomic) CGFloat part;

für die Implementierung reicht hingegen die Anweisung @dynamic part; aus.

Achtung bei automatisch synthetisierten Propertys

Mit Xcode 4.5 hat sich das Verhalten des Compilers geändert: In den älteren Versionen konnten Sie die @dynamic-Anweisung auch einfach weglassen. Der Compiler hat dann zwar eine Warnung ausgespuckt, das Programm hat indes trotzdem funktioniert. Xcode 4.5 nimmt bei fehlender Implementierungsanweisung für eine Property an, dass die Applikation sie synthetisieren soll.

Durch die dynamische Implementierung der Property ruft die Laufzeitumgebung immer, wenn Sie lesend oder schreibend auf part zugreifen, die Methoden valueForKey: beziehungsweise setValue:forKey: mit @"part" als Schlüssel auf. Dabei packt sie den Fließkommawert immer schön aus dem NSNumber-Objekt aus beziehungsweise in ein solches Objekt ein.

Layer-Eigenschaften über Propertys

Legen Sie die Propertys eines Layers möglichst immer dynamisch an. Sie können zwar auch synthetisierte Propertys verwenden oder eigene Implementierungen dafür schreiben. Allerdings dürfen Sie in der Implementierung nicht die Key-Value-Coding-Methoden mit dem Property-Namen als Schlüssel verwenden. Die Implementierung

- (CGFloat)part {
return [[self valueForKey:@"part"] floatValue];
}

führt zu einer Endlosrekursion, da der Aufruf von valueForKey: wieder die Methode part aufruft. Sie bekommen indes weiteren Ärger, wenn Sie diese Propertys animieren wollen. Also nutzen Sie lieber das Key-Value-Coding. Das ist ein Angebot, das Sie einfach nicht ablehnen können.

Natürlich sollte der Layer seine Propertys auch mit Werten vorbelegen. Dafür stellt die Klasse CALayer die Klassenmethode defaultValueForKey: zur Verfügung, die Sie überschreiben können. Sie liefert für die Schlüssel des Layers entweder den Standardwert zurück oder reicht den Aufruf an die Methode der Oberklasse weiter. Da diese Methode den Rückgabetyp id hat, muss sie den Standardwert für die Property part als Objekt liefern.

static NSString * const kPartKey = @"part";

+ (id)defaultValueForKey:(NSString *)inKey {
return [kPartKey isEqualToString:inKey] ? Zeilenumbruch
[NSNumber numberWithFloat:0.0] : Zeilenumbruch
[super defaultValueForKey:inKey];
}

Listing 6.35 Standardwert für die Property »part«

Wenn Sie jetzt die Klasse PieLayer verwenden, sehen Sie noch nichts – zumindest kein Tortenstück. Das könnte daran liegen, dass die Größe des Segments den Wert 0 hat. Jedoch selbst mit einem anderen Standardwert für part gibt’s immer noch keinen Kuchen, und die Fläche bleibt immer noch weiß.

Das liegt daran, dass niemand dem Layer gesagt hat, dass er etwas zeichnen soll. Sie können das erreichen, indem Sie ihm die Nachricht setNeedsDisplay schicken. Das müssen Sie immer machen, wenn Sie den Wert für part verändern. Das ist allerdings sehr unpraktisch, da Sie dafür entweder die Methode setPart: implementieren oder KVO verwenden müssten.

Um dieses Problem zu umgehen, bietet die Layer-Klasse über die Klassenmethode needsDisplayForKey: eine einfachere Möglichkeit an. Core Animation zeichnet den Layer bei einer Änderung eines Property-Wertes automatisch neu, wenn die Methode für den Property-Namen YES liefert. Sie können dazu diese Methode in Ihren Klassen einfach wie in Listing 6.36 überschreiben.

+ (BOOL)needsDisplayForKey:(NSString *)inKey {
return [kPartKey isEqualToString:inKey] || Zeilenumbruch
[super needsDisplayForKey:inKey];
}

Listing 6.36 Layer bei Änderung des Property-Wertes neu zeichnen

Layer-Propertys gesucht

Core Animation ruft die Methode needsDisplayForKey: einmal für jede Property der Layer-Klasse auf. Sie sollten auch deshalb die Layer-Eigenschaften als dynamische Property deklarieren. Für Eigenschaften, die Sie nur über Getter und Setter deklariert haben, ruft Core Animation needsDisplayForKey: hingegen nicht auf. Das klingt ja schon wieder nach einem Angebot, das Sie nicht ablehnen können.

Durch die Implementierung der dynamischen Property und der drei Methoden defaultValueForKey:, needsDisplayForKey: und drawInContext: zeigt der Layer nun auch ein Tortenstück an.


Galileo Computing - Zum Seitenanfang

6.3.2 Vordefinierte Layer-KlassenZur nächsten ÜberschriftZur vorigen Überschrift

Core Animation stellt auch fertige Unterklassen von CALayer für verschiedene Anwendungsfälle bereit, deren Handhabung das Beispielprojekt Layer zeigt. Die Ausgabe der beschriebenen Layer sehen Sie in Abbildung 6.17.

Projektinformation

Den Quellcode des Beispielprojekts Layer finden Sie auf der DVD unter Code/Apps/Layer oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_2/Apps/Layer.

Abbildung

Abbildung 6.17 Ausgabe der Layer-App

Mit einem Layer der Klasse CAGradientLayer können Sie Farbverläufe darstellen, die auf zwei oder mehr Farben basieren. Die Farben übergeben Sie an den Layer als CG-ColorRef-Werte in einem NSArray. Der Farbverlauf verläuft entlang einer Linie, die Sie über die Propertys startPoint und endPoint festlegen.

Abbildung

Abbildung 6.18 Gradientenverlauf zwischen Start- und Endpunkt

Relative Positionsangaben bei Layern

Positionen oder Rechtecke innerhalb eines Layers müssen Sie in der Regel durch Werte zwischen 0 und 1 beschreiben und nicht durch absolute Koordinaten. Den Ankerpunkt eines Layers können Sie beispielsweise über die Property anchorPoint festlegen. Standardmäßig ist das der Mittelpunkt des Layers, und diese Property hat somit den Wert (0,5, 0,5).

Abbildung 6.18 enthält zwei Gradientenverläufe mit unterschiedlichen Start- und Endpunkten. Die Lage der Punkte verdeutlicht dabei jeweils eine Verbindungslinie auf dem Gradienten. Der linke Verlauf startet in der Ecke links oben und endet unten rechts. Der Startpunkt hat also den Wert (0, 0) und der Endpunkt (1, 1). Der rechte Gradient verläuft von oben nach unten, was sich über den Startpunkt (0,5, 0) und den Endpunkt (0,5, 1) realisieren lässt.

Das Beispielprojekt legt einen Gradienten-Layer mit drei Farben und einem Verlauf von oben links nach unten rechts folgendermaßen an:

CAGradientLayer *theLayer = [CAGradientLayer layer];
NSArray *theColors = [NSArray arrayWithObjects: Zeilenumbruch
(id)[UIColor redColor].CGColor, Zeilenumbruch
(id)[UIColor greenColor].CGColor, Zeilenumbruch
(id)[UIColor blueColor].CGColor, nil];

self.frame = inFrame;
theLayer.colors = theColors;
theLayer.startPoint = CGPointMake(0.0, 0.0);
theLayer.endPoint = CGPointMake(1.0, 1.0);

Listing 6.37 Anlegen eines Layers mit Gradientenverlauf

Über Layer der Klasse CATextLayer können Sie Texte darstellen. Die Implementierung des Layers beruht auf Core Text. Das ist ein Low-Level-Framework für die Ausgabe von Texten. Sie können über diesen Layer auch Texte mit Hervorhebungen (z. B. fett oder kursiv) und unterschiedlichen Schriftarten ausgeben. In Listing 6.38 sehen Sie ein Beispiel, wie Sie einen Textlayer anlegen können. Dabei sorgt die Property wrapped dafür, dass der Text im Layer umbricht.

CATextLayer *theLayer = [CATextLayer layer];
CGAffineTransform theIdentity = CGAffineTransformIdentity;
CTFontRef theFont = CTFontCreateWithName( Zeilenumbruch
(CFStringRef)@"Courier", 24.0, &theIdentity);

theLayer.frame = inFrame;
theLayer.font = theFont;
theLayer.fontSize = 20.0;
theLayer.backgroundColor = [UIColor whiteColor].CGColor;
theLayer.foregroundColor = [UIColor blackColor].CGColor;
theLayer.wrapped = YES;
theLayer.string = @"Die heiße Zypernsonne quälte Max und Zeilenumbruch
Victoria ja böse auf dem Weg bis zur Küste.";e
CFRelease(theFont);

Listing 6.38 Anlegen eines Textlayers

Die Klasse CAScrollLayer erlaubt die Anzeige von Inhalten, die größer sind als der verfügbare Bereich für die Darstellung. Sie zeigen also wie ein UIScrollView immer nur einen Ausschnitt des Inhalts an. Im Gegensatz zum View verarbeitet der Layer hingegen keine Scrollgesten, und er zeigt auch keine Scrollbalken an.

Sie können den angezeigten Ausschnitt des Scroll-Layers über die Methoden scrollToPoint: und scrollToRect: festlegen. Über scrollToPoint: legen Sie die linke obere Ecke des Ausschnitts für die Anzeige fest. Dabei geben Sie den Punkt in absoluten Koordinaten zum Layer-Inhalt an. Bei scrollToRect: übergeben Sie ein Rechteck aus dem Koordinatensystem des Inhalts. Der Scroll-Layer stellt dann einen Ausschnitt des Inhalts dar, der dieses Rechteck enthält. Listing 6.39 legt einen Scroll-Layer an, der einen Text-Layer enthält.

CAScrollLayer *theLayer = [CAScrollLayer layer];
CATextLayer *theTextLayer = ...;

theLayer.frame = inFrame;
[theLayer addSublayer:theTextLayer];
theLayer.scrollMode = kCAScrollVertically;

Listing 6.39 Anlegen eines Scrolllayers

Ein Layer der Klasse CAShapeLayer stellt einen Core-Graphics-Pfad dar. Dabei können Sie diesen Pfad über die Property path setzen. Auch für die anderen üblichen Verdächtigen gibt es entsprechende Propertys. Sie können beispielsweise die Füllfarbe über fillColor und die Linienfarbe über strokeColor setzen.

CAShapeLayer *theLayer = [CAShapeLayer layer];
CGMutablePathRef thePath = CGPathCreateMutable();
CGAffineTransform theIdentity = CGAffineTransformIdentity;
CGFloat theOffset = CGRectGetHeight(inFrame) / 2.0;
CGFloat theWidth = CGRectGetWidth(inFrame);

theLayer.frame = inFrame;
theLayer.backgroundColor = [UIColor whiteColor].CGColor;
theLayer.strokeColor = [UIColor blackColor].CGColor;
theLayer.fillColor = [UIColor clearColor].CGColor;
theLayer.lineWidth = 1.0;
CGPathMoveToPoint(thePath, &theIdentity, 0.0, f(0.0) + theOffset);
for(CGFloat x = 1.0; x < theWidth; x += 1.0) {
CGPathAddLineToPoint(thePath, &theIdentity, x, Zeilenumbruch
f(x * M_PI / theWidth) + theOffset);
}
theLayer.path = thePath;
CGPathRelease(thePath);

Listing 6.40 Erzeugung eines Shapelayers mit einem Pfad

Wie Sie in Listing 6.40 sehen, erzeugen Sie Core-Graphics-Pfade analog zu den Zeichenpfaden in einem Grafikkontext. Im Gegensatz zu den Kontextpfaden können Sie die Pfade der Typen CGPathRef und CGMutablePathRef mehrmals verwenden und auch als Werte in Ihren Objekten speichern. Auch der Grafikkontext kann mit diesen Pfaden umgehen; er unterstützt verschiedene Funktionen, mit denen Sie Pfade zeichnen, füllen oder zum Kontext hinzufügen können.


Galileo Computing - Zum Seitenanfang

6.3.3 Unser Button soll schöner werdenZur nächsten ÜberschriftZur vorigen Überschrift

Die Klasse GradientButton der Games-App verwendet einen Gradienten-Layer, um optisch ansprechende Buttons darzustellen. Dazu fügt er in seiner Methode awakeFromNib einen weiteren Sublayer hinzu:

- (void)awakeFromNib {
[super awakeFromNib];
CALayer *theLayer = self.layer;
CAGradientLayer *theBackground = [CAGradientLayer layer];

theLayer.cornerRadius = 10.0;
theLayer.masksToBounds = YES;
theBackground.frame = theLayer.bounds;
theBackground.startPoint = CGPointMake(0.5, 0.2);
theBackground.endPoint = CGPointMake(0.5, 0.9);
theBackground.colors = self.normalColors;
theBackground.zPosition = –1;
[theLayer addSublayer:theBackground];
self.backgroundLayer = theBackground;
}

Listing 6.41 Hinzufügen eines Layers für den Hintergrund

Der Layer für den Hintergrund erhält die z-Position (siehe Abschnitt 6.3.9, »Die 3. Dimension«) –1, damit er hinter den anderen Elementen des Buttons liegt. Da der Button einen transparenten Hintergrund hat, verdeckt er indes nicht den Hintergrundlayer. Wenn Sie den Button drücken, verändert sich die Farbe des Verlaufs wie beim Redo-Button in Abbildung 6.19. Dazu besitzt die Klasse zwei Getter, die jeweils ein Array mit Farben liefern. Die Arrays müssen allerdings die Farbwerte als CGColorRef-Werte enthalten:

- (NSArray *)normalColors {
return [NSArray arrayWithObjects:Zeilenumbruch
(id)[UIColor colorWithRed:0.4 green:0.4 blue:1.0 Zeilenumbruch
alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.0 green:0.0 blue:0.6 Zeilenumbruch
alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.0 green:0.0 blue:0.8 Zeilenumbruch
alpha:1.0].CGColor, nil];
}

- (NSArray *)highligthedColors {
return [NSArray arrayWithObjects:Zeilenumbruch
(id)[UIColor colorWithRed:1.0 green:0.4 blue:0.4 Zeilenumbruch
alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.6 green:0.0 blue:0.0 Zeilenumbruch
alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.8 green:0.0 blue:0.0 Zeilenumbruch
alpha:1.0].CGColor, nil];
}

Listing 6.42 Definition der Verlaufsfarben für den Hintergrund

Abbildung

Abbildung 6.19 Änderung des Hintergrundverlaufs bei gedrückten Buttons

Damit der Button die Farbe beim Drücken verändert, überschreiben Sie die Methode setHighlighted:, die Cocoa Touch bei dieser Zustandsänderung aufruft.

- (void)setHighlighted:(BOOL)inHighlighted {
super.highlighted = inHighlighted;
if(inHighlighted) {
self.backgroundLayer.colors = self.highligthedColors;
}
else {
self.backgroundLayer.colors = self.normalColors;
}
}

Listing 6.43 Änderung des Farbverlaufs beim Drücken des Buttons

Das sind auch schon alle Methoden, die der Gradientenbutton benötigt. Wenn Sie einen solchen Button im Interface Builder anlegen möchten, fügen Sie dafür ein Objekt der Klasse UIButton an, dem Sie im Attributinspektor den Typ Custom und Weiß als Textfarbe zuordnen; über den Identitätsinspektor weisen Sie ihm außerdem noch die Klasse GradientButton zu.


Galileo Computing - Zum Seitenanfang

6.3.4 Spieglein, Spieglein an der WandZur nächsten ÜberschriftZur vorigen Überschrift

Sie können auch auf den Layer-Inhalt zugreifen, beispielsweise um Screenshots zu erzeugen. Aber auch andere Möglichkeiten, wie beispielsweise Spiegelungen oder Vergrößerungen (Lupeneffekt), stehen Ihnen damit offen. Die beiden Spiele verwenden einen View der Klasse NumberView, um ihre Spielstände anzuzeigen. In diesem View liegen jeweils drei Views der Klasse DigitView, die jeweils die einzelnen Ziffern darstellen. Der Numberview erzeugt auch eine gespiegelte, abgeschwächte Kopie der Ziffern, sodass der Eindruck einer glänzenden Oberfläche entsteht.

Abbildung

Abbildung 6.20 Beispiel für eine Spiegelung

Für einen Spiegelungseffekt müssen Sie eine vertikal gespiegelte und gestauchte Kopie des Originalbildes erstellen. Außerdem muss sich das gespiegelte Bild mit zunehmender Entfernung vom Originalbild abschwächen. Ein Beispiel für eine Spiegelung stellt Abbildung 6.20 dar, wo Sie diese drei Effekte – spiegelverkehrte Darstellung, Stauchung und Abschwächung – sehen können.

Tipp

Die Klasse NumberView spiegelt nach diesem Prinzip alle enthaltenen Views. Sie können die nachfolgend beschriebenen Methoden aus der Kategorie UIView(MirrorImage) auch in eigene View-Klassen integrieren, um Spiegelungen für beliebige Views zu erhalten.

Die erste und vielleicht auch schwierigste Aufgabe, um die Spiegelung zu erzeugen, ist die Bestimmung des Layer-Inhalts. Sie müssen dazu den Inhalt in ein Bild zeichnen. Die Klasse CALayer stellt für das Zeichnen der Layer-Hierarchie die Methode renderInContext: zur Verfügung, die den Layer-Inhalt in einen beliebigen Grafikkontext zeichnet. Das Abbild eines Views wird im Beispiel über die Methode mirroredImageWithScale: in der Kategorie UIView(MirrorImage) erzeugt. Sie erstellt ein vertikal gespiegeltes und gestauchtes Abbild eines Views, wobei Sie über den Parameter den Grad der Stauchung bestimmen können. Wenn Sie hierfür negative Werte verwenden, schalten Sie die Spiegelung aus. Sie können also über den Wert –1 ein genaues Abbild des Views erzeugen.

Damit das Bild auf einem Retina-Display nicht unscharf wirkt, erzeugt die Methode den Grafikkontext mit der gleichen Pixelskalierung, wie der Bildschirm, wozu sie die Property scale des Hauptbildschirms ausliest. Diesen Wert können Sie für den dritten Parameter der Funktion UIGraphicsBeginImageContextWithOptions verwenden, um Bilder in der entsprechenden Auflösung zu erzeugen.

- (UIImage *)mirroredImageWithScale:(CGFloat)inScale {
CALayer *theLayer = self.layer;
CALayer *thePresentationLayer = Zeilenumbruch
[theLayer presentationLayer];
CGRect theFrame = self.frame;
CGSize theSize = theFrame.size;
UIScreen *theScreen = [UIScreen mainScreen];
CGContextRef theContext;
UIImage *theImage;

if(thePresentationLayer) {
theLayer = thePresentationLayer;
}
if([theScreen respondsToSelector:@selector(scale)]) {
CGFloat theScreenScale = [theScreen scale];

UIGraphicsBeginImageContextWithOptions(Zeilenumbruch
theSize, NO, theScreenScale);
}
else {
UIGraphicsBeginImageContext(theSize);
}
theContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(theContext, 1.0, -inScale);
CGContextTranslateCTM(theContext, 0.0, -theSize.height);
[theLayer renderInContext:theContext];
theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
}

Listing 6.44 Erzeugung eines gespiegelten Abbildes eines Views

Das doppelte Layerchen

Core Animation verwendet für die Darstellung eines Layers zwei weitere Layer. Der Präsentations-Layer enthält den aktuellen Zustand des Layers. In der Regel ist er einfach eine Kopie des Layers des Views. Während einer Animation unterscheiden sich jedoch die Werte der animierten Propertys des Präsentations-Layers von dem Original, da der Präsentations-Layer den aktuellen Animationszustand darstellt. Weil die Spiegelung auch mit Animationen funktionieren soll, verwendet Listing 6.44 den Präsentations-Layer des Layers, sofern er einen besitzt.

Vor der Befüllung des Kontexts über den Aufruf der Methode renderInContext: müssen Sie die komplette Layer-Hierarchie für das Neuzeichnen markieren. Dazu senden Sie dem Layer und allen Sublayern die Nachricht setNeedsDisplay. Eine Ausnahme sind die Layer, die ihren Inhalt über die Property contents bereitstellen. Diese Aufrufe erfolgen über die rekursive Methode setAllNeedsDisplay der Kategorie CALayer(MirrorImage).

- (void)setAllNeedsDisplay {
if(self.contents == nil) {
[self setNeedsDisplay];
}
for(id theLayer in self.sublayers) {
[theLayer setAllNeedsDisplay];
}
}

Listing 6.45 Rekursives Markieren zum Neuzeichnen

Da Sie jetzt ein gestauchtes und gespiegeltes Abbild eines Views erzeugen können, fehlt nur noch der Abschwächungseffekt, bei dem die Intensität der Farben des Spiegelbildes mit zunehmender Entfernung vom Originalbild nachlässt. Dieser Effekt lässt sich über einen grauen Gradientenverlauf und die Funktion CGContextClipToMask erreichen. Die Maske ist dabei der Graustufenverlauf mit Grauwerten von 80 % bis 0 %. Der Kontext verwendet jeweils den Grauwert der Maske als Alphawert für den Pixel, sodass dunklere Grauwerte zu durchsichtigeren Pixeln führen. Der Verlauf ist ebenfalls ein Bild, das die Methode createGradientImageWithSize:gray: erzeugt; es braucht allerdings nur einen Punkt breit zu sein, da es CGContextClipToMask automatisch auf die richtige Breite streckt.

- (void)drawMirrorWithScale:(CGFloat)inScale {
CGRect theFrame = self.frame;
CGSize theSize = theFrame.size;
CGPoint thePoint = CGPointMake(Zeilenumbruch
CGRectGetMinX(theFrame), CGRectGetMaxY(theFrame));
CGFloat theHeight = theSize.height * inScale;
CGImageRef theGradient = [self createGradientImageWithSize: Zeilenumbruch
CGSizeMake(1.0, theHeight) gray:0.8];
CGContextRef theContext = UIGraphicsGetCurrentContext();
UIImage *theImage = [self mirroredImageWithScale:inScale];
CGRect theRect;

theRect.origin = thePoint;
theRect.size = theSize;
CGContextSaveGState(theContext);
CGContextClipToMask(theContext, theRect, theGradient);
[theImage drawAtPoint:thePoint];
CGImageRelease(theGradient);
CGContextRestoreGState(theContext);
}

Listing 6.46 Zeichnen der Spiegelung mit einer Graustufenmaske

Die Methode drawMirrorWithScale: positioniert die Maske und das Spiegelbild an der linken unteren Ecke des Views. Dabei erzeugt die Methode createGradientImageWithSize:gray: einen Graustufenverlauf vom angegebenen Wert bis 0 %. Die Implementierung beruht dabei komplett auf Core Graphics. Der Bitmapkontext in der Methode benutzt einen Graustufenfarbraum, der nur zwei Kanäle (Weiß- und Alphawert) besitzt. Aus diesem Grund muss das Array theColors mit den Farben für den Verlauf auch nur vier Werte statt acht enthalten.

- (CGImageRef)createGradientImageWithSize:(CGSize)inSize Zeilenumbruch
gray:(float)inGray {
CGImageRef theImage = NULL;
CGColorSpaceRef theColorSpace = Zeilenumbruch
CGColorSpaceCreateDeviceGray();
CGContextRef theContext = CGBitmapContextCreate(Zeilenumbruch
NULL, inSize.width, inSize.height, 8, 0, Zeilenumbruch
theColorSpace, kCGImageAlphaNone);
CGFloat theColors[] = {inGray, 1.0, 0.0, 1.0};
CGGradientRef theGradient = Zeilenumbruch
CGGradientCreateWithColorComponents(Zeilenumbruch
theColorSpace, theColors, NULL, 2);

CGContextDrawLinearGradient(theContext, theGradient, Zeilenumbruch
CGPointZero, CGPointMake(0, inSize.height), Zeilenumbruch
kCGGradientDrawsAfterEndLocation);
CGColorSpaceRelease(theColorSpace);
CGGradientRelease(theGradient);
theImage = CGBitmapContextCreateImage(theContext);
CGContextRelease(theContext);
return theImage;
}

Listing 6.47 Erzeugung des Graustufenverlaufs als Bild

Die Klasse NumberView verwendet nun die Methode drawMirrorWithScale: in ihrer Implementierung von drawRect:, um den Spiegelungseffekt zu erzeugen. Dabei erzeugt sie für jeden Subview eine eigene Spiegelung (siehe Listing 6.48) mit dem Skalierungsfaktor 0,6.

- (void)drawRect:(CGRect)inRect {
for(UIView *theView in self.subviews) {
[theView drawMirrorWithScale:0.6];
}
}

Listing 6.48 Spiegelung der Subviews

Abbildung 6.21 zeigt die Spiegelung der Klasse NumberView in der Applikation. Wie bereits erwähnt wurde, funktioniert sie auch während der Animation der enthaltenen Layer, sodass der Effekt sehr realistisch wirkt. Für die Anzeige der Ziffern verwendet der Numberview drei Views mit der Klasse DigitView, die einen animierten Wechsel ihrer Ziffern erlauben.

Wie der Pieview besitzt auch der Numberview einen Layer mit einer eigenen Klasse, die allerdings relativ klein ist und keine eigene Header- und Implementierungsdatei besitzt; der komplette Code befindet sich stattdessen in der Datei DigitView.m. Sie stellt nur die dynamische Property digit zur Verfügung. Listing 6.49 enthält die komplette Deklaration und Implementierung:

@interface DigitLayer : CALayer

@property (nonatomic) CGFloat digit;

@end

@implementation DigitLayer

@dynamic digit;

+ (id)defaultValueForKey:(NSString *)inKey {
return [inKey isEqualToString:kDigitKey] ? Zeilenumbruch
[NSNumber numberWithFloat:0.0] : Zeilenumbruch
[super defaultValueForKey:inKey];
}

+ (BOOL)needsDisplayForKey:(NSString *)inKey {
return [inKey isEqualToString:kDigitKey] || Zeilenumbruch
[super needsDisplayForKey:inKey];
}

Listing 6.49 Der Layer für die Darstellung der Ziffern

Abbildung

Abbildung 6.21 Spiegelung der Ziffern des Zugzählers

Obwohl die Ziffern nur ganzzahlige Werte zwischen 0 und 10 annehmen können, speichert der Layer sie als Fließkommazahl ab. Er soll bei der Animation allerdings auch Zwischenzustände darstellen können, was sich über ganze Zahlen nur sehr schwer realisieren lässt. Die Darstellung übernimmt der Digitview, der ja das Delegate des Layers ist. Er zeichnet dazu zwölf Ziffern (9, 0 bis 9 und noch mal die 0) untereinander, wobei er die vertikale Startposition so verschiebt, dass genau die richtige Ziffer im sichtbaren Bereich des Views zu sehen ist. Dabei stellt er durch ein geeignetes Clipping sicher, dass nur die gewünschten Teile sichtbar sind. Abbildung 6.22 stellt den Wechsel von der Ziffer 5 zur 6 dar.

Abbildung

Abbildung 6.22 Ziffernwechsel des Digitviews

Der View zeichnet den Layer-Inhalt über die Delegate-Methode drawLayer:inContext:, die Listing 6.50 enthält. Die Implementierung basiert dabei komplett auf Core Graphics und verwendet größtenteils Funktionen, die Sie bereits in Kapitel 3, »Views und Viewcontroller«, kennengelernt haben.

Zur Darstellung der Texte für die Ziffern legt die Methode in Listing 6.50 zunächst den Font über die Funktion CGContextSelectFont und eine Transformationsmatrix über CGContextSetTextMatrix fest. Da Core Graphics Texte normalerweise vertikal gespiegelt darstellt, gleicht die Transformationsmatrix im Listing das wieder aus.

- (void)drawLayer:(CALayer *)inLayer Zeilenumbruch
inContext:(CGContextRef)inContext {
CGRect theBounds = self.bounds;
CGSize theSize = theBounds.size;
float theDigit = [(DigitLayer *)inLayer digit];
UIFont *theFont = self.font;
CGSize theFontSize = [@"0" sizeWithFont:theFont];
CGFloat theX = (theSize.width – theFontSize.width) / 2.0;
CGFloat theY = (theFont.capHeight – theSize.height) / 2.0;

theY -= theDigit * theSize.height;
CGContextSaveGState(inContext);
CGContextClipToRect(inContext, theBounds);
CGContextSetFillColorWithColor(inContext, Zeilenumbruch
self.backgroundColor.CGColor);
CGContextFillRect(inContext, theBounds);
CGContextSetRGBFillColor(inContext, 0.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(inContext, 0.0, 0.0, 0.0, 1.0);
CGContextSelectFont(inContext, [theFont.fontName Zeilenumbruch
cStringUsingEncoding:NSMacOSRomanStringEncoding], Zeilenumbruch
theFont.pointSize, kCGEncodingMacRoman);
CGContextSetTextMatrix(inContext, Zeilenumbruch
CGAffineTransformMakeScale(1.0, –1.0));
for(int i = 9; i <= 20; ++i) {
char theCharacter = '0' + (i % 10);

CGContextShowTextAtPoint(inContext, theX, theY, Zeilenumbruch
&theCharacter, 1);
theY += theSize.height;
}
CGContextRestoreGState(inContext);
[self.superview setNeedsDisplay];
}

Listing 6.50 Zeichnen der Ziffern im Digitview

Die Variablen theX und theY enthalten jeweils die Koordinaten der Ziffern, wobei der x-Wert fest bleibt und der y-Wert sich in jedem Schleifendurchlauf um die Höhe des Views erhöht. Die Anfangswerte für diese beiden Variablen sind jeweils die Abstände zum Rand des Views bei zentrierter Positionierung des Zeichens »0« im View; außerdem zieht die Methode noch für jede Ziffer, die vor dem aktuellen Wert liegt, jeweils einmal die Viewhöhe vom y-Wert ab.

Die etwas seltsamen Randwerte für die Schleifenvariable i kommen übrigens durch die eigentliche Zahlenfolge 9, 0, 1, ..., 9, 0 zustande, die sich relativ einfach über den Divisionsrest mit 10 berechnen lässt (siehe Kapitel 3). Sie können das leicht nachprüfen: 9 = 9 % 10, 0 = 10 % 10, 1 = 11 % 10 und so weiter.

Es geht auch einfacher

Mit der Klasse CAReplicationLayer lässt sich der Programmieraufwand für den Spiegelungseffekt reduzieren; allerdings ist damit auch der Effekt nicht ganz so schön wie mit dem bereits vorgestellten Weg und der Replication-Layer sollte nur gleich hohe Subviews spiegeln, da der Layer die gleiche Transformationsmatrix auf alle Sublayer anwendet.

@implementation MirrorView

+ (Class)layerClass {
return [CAReplicatorLayer class];
}

- (void)awakeFromNib {
[super awakeFromNib];
CAReplicatorLayer *theLayer = Zeilenumbruch
(CAReplicatorLayer *) self.layer;
UIView *theSubview = [self.subviews objectAtIndex:0];
CATransform3D theTransform = CATransform3DIdentity;
CGFloat theHeight = –0.6 * Zeilenumbruch
CGRectGetHeight(theSubview.frame);
CATransform3D theScale = CATransform3DMakeScale(Zeilenumbruch
1.0, –0.6, 1.0);

theLayer.instanceAlphaOffset = –0.3;
theLayer.instanceCount = 2;
theLayer.instanceTransform = CATransform3DTranslate(
theScale, 0.0, theHeight, 0.0);
}

@end

Der Datentyp CATransform3D stellt affine Transformationsmatrizen im dreidimensionalen Raum dar, deren Verwendung der von CGAffineTransform sehr ähnelt. Die Property instanceCount legt die Anzahl der Kopien fest; in diesem Fall ein Original und eine Kopie für die Spiegelung. Der Layer addiert den Wert der Property instanceAlphaOffset zu dem Alphawert der Spiegelung hinzu. Ein Beispiel für eine solche Spiegelung sehen Sie in Abbildung 6.23.

Sowohl die Erzeugung als auch die Darstellung einer Spiegelung über Replication-Layer ist also einfacher als der Weg über die Methode renderInContext:.

Abbildung

Abbildung 6.23 Spiegelungseffekt für Arme

So, jetzt wird es aber langsam Zeit, dass Sie Ihren Layer Beine machen und sie in Bewegung setzen.


Galileo Computing - Zum Seitenanfang

6.3.5 Der bewegte LayerZur nächsten ÜberschriftZur vorigen Überschrift

Sie können Layer mit Animationen versehen und dadurch ihre Inhalte bewegen. Dadurch ergeben sich weitere Möglichkeiten für eine ansprechendere Oberflächengestaltung. Dabei sind Animationen Objekte, deren Klassen die Basisklasse CAAnimation haben, und Sie starten sie durch das Hinzufügen zum Animationsverzeichnis des Layers. Das geschieht über die Methode addAnimation:forKey:.

Wenn Sie eine Animation über eine der beiden Methoden removeAnimationForKey: oder removeAllAnimations aus dem Verzeichnis entfernen, stoppt sie sofort, und der Layer zeigt den Zustand seiner Werte an. Nehmen wir an, die Property opacity eines Layers hat den Wert 1 und Sie animieren diesen Wert von 1 nach 0. Wenn Sie die Animation beim Wert 0,5 aus dem Verzeichnis entfernen, springt die Darstellung sofort auf den Wert 1 um. Das passiert sogar, wenn Core Animation die Animation nach ihrem regulären Ende automatisch entfernt. Der Property-Wert von opacity ist also in diesem Beispiel nach der Animation immer 1.

Gibt es ein Leben nach der Animation?

Das automatische Entfernen lässt sich über die Property removeOnCompletion der Animation unterbinden. Wenn sie den Wert NO hat, verbleibt die Animation nach Ablauf im Animationsverzeichnis des Layers, der dann auch weiterhin den Endwert der Animation für die Property verwendet. Im Beispiel hätte der Layer in diesem Fall auch nach Ablauf der Animation den Wert 0 für opacity; allerdings nur so lange, bis Sie die Animation aus dem Verzeichnis entfernen. In der Regel ist es besser, die Property vor der Animation auf den gewünschten Endwert zu setzen. Dadurch hat der Layer nach der Animation immer diesen Wert.

Die Property duration der Animation legt die Anzahl der Zeiteinheiten der Animation fest. Eine Zeiteinheit ist in der Regel eine Sekunde; Sie können die Dauer aber über die Property speed anpassen.

Bei einer kontinuierlichen Animation setzen Sie über die Property repeatCount die Anzahl der Wiederholungen. Die Gesamtzeit beträgt in diesem Fall repeatCount * duration Zeiteinheiten. Alternativ können Sie auch über repeatDuration die Gesamtzeit festlegen, und die Anzahl der Wiederholungen ist repeatDuration / duration. Wenn Sie die Property autoreverses auf YES setzen, ändert die Animation jeweils am Ende ihre Richtung, wodurch sich außerdem entweder die Gesamtzeit verdoppelt oder die Anzahl der Wiederholungen halbiert.

Core Animation stellt Ihnen drei grundlegende Animationstypen zur Verfügung:

  • Property-Animationen
  • Transitionen
  • Animationsgruppen

Wir stellen Ihnen diese Typen im Folgenden genauer vor.

Property-Animationen

Dieser Animationstyp verändert den Wert einer Layer-Property während der Ausführung. Er basiert auf der Klasse CAPropertyAnimation, deren Objekten Sie den Keypath auf die zu animierende Property angeben müssen. Um diesen Animationstyp anzuwenden, verwenden Sie die Unterklassen CABasicAnimation oder CAKeyframeAnimation. Alternativ verwendet die Animation den Schlüssel aus dem Animationsverzeichnis, wenn Sie sie über den Convenience-Konstruktor animation anlegen. Dadurch können Sie mit einem Animationsobjekt mehrere unterschiedliche Property-Werte animieren. Beispielsweise können Sie die Property opacity folgendermaßen animieren:

CABasicAnimation *theAnimation = [CABasicAnimation animation];

theLayer.opacity = 0.0;
theAnimation.fromValue = @1;
theAnimation.toValue = @0;
theAnimation.duration = 1.0;
[theLayer addAnimation:theAnimation forKey:@"opacity"];

Listing 6.51 Einfache Animation eines Property-Wertes

Abhängig von der Property können dabei die Interpolationswerte Objekte der Klassen NSNumber (für Fließkommawerte) oder NSValue (für die Strukturen CGPoint, CGSize, CGRect und CATransform3D) sein.

Eine Animation der Klasse CABasicAnimation interpoliert zwischen zwei Property-Werten. Sie können dabei über die Propertys fromValue und toValue den Start- beziehungsweise den Endwert der Animation festlegen oder über die Property byValue den Abstand vom Start- oder Endwert angeben. Dabei sind unterschiedliche Kombinationen beim Setzen dieser Propertys möglich. Vier mögliche Kombinationen sind in Tabelle 6.6 dargestellt. Dabei bedeutet ein »x« in den drei linken Spalten, dass Sie die entsprechende Property im Animationsobjekt auf einen Wert ungleich nil gesetzt haben.

Tabelle 6.6 Animationsbereich in Abhängigkeit von den gesetzten Propertys

fromValue byValue toValue Animation (von–nach)

x

nil

x

fromValue

toValue

x

x

nil

fromValue

fromValue + byValue

nil

x

x

toValuebyValue

toValue

x

nil

nil

fromValue

Aktueller

Property-Wert

Das Setzen des Property-Wertes toValue in Listing 6.51 ist also nicht notwendig, da die Animation auch automatisch diesen Wert als Zielwert verwendet hätte. Das folgende Beispiel verschiebt einen Layer zwischen zwei Punkten. Als Property-Namen verwendet die Animation den Schlüssel, unter dem sie im Animationsverzeichnis des Layers liegt – also position.

CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = Zeilenumbruch
[NSValue valueWithCGPoint:CGPointMake(10.0, 10.0)];
theAnimation.toValue = Zeilenumbruch
[NSValue valueWithCGPoint:CGPointMake(100.0, 10.0)];
[theLayer addAnimation:theAnimation forKey:@"position"];

Listing 6.52 Verschieben eines Layers über eine Animation

Über die Klasse CAKeyframeAnimation können Sie Property-Animationen mit komplexeren Verläufen erzeugen. Über die Property values übergeben Sie dabei ein Array mit den Werten für die Zwischenzustände der Animation. Falls Sie mit der Animation Werte vom Typ CGPoint animieren wollen, können Sie auch die Property path verwenden. Sie erwartet einen Core-Graphics-Pfad. Damit ist es beispielsweise einfach, einen Layer entlang einer Kreislinie zu bewegen:

CAKeyframeAnimation *theAnimation =
[CAKeyframeAnimation animation];
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathAddEllipseInRect(thePath, NULL,
CGRectMake(140.0, 150.0, 20.0, 20.0));
theAnimation.path = thePath;
theAnimation.repeatCount = 3;
theAnimation.autoreverses = YES;
theAnimation.duration = 0.5;
[theLayer addAnimation:theAnimation forKey:@"position"];
CGPathRelease(thePath);

Listing 6.53 Layer-Bewegung entlang eines Pfades

Mit Keyframe-Animationen können Sie recht ungewöhnliche Animationen verwirklichen. Abschnitt 6.3.6, »Daumenkino«, stellt dafür noch ein komplexeres Beispiel vor.

Transitionen

Der zweite Animationstyp sind Transitionen, die Core Animation über die Klasse CATransition abbildet. Diese Klasse unterstützt insgesamt vier unterschiedliche Transitionstypen, die Tabelle 6.7 auflistet. Außer in der Fade-Animation können Sie eine Richtung in der Property subtype angeben; die Konstanten kCATransitionFromRight, -Left, -Top und -Bottom enthalten die erlaubten Werte hierfür.

Tabelle 6.7 Die Transitionstypen von Core Animation

Name Beschreibung

kCATransitionFade

Blendet den bestehenden Layer aus und den neuen Layer ein.

kCATransitionMoveIn

Bewegt den neuen Layer über den bestehenden.

kCATransitionPush

Schiebt den neuen Layer herein und den bestehenden gleichzeitig hinaus.

kCATransitionReveal

Schiebt den bestehenden Layer hinaus und gibt dadurch den neuen View frei.

Sie wenden eine Transition an, indem Sie sie in das Animationsverzeichnis desjenigen Layers einfügen, der die Transitionslayer enthält. Außerdem müssen Sie einen bestehenden Layer unsichtbar machen, indem Sie ihn verstecken oder aus der Layer-Hierarchie entfernen. Analog müssen Sie den neuen Layer anzeigen oder in die Hierarchie einfügen. Listing 6.54 zeigt ein einfaches Beispiel für diesen Animationstyp.

CATransition *theTransition = [CATransition animation];
theTransition.type = kCATransitionPush;
theTransition.subtype = kCATransitionFromTop;
[[theLayer.sublayers objectAtIndex:0] setHidden:YES];
[[theLayer.sublayers objectAtIndex:1] setHidden:NO];
[theLayer addAnimation: theTransition forKey:@"transition"];

Listing 6.54 Ausführen einer Transition

Die Transitionen in Core Animation sind also sehr ähnlich zu den Transitionen des UIKits. Allerdings bietet Letzteres erstaunlicherweise eine größere Vielfalt an Standardübergängen.

Animationsgruppen

Die Klasse CAAnimationGroup fasst mehrere Layer-Animationen zu einer Gruppe zusammen, die Sie in einem Array an die Property animations übergeben. Die Gruppe gibt die maximale Dauer der Animationen vor und beendet die Ausführung längerer Animationen gegebenenfalls vorzeitig. Eine Animationsgruppe ist also die Queen unter den Animationen. Wenn sie aufhört zu essen, dann müssen auch alle anderen den Dessertlöffel fallen lassen. Hat also beispielsweise die Gruppe eine Dauer von einer Sekunde und enthält sie eine 4 Sekunden dauernde Animation, dann wird diese Animation nur zu einem Viertel ausgeführt. In Listing 6.55 führt diese Konfiguration beispielsweise dazu, dass die Animation des Eckradius nur von 0 bis 40 anstatt bis 160 läuft.

CABasicAnimation *theAnimation =
[CABasicAnimation animationWithKeyPath:@"cornerRadius"];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theAnimation.toValue = [NSNumber numberWithFloat:160.0];
theAnimation.duration = 4.0;
theGroup.duration = 1.0;
theGroup.animations = [NSArray arrayWithObject:theAnimation];
theGroup.repeatCount = 3.0;
[theLayer addAnimation:theGroup forKey:@"group"];

Listing 6.55 Erzeugung einer Animationsgruppe


Galileo Computing - Zum Seitenanfang

6.3.6 DaumenkinoZur nächsten ÜberschriftZur vorigen Überschrift

Nachdem Abschnitt 6.3.5 die verschiedenen Animationsarten kurz vorgestellt hat, soll jetzt ein etwas umfangreicheres Beispiel für eine Keyframe-Animation folgen. In Listing 6.53 haben Sie bereits ein Beispiel für eine Bewegung entlang eines Pfades kennengelernt. Es ist jedoch auch möglich, mit einer Keyframe-Animation die Objekte einer Property des Layers auszutauschen. Sie können diese Möglichkeit beispielsweise dafür nutzen, Bildfolgen ablaufen zu lassen und somit einfache Filme wie bei einem Daumenkino abzuspielen.

Projektinformation

Den Quellcode des Beispielprojekts FlipBookAnimation finden Sie auf der DVD unter Code/Apps/Storyboard/FlipBookAnimation oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_2/Apps/Storyboard/FlipBookAnimation.

Das Beispielprojekt FlipBookAnimation lädt eine Bildfolge aus einem GIF-Bild und stellt sie über eine Keyframe-Animation dar. Dabei öffnet und lädt die Applikation die Bildfolge über das ImageIO-Framework in der Methode readAnimationImages der Klasse FlipBookViewController, die Sie in Listing 6.56 sehen.

- (NSArray *)readAnimationImages {
NSBundle *theBundle = [NSBundle mainBundle];
NSURL *theURL = [theBundle URLForResource:Zeilenumbruch
@"animated-image" withExtension:@"gif"];
NSDictionary *theOptions = [NSDictionary Zeilenumbruch
dictionaryWithObjectsAndKeys:nil];
CGImageSourceRef theSource = CGImageSourceCreateWithURL( Zeilenumbruch
(__bridge CFURLRef)theURL, Zeilenumbruch
(__bridge CFDictionaryRef)theOptions);
size_t theCount = CGImageSourceGetCount(theSource);
NSMutableArray *theResult = [NSMutableArray Zeilenumbruch
arrayWithCapacity:theCount];

for(size_t i = 0; i < theCount; ++i) {
CGImageRef theImage = CGImageSourceCreateImageAtIndex( Zeilenumbruch
theSource, i, Zeilenumbruch
(__bridge CFDictionaryRef)theOptions);

[theResult addObject:(__bridge_transfer id)theImage];
}
CFRelease(theSource);
return theResult;
}

Listing 6.56 Laden der Bildfolge für die Animation

Core Graphics und Objective-C

Core Foundation enthält viele Datentypen, deren zugehörige Funktionen sich analog zu den Methoden einer entsprechenden Foundation-Klasse verhalten.

Ein Beispiel dafür sind die Datentypen CFURLRef und CFDictionaryRef mit den analogen Klassen NSURL und NSDictionary.

Tatsächlich implementiert das Foundation-Framework die Klasse NSURL über den Datentyp CFURLRef, sodass Sie NSURL-Objekte auf CFURLRef und umgekehrt casten können, wie das die vierte Anweisung in Listing 6.56 macht. Diese Technologie nennt Apple Toll-Free Bridging.

Wenn Sie allerdings Foundation- auf Core-Foundation-Referenzen in einer ARC-Umgebung casten, müssen Sie die Modifizierer __bridge, __bridge_retained oder __bridge_transfer verwenden, damit der ARC-Compiler weiß, wie er mit der Eigentümerschaft auf den Referenzen umgehen soll.

Wenn Sie den Modifizierer weglassen, warnt Xcode Sie an der betreffenden Stelle und macht einen Vorschlag, wie Sie die Warnung vermeiden können. In der Regel sollten Sie diesen Vorschlag befolgen. Abschnitt 6.5, »Über diese Brücke musst du gehen«, geht noch genauer auf die Toll-Free Bridge und die Modifizierer ein.

Die Methode öffnet über die URL das GIF-Bild und liefert eine Referenz Typs CGImageSourceRef darauf. Danach ermittelt sie die Anzahl der Einzelbilder und schreibt diese Zahl in die Variable theCount. Über eine Schleife liest die Methode die einzelnen Bilder aus der Imagesource in ein Array. Die gelesenen Bilder sind allerdings keine UIImages, sondern haben den Typ CGImageRef, was sehr praktisch ist, da ja die contents-Property des Layers diesen Typ erwartet.

Die Methode viewDidAppear: des Viewcontrollers erzeugt und startet die Animation. Dazu erzeugt sie eine Keyframe-Animation und übergibt das Array mit den Bildern an deren Property values. Die Dauer eines kompletten Durchlaufs ist eine Sekunde, und die Property repeatCount bekommt den Wert HUGE_VALF zugewiesen, sodass die Animation unbegrenzt läuft.

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
NSArray *theImages = [self readAnimationImages];
CALayer *theLayer = self.animationView.layer;
CAKeyframeAnimation *theAnimation = [CAKeyframeAnimation Zeilenumbruch
animationWithKeyPath:@"contents"];

theAnimation.values = theImages;
theAnimation.repeatCount = HUGE_VALF;
theAnimation.duration = 1.0;
[theLayer addAnimation:theAnimation forKey:@"contents"];
}

Listing 6.57 Ausführen der Bildfolge über eine Keyframe-Animation

Die Animation stellt einen Atomkern dar, um den drei Elektronen in unterschiedlichen Bahnen kreisen. Wenn Sie die Animation langsamer laufen lassen, beispielsweise mit einer Dauer von fünf Sekunden, dann flackern die Elektronen bei der Bewegung, und Sie sehen für jedes Elektron jeweils zwei blasse Punkte (siehe Abbildung 6.24). Das liegt daran, dass die Animation die Bilder nicht wechselt, sondern aus- beziehungsweise einblendet. Diese Überblendzeiten sind dabei so lang, dass Sie die Punkte doppelt sehen. Das Beispielprojekt auf der DVD zeigt außerdem einen Pause-Button an, mit dem Sie die Animation unterbrechen können. Wenn Sie ihn drücken, können Sie sich diesen Dopplungseffekt in Ruhe ansehen.

Über die Property calculationMode der Klasse CAKeyframeAnimation können Sie das Überblendverhalten ändern. Sie hat standardmäßig den Wert kCAAnimationLinear; blendet also das neue Bild in der gleichen, gleichmäßigen Geschwindigkeit ein, wie sie das bestehende ausblendet. Wenn Sie stattdessen den Wert auf kCAAnimationDiscrete setzen, tauscht die Animation die Bilder ohne Überblendung aus. Dadurch sehen Sie jedes Elektron sowohl während der Bewegung als auch in den Pausen nur einmal.

Abbildung

Abbildung 6.24 Die Animation macht mal eine Pause.


Galileo Computing - Zum Seitenanfang

6.3.7 RelativitätstheorieZur nächsten ÜberschriftZur vorigen Überschrift

Icon

In Abschnitt 6.3.5, »Der bewegte Layer«, haben Sie die Propertys duration, speed, repeatCount und autoreverses kennengelernt, mit denen Sie das Ablaufverhalten der Animation steuern. Allerdings deklariert nicht die Klasse CAAnimation diese Propertys, sondern das Protokoll CAMediaTiming. Der Versuch, die Eigenschaften einer laufenden Animation zu ändern, scheitert jedoch mit folgender Ausnahme:

Terminating app due to uncaught exception 'CAAnimationImmutable', reason:
 'attempting to modify read-only animation <CAKeyframeAnimation: 0x7546810>'.

Sie können also beispielsweise eine Animation nicht einfach über deren Property speed verlangsamen oder gar pausieren lassen.

Allerdings implementiert auch die Klasse CALayer das Protokoll, und so können Sie die Eigenschaften auch bei den Layern setzen und damit den Zeitraum beziehungsweise die Zeitachse der enthaltenen Animationen verbiegen. Wenn Sie beispielsweise die Geschwindigkeit des Layers auf 2 setzen, laufen seine Animationen doppelt so schnell ab, und wenn Sie sie auf 0 setzen, hält die Animation an.

Geschachtelte Zeiträume

Jeder Layer legt also seine eigene Zeitachse fest, die allerdings nicht nur für die enthaltenen Animationen, sondern auch für alle enthaltenen Layer gilt. Der innere Layer liegt also auch immer im Zeitraum seines Superlayers. Wenn der äußere Layer beispielsweise die Geschwindigkeit 3 hat, der innere die halbe Geschwindigkeit und eine Animation des inneren Layers den Wert 4 hat, dann ist die Gesamtgeschwindigkeit 3 × 0,5 × 4 = 6.

Sie können also über den Layer eine Animation pausieren lassen. Um diese Funktionalität unabhängig von der Layer-Klasse zu machen, verwendet das Beispielprojekt dafür die Kategorie CALayer(AnimationPausing) mit den Methoden pause, resume und isPausing. Wenn Sie nun allerdings diese drei Methoden folgendermaßen implementieren ...

- (void)pause {
self.speed = 0.0;
}
- (void)resume {
self.speed = 1.0;
}
- (BOOL)isPausing {
return self.speed == 0.0;
}

Listing 6.58 Naive Implementierung der Pause-Funktionalität

... stellen Sie zwei unerwünschte Effekte fest: Einerseits verschwindet der Layer-Inhalt, und andererseits scheint die Animation im Hintergrund weiterzulaufen. Denn nach dem Ende der Pause befinden sich die Elektronen meistens nicht an der Position wie zu Beginn der Pause. Vermutlich zeigt der Layer immer das Bild an, das er vor dem Beginn der Animation hatte, und das war keins, weil die App der contents-Property keinen Wert zugewiesen hat. Sie können das leicht überprüfen, indem Sie diesen Wert durch die Anweisung theLayer.contents = [theImages objectAtIndex:0]; in der Methode viewWillAppear: setzen. Durch diese Änderung zeigt nun der Layer immer das gleiche Bild in der Pause an.

Diese Änderung löst zwar nicht das Problem, jedoch wissen Sie nun, was die Ursache des Fehlers ist. Durch die Änderung der Geschwindigkeit fällt die Zeitachse des Layers auf einen Punkt, den Startzeitpunkt, zusammen, und somit bleibt auch die Animation am Startzeitpunkt stehen.

Sie müssen also die Animation so anhalten, dass sie stattdessen am aktuellen Zeitpunkt hält. Dazu liefert Ihnen die Funktion CACurrentMediaTime die aktuelle Systemzeit für die Animationen, die Sie über die Methode convertTime:fromLayer: in die aktuelle Zeit des Layers umrechnen können. Da Sie hier mit der absoluten Zeit rechnen, geben Sie für den zweiten Parameter den Wert nil an.

Den so errechneten Wert können Sie über die Property timeOffset des Layers setzen. Sie enthält eine Zeitspanne, die Core Animation zu der aktuellen Zeit der Animation hinzuzählt; diese Property verschiebt also den aktuellen Punkt in der Animation. Im Gegensatz dazu legen Sie über die Property beginTime die Startzeit fest.

Sie können sich das so vorstellen: Sie stehen in Ihrer Küche und möchten plötzlich ein bestimmtes Musikstück hören. Also gehen Sie ins Wohnzimmer, suchen die Schallplatte heraus, legen Sie auf den Plattenspieler und setzen die Nadel genau auf den Beginn des zweiten Stückes auf der Platte. Dann ist die Zeit vom Start in der Küche bis zum Aufsetzen der Nadel die Begintime, und die Dauer des ersten Stückes auf der Platte ist der Timeoffset, wenn die gesamte Musik auf der Platte der Animation entspricht. Abbildung 6.25 stellt diesen Zusammenhang grafisch dar. Die Animation startet an dem Zeitpunkt, auf den die Spitze des Pfeils beginTime zeigt. Allerdings startet sie nicht am Anfang, sondern um den Timeoffset nach links verschoben. Der Animationsablauf entspricht also der dicken Linie zwischen den schwarzen Punkten.

Abbildung

Abbildung 6.25 Startzeitpunkt auf der Zeitachse

Der Timeoffset bietet also genau die Möglichkeit, um die richtige Position im Animationsablauf auszuwählen, und Sie ändern die Methode pause wie folgt:

-(void)pause {
CFTimeInterval theCurrentTime = CACurrentMediaTime();
CFTimeInterval theTime = Zeilenumbruch
[self convertTime:theCurrentTime fromLayer:nil];

self.speed = 0.0;
self.timeOffset = theTime;
}

Listing 6.59 Anhalten zum richtigen Animationszeitpunkt ...

Mit der Änderung aus Listing 6.59 zeigt der Layer immer schön den Animationszustand an, in dem Sie Pause gedrückt haben. Allerdings lässt das Fortsetzen der Animation noch zu wünschen übrig, da hierbei unschöne Elektronensprünge auftreten. Sie müssen also beim Wiederanlaufen dafür sorgen, dass der Animationszustand genau dem Zustand in der Pause entspricht. Diese Berechnung ist etwas komplizierter. Abbildung 6.26 stellt das Vorgehen dafür grafisch dar.

Abbildung

Abbildung 6.26 Zeitverschiebung beim Fortsetzen der Animation

Die Darstellung enthält zwei Zeitverläufe. Die obere Zeitachse enthält den realen Animationsverlauf: Die Animation startet und läuft eine gewisse Zeit. Dann halten Sie sie an, bis Sie über resume die Bewegung fortsetzen. Wenn Sie das Zeitstück der Pause nun mit dem abgelaufenen Animationsteil vertauschen, erhalten Sie die untere Zeitachse aus der Abbildung, und die beiden Animationsteile knüpfen nahtlos aneinander an. Auf diese Weise lässt sich also der Sprung vermeiden.

Im Code verschieben Sie das Zeitstück der Pause, indem Sie es als Begintime der Animation setzen (siehe Abbildung 6.25), und die Länge der Pause entspricht der Differenz von der aktuellen Zeit und der Startzeit der Pause.

Listing 6.60 enthält den kompletten Code für die resume-Methode. Da die Methode convertTime:fromLayer: für ihre Berechnungen auch den Wert der Property beginTime verwendet, ist es übrigens wichtig, diesen Wert vorher auf null zu setzen.

-(void)resume {
CFTimeInterval theTime = self.timeOffset;

self.speed = 1.0;
self.timeOffset = 0.0;
self.beginTime = 0.0;
theTime = [self convertTime:CACurrentMediaTime() Zeilenumbruch
fromLayer:nil] – theTime;
self.beginTime = theTime;
}

Listing 6.60 ... und Fortfahren mit dem richtigen Animationszustand


Galileo Computing - Zum Seitenanfang

6.3.8 Der View, der Layer, seine Animation und ihr LiebhaberZur nächsten ÜberschriftZur vorigen Überschrift

Icon

Mit einer Property-Animation können Sie allerdings nicht nur die Standard-Propertys der Layer, sondern auch beliebige eigene Propertys animieren. Dazu sollten Sie diese Propertys analog zu den Propertys part und digit in den Klassen PieLayer beziehungsweise DigitView anlegen, wie das Abschnitt 6.3.1, »Layer«, beschreibt. Hier sind noch einmal kurz die Punkte, auf die Sie bei animierbaren Propertys achten sollten:

  • Deklarieren Sie die Property in Ihrem Layer, und implementieren Sie sie über das Schlüsselwort @dynamic. Core Animation durchsucht nur die Propertys, jedoch keine Methoden der Layer-Klasse nach animierbaren Propertys.
  • Legen Sie über die Klassenmethode defaultValueForKey: einen Initialwert für Ihre Property fest.
  • Außerdem müssen Sie die Klassenmethode needsDisplayForKey: überschreiben, damit Core Animation den Layer bei einer Änderung des Property-Wertes auch zeichnet.

Sie können also beispielsweise eine Animation der Klasse CABasicAnimation mit dem Schlüssel part in das Animationsverzeichnis eines Pielayers einfügen. Core Animation erzeugt dann während der Animation die entsprechenden Zwischenwerte für diese Property, sodass ein flüssiger Bewegungsablauf entsteht.

Bist du flüssig?

Wie Sie vielleicht schon in Abschnitt 6.3.7, »Relativitätstheorie«, bemerkt haben, sind die Animationen in Core Animation zeit- und nicht framebasiert. Das heißt, dass die von Ihnen vorgegebene Zeit für die Animation wichtiger ist als die Anzahl der erzeugten Einzelbilder (Frames). Core Animation berechnet, sofern das möglich ist, für die Animation so viele Einzelbilder, dass die Animation die vorgegebene Zeit nicht überschreitet und dabei trotzdem möglichst flüssig abläuft. Dahinter steckt sicherlich jede Menge Hirnschmalz. Wenn Sie also für Ihre Views und Layer Animationen brauchen, sollten Sie dafür Core Animation verwenden.

Mit den Methoden aus Abschnitt 6.2.1, »Animationen mit Blöcken«, ist es wesentlich einfacher, eine Animation zu erzeugen. Natürlich verwendet Cocoa Touch auch dafür Layer-Animationen, die der Layer bei Änderungen der entsprechenden Property automatisch erzeugt. Dafür wäre es naheliegend, die Erzeugung der Animation in den Setter der Property des Views oder des Layers zu verschieben. Allerdings würden Sie damit die Animationserzeugung fest mit dem View oder dem Layer verdrahten.

Core Animation stellt stattdessen für die Animationserzeugung einen eleganteren Weg zur Verfügung. Ein Layer kann bei der Änderung einer Property oder der Veränderung der Layer-Hierarchie eine Aktion auslösen. Eine Aktion ist dabei ein Objekt, dessen Klasse das Protokoll CAAction implementiert. Die Property-Änderung besteht dabei aus drei Schritten:

  1. Zuerst übernimmt der Layer den neuen Wert.
  2. Als Nächstes erzeugt er die Aktion.
  3. Danach führt der Layer die Aktion aus.

Die Layer-Methode actionForKey: erzeugt die Aktionen des Layers. Wenn Sie die Standardaktionen eines Layers verändern oder neue Aktionen hinzufügen wollen, haben Sie mehrere Möglichkeiten:

  1. Sie können die Methode actionForLayer:forKey: im Delegate des Layers
    implementieren.
  2. Über die Property actions können Sie im Layer ein Verzeichnis mit Aktionen
    setzen.
  3. Der Layer durchsucht außerdem sein Style-Verzeichnis rekursiv nach Aktionen.
  4. Sie können die Klassenmethode defaultActionForKey: implementieren.

Die Methode actionForKey: sucht in der angegebenen Reihenfolge nach den Aktionen. Wenn ein Suchweg dabei nil liefert, sucht der Layer weiter. Ein Aktionsobjekt oder das Nullobjekt [NSNull null] bricht die Suche sofort ab. Dabei bedeutet »Nullobjekt«, dass der Layer keine Aktion zum angegebenen Schlüssel hat.

Das Protokoll CAAction besitzt nur eine Methode runActionForKey:object:arguments:, die der Layer im dritten Schritt aufruft. Dabei ist der Parameter object der Layer, der die Aktion ausgelöst hat, und der Parameter arguments ist in der Regel nil. Die Klasse CAAnimation implementiert das CAAction-Protokoll. Die Implementierung der Methode fügt dabei lediglich das Animationsobjekt in das Animationsverzeichnis des Layers ein. Die Implementierung sieht laut Dokumentation so aus: [siehe Core Animation Programming Guide, Layer Actions, Adopting the CAAction Protocol, Listing 1 (https://developer.apple.com/library/ios/documentation/cocoa/conceptual/coreanimation_guide/Articles/Actions.html#//apple_ref/doc/uid/TP40006095-SW8) ]

- (void)runActionForKey:(NSString *)inKey object:(id)inLayer
arguments:(NSDictionary *)inArguments {
[(CALayer *)inLayer addAnimation:self forKey:inKey];
}

Listing 6.61 Actionmethode einer Animation

Wenn Sie also eigene Layer-Propertys automatisch animieren möchten, brauchen Sie nur über einen der oben beschriebenen Suchwege ein Animationsobjekt zu erzeugen. Alternativ können Sie in Ihrer Layer-Klasse hingegen auch die Methode actionForKey: überschreiben.

- (id<CAAction>)actionForLayer:(CALayer *)inLayer
forKey:(NSString *)inKey {
if([@"part" isEqualToString:inKey]) {
CABasicAnimation *theAnimation =
[CABasicAnimation animationWithKeyPath:inKey];
theAnimation.fromValue = [inLayer valueForKey:@"part"];
return theAnimation;
}
else {
return [super actionForLayer:inLayer forKey:inKey];
}
}

Listing 6.62 Animationserzeugung für eine Layer-Property

Mit der Implementierung aus Listing 6.62 führt jede Änderung der Property part der Klasse PieView in einem Animationsblock zu einer animierten Veränderung der Grafik. Da der Layer die Aktion vor der Übernahme des Property-Wertes anlegt, liefert der Aufruf von valueForKey: noch den alten Wert. Weil der Layer dagegen den Wert vor der Ausführung der Aktion übernimmt, reicht es nach Tabelle 6.6 aus, die Property fromValue zu setzen.

Das klingt ja schon mal ganz gut, hat jedoch noch einen kleinen Schönheitsfehler. Wenn Sie die Dauer des Animationsblocks verändern, ist das der Animation für die Property part völlig schnuppe. Leider hat Apple den Zugriff auf diesen und die anderen Animationsparameter sehr gut versteckt, sodass Sie diese Werte nicht einfach auslesen können.

Die Lösung dieses Problems ist allerdings einfach und erfolgt nach dem Motto: »Klauen wir gleich die ganze Animation.« Weniger cineastisch ausgedrückt heißt das, dass die Methode also nicht die Animation selbst erzeugt, sondern dass sie sie für eine Standard-Property, in diesem Fall opacity, erzeugen lässt und sie danach anpasst.

CABasicAnimation *theAnimation = Zeilenumbruch
(id)[super actionForKey:@"opacity"];
theAnimation.keyPath = inKey;
theAnimation.fromValue = self.part;
theAnimation.toValue = nil;
theAnimation.byValue = nil;

Listing 6.63 Erzeugung der Animation über eine Standard-Property

Listing 6.63 gibt den entsprechenden Codeschnipsel wieder, der die Animation über die Animation für die Property opacity erzeugt. Sie müssen in dieser Animation natürlich die Werte der Propertys keyPath und fromValue anpassen. Da Vorsicht bekanntlich die Mutter der Porzellankiste ist, sollten Sie die Property-Werte für to-Value und byValue löschen. Sie können damit jetzt auch die Property part der Klasse PieView in einem Animationsblock setzen, um die Änderung zu animieren:

[UIView animateWithDuration:0.75 animations:^{
self.pieView.part = 0.6;
}];

Listing 6.64 Animation einer selbst definierten Property

Verwendung der Delegate-Methode

Sie können dieses Vorgehen natürlich auch über die Delegate-Methode actionForLayer:forKey: realisieren. Allerdings sollten Sie hier nicht die Delegate-Methode der Superklasse, sondern die Methode actionForKey: des Layers aufrufen, um die Standardanimation zu erzeugen. Das hat den Grund, dass Sie hier eine neue Anfrage für eine andere Property starten, und dafür sollten Sie nicht die Delegate-Methode, sondern die Methode des Layers verwenden. Für die Property opacity des Programmbeispiels soll der Layer ja alle Möglichkeiten der Animationserzeugung durchprobieren.

- (id<CAAction>)actionForLayer:(CALayer *)inLayer Zeilenumbruch
forKey:(NSString *)inKey {
if([kPartKey isEqualToString:inKey]) {
CABasicAnimation *theAnimation = Zeilenumbruch
(id)[inLayer actionForKey:@"opacity"];

theAnimation.keyPath = inKey;
theAnimation.fromValue = Zeilenumbruch
[inLayer valueForKey:kPartKey];
theAnimation.toValue = nil;
theAnimation.byValue = nil;
return theAnimation;
}
else {
return [super actionForLayer:inLayer forKey:inKey];
}
}

Die Erzeugung der Aktion für die Standard-Propertys erfolgt hingegen über den Superaufruf, da hier der Aufruf der Methode des Layers zu einer Endlosrekursion führen würde.

Die Klasse NumberView im Beispielprojekt Games animiert die drei Anzeigen für die Ziffern auch über Viewanimationen. Sie besitzt zwei Methoden, um einen neuen Wert zu setzen. Da ist einerseits der Setter setValue:, der nur den Wert setzt, und andererseits setValue:animated:, der außerdem eine animierte Aktualisierung der Anzeige erlaubt. Dabei basiert die zweite Methode auf der Implementierung der ersten; abhängig vom zweiten Parameter kapselt sie die Zuweisung einfach in einen Animationsblock und ähnelt damit in der Signatur vielen Methoden des UIKits:

- (void)setValue:(NSUInteger)inValue Zeilenumbruch
animated:(BOOL)inAnimated {
if(inAnimated) {
[UIView animateWithDuration:0.75 animations:^{
self.value = inValue;
}];
}
else {
self.value = inValue;
}
}

Listing 6.65 Aktualisierung des Spielstands mit und ohne Animation

Der Setter teilt den Wert für die einzelnen Ziffern der enthaltenen Digitviews auf und aktualisiert deren Werte. Dabei liegen diese Views allerdings in umgekehrter Reihenfolge im Numberview. Der erste Subview liegt also am weitesten rechts und enthält die Einerstelle, der zweite Subview liegt links daneben und enthält die Zehner und so weiter. Die Berechnung der Ziffern erfolgt dabei über eine Division durch 10 mit Rest.

- (void)setValue:(NSUInteger)inValue {
NSUInteger theValue = inValue;

for(DigitView *theView in self.subviews) {
theView.digit = value % 10;
theValue /= 10;
}
value = inValue;
}

Listing 6.66 Aktualisierung der Ziffern

Mit einer entsprechend adaptierten Version der oben beschriebenen Methode actionForLayer:forKey: erhalten Sie einen animierten Wechsel der Ziffern. Allerdings läuft der View alle Ziffern bei einem Wechsel von 9 auf 0 durch, der beispielsweise beim Übergang von der 9 auf die 10 auftritt. Um diesen Darstellungsfehler zu vermeiden, berechnet der Digitview im Setter der Property value einen besseren Wert für die Property fromValue der Animation. Der Digitview speichert diesen Wert in einer eigenen Property und übergibt ihn an die Animation. Der Wert ist 10 für den Übergang von 9 auf 0 und –1 für den Übergang von 0 auf 9 – 1.

- (void)setDigit:(NSUInteger)inDigit {
NSInteger theOldDigit = self.digit;
NSInteger theNewDigit = inDigit % 10;

if(theOldDigit == 9 && theNewDigit == 0) {
theOldDigit = –1;
}
else if(theOldDigit == 0 && theNewDigit == 9) {
theOldDigit = 10;
}
self.fromValue = [NSNumber numberWithInt:theOldDigit];
[(DigitLayer *)self.layer setDigit:theNewDigit];
}

Listing 6.67 Berechnung eines geeigneteren Startwertes für die Animation

Diesen berechneten Startwert in fromValue übergibt die Methode actionForLayer:forKey: an die Property fromValue der Animation. Listing 6.68 zeigt die komplette Implementierung und hebt die angepasste Wertübergabe hervor.

- (id<CAAction>)actionForLayer:(CALayer *)inLayer Zeilenumbruch
forKey:(NSString *)inKey {
if([kDigitKey isEqualToString:inKey]) {
CABasicAnimation *theAnimation = Zeilenumbruch
(id)[inLayer actionForKey:@"opacity"];

theAnimation.keyPath = inKey;
theAnimation.fromValue = self.fromValue;
theAnimation.toValue = nil;
theAnimation.byValue = nil;
return theAnimation;
}
else {
return [super actionForLayer:inLayer forKey:inKey];
}
}

Listing 6.68 Erzeugung der Animation für den Digitview


Galileo Computing - Zum Seitenanfang

6.3.9 Die 3. DimensionZur vorigen Überschrift

Im Gegensatz zu einem View befinden sich Layer in einem dreidimensionalen Raum. Über die Property zPosition legen Sie dabei die Tiefenposition eines Layers fest, deren Standardwert 0 ist. Core Animation beschreibt also die Position eines Layers in einem dreidimensionalen Koordinatensystem (siehe Abbildung 6.27).

Abbildung

Abbildung 6.27 Dreidimensionales Koordinatensystem der Layer

Sie können zwar die Layer darin räumlich transformieren (drehen, strecken oder verschieben). Die Layer selbst bleiben hingegen flach. Beispielsweise verschwindet ein um 90° um die y-Achse gedrehter Layer, weil seine Ausdehnung in x-Richtung in diesem Fall null ist.

Die affine Transformation eines Layers beschreibt die C-Struktur CATransform3D, die eine 4×4-Matrix darstellt und auf die Sie über die Property transform zugreifen können. Sie können sie analog zu den Transformationen verwenden, da es hierfür analoge Funktionen gibt. Beispielsweise entspricht der Funktion CGAffineTransformMakeScale die Funktion CATransform3DMakeScale, die jedoch einen Parameter mehr für die z-Achse besitzt. Etwas komplizierter ist es allerdings bei CATransform3DMakeRotation, da Sie hier einen Winkel und einen Richtungsvektor in Form einer dreidimensionalen Koordinate angeben müssen. Dabei können Sie sich den Richtungsvektor als eine Linie vom Nullpunkt zu der Koordinate vorstellen. Die Ergebnismatrix beschreibt nun eine Drehung um diesen Vektor. Beispielsweise beschreibt die Transformation

CATransform3DMakeRotation(M_PI / 4.0, 0.0, 0.0, 1.0)

eine 45°-Drehung um die z-Achse als Rotationsachse.

Layer mit Tiefgang

Wenn Sie einen Layer um die x- oder y-Achse drehen, sieht das Ergebnis indes nach wie vor flach aus, und die Darstellung entspricht eher einer Stauchung. Das liegt daran, dass die Layer zwar in einem dreidimensionalen Raum liegen, jedoch noch keine Perspektive haben. Bei einer perspektivischen Darstellung sind die weiter hinten liegenden Bildteile kleiner als die weiter vorne liegenden.

Diesen Effekt können Sie dadurch erreichen, dass Sie jeweils einen Anteil der z-Position zu jeweils der x- und der y-Position hinzuaddieren. Das verkleinert den hinten liegenden Teil des Layers und vergrößert den weiter vorne liegenden. Bei Layern mit Tiefe erzeugt das eine perspektivische Darstellung. Auch diese Berechnung lässt sich durch eine Transformationsmatrix durchführen.

Über die Property sublayerTransform lässt sich eine Transformation für die Sublayer eines Layers festlegen. Da Core Animation diese Transformation zusätzlich zu der Transformation des Sublayers anwendet, können Sie damit die Perspektivenberechnung durchführen.

CATransform3D theTransform = CATransform3DIdentity;
theTransform.m34 = 0.0005;
theParentLayer.sublayerTransform = theTransform;

Mit dieser Transformation stellt theParentLayer alle um die x- oder y-Achse gedrehten Sublayer perspektivisch dar.

Das Beispielprojekt Pie verwendet diesen Effekt. Wenn Sie den Pieview berühren, dreht er sich um die x-Achse, wobei durch die beschriebene Transformation ein räumlicher Eindruck entsteht (siehe Abbildung 6.28).

Abbildung

Abbildung 6.28 Layer mit Perspektive

Die Layer-Property transform lässt sich auch über Property-Animationen verändern. Dazu erzeugen Sie die gewünschte Transformationsmatrix, die Sie danach über den Convenience-Konstruktor valueWithCATransform3D: in ein NSValue-Objekt umwandeln müssen, weil die Animationen ja Objekte und keine C-Strukturen erwarten.

CATransform3D theTransformation = CATransform3DMakeRotation(
M_PI / 3.0, 1.0, 1.0, 0.0);
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theTransform = CATransform3DScale(theTransform, 0.5, 0.5, 0.5);
theAnimation.fromValue =
[NSValue valueWithCATransform3D:CATransform3DIdentity];
theAnimation.toValue =
[NSValue valueWithCATransform3D:theTransform];
[theLaver addAnimation:theAnimation forKey:@"transform"];

Listing 6.69 Dreidimensionale Layer-Animation

Die Anweisungen in Listing 6.69 erzeugen eine Animation mit einer 60°-Drehung (p / 3) und einer Stauchung auf die halbe Größe. Bei der Erzeugung einer Drehung im dreidimensionalen Raum müssen Sie ja die Rotationsachse beschreiben. Das ist in diesem Beispiel der Richtungsvektor (1, 1, 0), also die Linie vom Nullpunkt zum Punkt mit den Koordinaten (1, 1, 0).

Drehungen über die Hauptachsen

Vorsicht, der Inhalt dieses Kastens kann Spuren von Mathematik enthalten, und das Lesen kann zu abstraktem Denken führen.

Sie können Rotationen um einen beliebigen Vektor auch durch ein bis drei Rotationen an den Hauptachsen x, y und z beschreiben, indem Sie den Rotationsvektor auch durch Drehungen darstellen. Die Linie des Vektors (1, 1, 0) hat beispielsweise den Winkel 45° zur x-Achse, und somit können Sie diesen Vektor als 45°-Drehung um die z-Achse ausdrücken. Die Rotation aus Listing 6.69 können Sie also auch durch zwei Transformationen wie folgt beschreiben:

CATransform3D theTransformation = CATransform3DMakeRotation(
M_PI / 3.0, 1.0, 0.0, 0.0);
theTransform = CATransform3DRotate(theTransform,
M_PI / 4.0, 0.0, 0.0, 1.0);

In vielen Fällen reichen jedoch Rotationen, Skalierungen und Translationen an den drei Hauptachsen aus, die Sie über spezielle Animationspfade auch einfacher beschreiben können. Die Pfade haben alle einen ähnlichen Aufbau. Sie beginnen mit dem Wort transform, darauf folgen die Operation und der Name der Achse. Als Operationen stehen rotation, scale und translate zur Verfügung. Eine einfache Rotation um die x-Achse können Sie beispielsweise so beschreiben:

CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = [NSNumber numberWithFloat:0.0];
theAnimation.toValue = [NSNumber numberWithFloat:M_PI / 2.0];
[theLayer addAnimation:theAnimation Zeilenumbruch
forKey:@"transform.rotation.x"];

Listing 6.70 Dreidimensionale Animation über einen Animationspfad

Sie können den Namen der Achse auch weglassen, um die Werte für mehrere Achsen gleichzeitig zu setzen. Die Werte in den Property-Animationen müssen Sie dann als NSArray mit jeweils drei NSValue-Objekten übergeben:

CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = [NSArray arrayWithObjects:Zeilenumbruch
[NSNumber numberWithFloat:0.0], Zeilenumbruch
[NSNumber numberWithFloat:0.0], Zeilenumbruch
[NSNumber numberWithFloat:0.0], nil];
theAnimation.toValue = [NSArray arrayWithObjects: Zeilenumbruch
[NSNumber numberWithFloat:0.5], Zeilenumbruch
[NSNumber numberWithFloat:0.5], Zeilenumbruch
[NSNumber numberWithFloat:0.5], nil];
[theLayer addAnimation:theAnimation forKey:@"transform.scale"];

Listing 6.71 Animierte Skalierung entlang mehrerer Achsen



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Einstieg in Objective-C 2.0 und Cocoa





 Einstieg in
 Objective-C 2.0
 und Cocoa


Zum Katalog: Apps entwickeln für iPhone und iPad - Videotraining






 Apps entwickeln für
 iPhone und iPad -
 Videotraining


Zum Katalog: Apps mit HTML5 und CSS3






 Apps mit HTML5
 und CSS3


Zum Katalog: iPhone- und iPad-Apps entwickeln






 iPhone- und
 iPad-Apps entwickeln


Zum Katalog: Android 4






 Android 4


Zum Katalog: Android-Apps entwickeln - Videotraining






 Android-Apps
 entwickeln -
 Videotraining


Zum Katalog: Windows Store Apps mit XAML und C#






 Windows Store Apps
 mit XAML und C#


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2013
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de