MyAppTheme Logo2

Blog

Alles rund um's iPhone

CoverFlow

CoverFlow


Bereits Anfang des Jahres habe ich einen Blog Beitrag zum Thema Cover Flow geschrieben. Ein Thema, zu dem mich seither immer wieder Mails und Anfragen erreicht haben. Insbesondere für Entwickler, die noch nicht so lange mit Objective-C und Cocoa entwickeln ist das Thema zugegebenermassen auch nicht ganz ohne.
Möchte man einen Cover Flow in eine eigene App einbauen gibt es zunächst drei Möglichkeiten:

  • Man benutzt den Cover Flow View aus den Private Frameworks von Apple (Keine gute Idee, wenn man die App später auch erfolgreich in den AppStore bringen möchte)
  • Man benutzt Core Animation und Layers
  • Man verwendet OpenGL

Auf die erste Möglichkeit gehe ich aus verständlichen Gründen nicht weiter ein. Die zweite Variante mit Core Animation kam in einem meiner recht frühen Projekte zum Einsatz (Den Beispielcode erläutere ich nacholgend) und hat sich gut bewährt. Variante 3 setze ich aktuell in den Projekten ein, kann als Library gekauft werden und ist zudem im Video zu sehen.

Da das Core Animation Framework auf dem iPhone nicht ganz so mächtig wie unter MacOSX ist, muss man auf Filter verzichten, trotzdem sind schöne Ergebnisse zu erzielen. Doch nun zum Code.

#import
#import 

@protocol RBCoverFlowDelegate
- (int)numberOfItems;
- (UIImage*)coverImageForIndex:(int)index;
- (UIImage*)placeholderImage;
- (void)coverHasChangedToIndex:(int)index;
- (void)clickOnCoverWithIndex:(int)index;
@end

@interface RBCoverFlowView : UIView {
	CGPoint _startTouchPosition;
	float _centerPosition;
	float _sidePosition;
	CATransform3D _leftTransform;
	CATransform3D _rightTransform;

	CALayer* _contentLayer;

	id _delegate;
	int _itemCount;

	CGImageRef _placeholderImageRef;
	UIImage* _placeholderImage;
	CGImageRef _maskedPlaceholderImageRef;

	CGSize _imageSize;
	CGFloat _imageScaleX;
	CGFloat _imageScaleY;

	float _rowScaleFactor;
}

// Start the cover flow
- (void)start:(id)delegate;

@end

Der Cover Flow besteht zunächst aus einem “Hauptview”, der von UIView abgeleitet ist und in dem die Covers dargestellt werden. Dieser ist zudem für das Verarbeiten der Touches und der gesamten Animation zuständig.

Die Covers selber bestehen aus einzelnen Layern, bzw. genau drei Layern pro Cover. Der Layer mit dem Bild selber, ein Layer, der die Spiegelung (Reflection) darstellt und ein Rahmen-Layer, der das ganze Cover umschliesst.

- (CALayer *)addCoverLayer:(int)index {
	CALayer* containerLayer = [CALayer layer];
	containerLayer.name = [NSString stringWithFormat:@"%d", index];
	containerLayer.frame = CGRectMake(0.0f, 0.0f, [[self maxAvalibleImageWidth] floatValue], 1.5f * [[self maxAvalibleImageHeight] floatValue]);
	containerLayer.contentsGravity = kCAGravityResize;

	CALayer* holderLayer = [CALayer layer];
	holderLayer.name = [NSString stringWithFormat:@"Holder_%d", index];
	holderLayer.frame = CGRectMake(0.0f, 0.0f, [[self maxAvalibleImageWidth] floatValue], 1.5 * [[self maxAvalibleImageHeight] floatValue]);
	holderLayer.contentsGravity = kCAGravityResize;

	CALayer* imageLayer = [CALayer layer];
	imageLayer.name = [NSString stringWithFormat:@"Image_%d", index];
	imageLayer.contents = (id)_placeholderImageRef;
	imageLayer.frame = [self imageRect:(CGImageRef)imageLayer.contents];
	imageLayer.contentsGravity = kCAGravityResizeAspectFill;
	imageLayer.masksToBounds = YES;

	CALayer* reflectionLayer = [CALayer layer];
	reflectionLayer.name = [NSString stringWithFormat:@"Reflection_%d", index];
	reflectionLayer.contents = (id)_maskedPlaceholderImageRef;
	CGRect frame = [self imageRect:(CGImageRef)reflectionLayer.contents];
	frame.origin.y = imageLayer.frame.size.height + 2;
	reflectionLayer.frame = frame;
	reflectionLayer.contentsGravity = kCAGravityResize;
	reflectionLayer.masksToBounds = YES;
	reflectionLayer.transform = CATransform3DMakeScale(1.0f, -1.0f, 1.0f);
	reflectionLayer.opacity = 0.6;

	[containerLayer setValue:imageLayer forKey:kImageLayerKey];
	[containerLayer setValue:reflectionLayer forKey:kImageReflectionKey];
	[containerLayer setValue:holderLayer forKey:kImageHolderKey];

	[holderLayer addSublayer:imageLayer];
	[holderLayer addSublayer:reflectionLayer];
	[containerLayer addSublayer:holderLayer];

	return containerLayer;
}

Sind die Layer erstellt worden, müssen die einzelnen Cover nun entsprechend positioniert werden, damit das Aussehen dem Cover Flow der iPod App entspricht. Dies erreicht man, in dem man jeden einzelnen Layer im Raum positioniert, skaliert und leicht dreht.

- (void)layoutSublayer:(CALayer*)rootLayer number:(int)number {
	int selectedIndex = [[_contentLayer valueForKey:@"selectedIndex"] integerValue];

	rootLayer.frame = CGRectMake(0.0f, 0.0f, [[self maxAvalibleImageWidth] floatValue], 1.5f * [[self maxAvalibleImageHeight] floatValue]);

	CATransform3D layerTransform = CATransform3DIdentity;
	layerTransform.m34 = 1.0f / _sidePosition;

	CALayer* holder = [rootLayer valueForKey:kImageHolderKey];

	CGPoint pos = CGPointMake(CGRectGetMidX(_contentLayer.bounds), CGRectGetMidY(_contentLayer.bounds));

	if (number < selectedIndex) {
		pos.x -= (selectedIndex - number) * _imageSize.width * _rowScaleFactor;
		holder.transform = _leftTransform;
		holder.zPosition = _sidePosition;
		rootLayer.zPosition = _sidePosition - 0.1f * (selectedIndex - number);
		rootLayer.sublayerTransform = layerTransform;
	} else if (number > selectedIndex) {
		pos.x += (number - selectedIndex) * _imageSize.width * _rowScaleFactor;
		holder.transform = _rightTransform;
		holder.zPosition = _sidePosition;
		rootLayer.zPosition = _sidePosition - 0.1f * (number - selectedIndex);
		rootLayer.sublayerTransform = layerTransform;
	} else {
		pos.x = (self.bounds.size.width - _imageSize.width) / 2;
		holder.transform = CATransform3DIdentity;
		holder.zPosition = _centerPosition;
		holder.filters = nil;
		rootLayer.zPosition = _centerPosition;
		rootLayer.sublayerTransform = CATransform3DIdentity;
	}

	rootLayer.position = pos;
}

Die Animation ist in dem Code (Hier sind die kompletten Source Files) bewusst recht einfach gehalten und unterstützt keine sogenannten Heartbeat Animationen, d.h. Animationen die sogleich beginnen, wenn der Finger aufgesetzt wird. Im käuflichen und auf OpenGL basierten Code ist dies selbstverständlich vorhanden und insbesondere das Laden und Aufbauen der Bilder noch stark optimiert. Aber als Einstieg und fürs bessere Verständnis hoffe ich, dass der Beispielcode hier erstmal weiterhilft. Wer mehr darüber erfahren möchte, dem möchte ich nochmals unsere Workshops ans Herz legen, in denen wir wesentlich tiefer in die Materie einsteigen können. Also, viel Spass beim Cover Flow programmieren und nicht vergessen, auf mein Blog zu verlinken wenn Ihr die erste iApp mit Cover Flow in den Appstore bringt :)

Anfangs basierte dieser Code übrigens auf einem Beispiel Code von Bill Dudney, der damals einen Artikel über die Programmierung eines Cover Flow unter MacOSX veröffenlichte und auch ein sehr gut geschriebenes Buch zum Thema Core Animation geschrieben hat.

One comment

  1. Borut says:

    Hello,

    I would like to test your CoverFlow plugin but don’t know how. Do you have any example?

    Thanks
    Borut