// // PSCoverFlow.m // CoverFlow // // Created by Adam Czubernat on 23.12.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "PSCoverFlow.h" #import @interface PSCoverFlow () { CAScrollLayer *scrollLayer; CATransform3D perspective; CATransform3D leftTransform; CATransform3D rightTransform; CGSize size; NSInteger selectedIndex; } - (void)selectIndex:(NSInteger)index; - (void)postSelectionChange; - (void)layoutLayers; - (void)layoutShadowLayer:(CALayer *)layer; @end @implementation PSCoverFlow @synthesize delegate; - (id)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { self.layer = [CALayer layer]; self.wantsLayer = YES; scrollLayer = [CAScrollLayer layer]; scrollLayer.frame = (CGRect) { CGPointZero, frameRect.size }; scrollLayer.scrollMode = kCAScrollHorizontally; scrollLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; scrollLayer.layoutManager = self; [self.layer addSublayer:scrollLayer]; perspective = CATransform3DIdentity; perspective.m34 = - 1.0f / 800.0f; // Gradient mask CAGradientLayer *gradient = [CAGradientLayer layer]; gradient.frame = (CGRect) { CGPointZero, frameRect.size }; gradient.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; id clearColor = (__bridge id)CGColorGetConstantColor(kCGColorClear); id opaqueColor = (__bridge id)CGColorGetConstantColor(kCGColorBlack); gradient.colors = @[ clearColor, opaqueColor, opaqueColor, clearColor ]; gradient.startPoint = CGPointMake(-0.1f, 0); gradient.endPoint = CGPointMake(1.1f, 0); self.layer.mask = gradient; } return self; } - (void)setFrame:(NSRect)frameRect { [CATransaction begin]; [CATransaction setDisableActions:YES]; [super setFrame:frameRect]; [self layoutLayers]; [CATransaction commit]; } - (void)viewDidMoveToWindow { if (self.window) [self reload]; } #pragma mark - CALayoutManager - (void)layoutSublayersOfLayer:(CALayer *)layer { CGFloat spacing = size.width * 0.8f; [scrollLayer scrollToPoint:(CGPoint) { size.width * selectedIndex - (self.frame.size.width - size.width) / 2.0f, 0.0f }]; [layer.sublayers enumerateObjectsUsingBlock:^(CALayer *sublayer, NSUInteger idx, BOOL *stop) { CATransform3D transform = CATransform3DIdentity; if (idx < selectedIndex) { CGFloat x = spacing * (selectedIndex - idx); transform = CATransform3DTranslate(transform, x, 0, 0); transform = CATransform3DConcat(leftTransform, transform); } else if (idx > selectedIndex) { CGFloat x = -spacing * (idx - selectedIndex); transform = CATransform3DTranslate(transform, x, 0, 0); transform = CATransform3DConcat(rightTransform, transform); } sublayer.transform = transform; }]; } #pragma mark - Private - (void)postSelectionChange { if (!self.superview) return; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(postSelectionChange) object:nil]; if ([delegate respondsToSelector:@selector(coverFlow:didSelectIndex:)]) [delegate coverFlow:self didSelectIndex:selectedIndex]; } - (void)selectIndex:(NSInteger)index { if (index >= scrollLayer.sublayers.count || index < 0 || index == selectedIndex) return; selectedIndex = index; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(postSelectionChange) object:nil]; [self performSelector:@selector(postSelectionChange) withObject:nil afterDelay:0.2f]; [CATransaction begin]; [CATransaction setAnimationTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; [scrollLayer setNeedsLayout]; [CATransaction commit]; } - (void)layoutLayers { CGFloat angle = 60.0f * M_PI/180; CGFloat recess = -200.0f; CGFloat sizeScale = 0.85f; size = self.frame.size; size.width = size.height *= sizeScale; CGFloat margin = size.width * 0.6f; CGFloat frameMargin = self.frame.size.height * (1.0f - sizeScale) / 2.0f; leftTransform = CATransform3DTranslate(perspective, -margin, 0, recess); leftTransform = CATransform3DRotate(leftTransform, angle, 0, 1, 0); rightTransform = CATransform3DTranslate(perspective, margin, 0, recess); rightTransform = CATransform3DRotate(rightTransform, -angle, 0, 1, 0); [scrollLayer.sublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) { // Resize layers layer.frame = (CGRect) { idx * size.width, frameMargin, size }; [self layoutShadowLayer:layer]; }]; } - (void)layoutShadowLayer:(CALayer *)layer { NSImage *image = layer.contents; if (!image) { layer.shadowOpacity = 0; return; } // Shadow aspect fit CGRect rect = { .size = [layer.contents size] }; CGFloat ratio = fminf(size.width / rect.size.width, size.height / rect.size.height); rect.size = (CGSize) { rect.size.width * ratio, rect.size.height * ratio }; rect.origin = (CGPoint) { (size.width - rect.size.width) / 2.0f, (size.height - rect.size.height) / 2.0f }; // Resize shadow CGPathRef shadowPath = CGPathCreateWithRect(rect, NULL); layer.shadowRadius = size.height * 0.02f; layer.shadowPath = shadowPath; layer.shadowOpacity = 1.0f; CGPathRelease(shadowPath); } #pragma mark - Public - (void)reload { [scrollLayer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)]; NSInteger imagesCount = 0; if ([delegate respondsToSelector:@selector(numberOfImagesInCoverFlow:)]) imagesCount = [delegate numberOfImagesInCoverFlow:self]; for (NSInteger index = 0; index < imagesCount; index++) { CALayer *layer = [CALayer layer]; layer.shouldRasterize = YES; layer.contentsGravity = kCAGravityResizeAspect; CGColorRef color = CGColorCreateGenericGray(0, 0.2f); layer.borderColor = color; CGColorRelease(color); layer.shadowOffset = CGSizeZero; layer.shadowColor = CGColorGetConstantColor(kCGColorBlack); [scrollLayer addSublayer:layer]; } [self reloadImagesInRange:(NSRange) { 0, imagesCount }]; [self selectIndex:0]; [self layoutLayers]; } - (void)reloadImagesInRange:(NSRange)range { NSInteger count = MIN(range.location + range.length, scrollLayer.sublayers.count); for (NSInteger index = range.location; index < count; index++) { NSImage *image = nil; if ([delegate respondsToSelector:@selector(coverFlow:imageForIndex:)]) image = [delegate coverFlow:self imageForIndex:index]; // image = image ?: [NSImage imageNamed:NSImageNameQuickLookTemplate]; CALayer *layer = [scrollLayer.sublayers objectAtIndex:index]; layer.contents = image; CGColorRef color = image ? nil : CGColorCreateGenericGray(0.7, 0.9f); layer.backgroundColor = color; CGColorRelease(color); layer.borderWidth = image ? 0 : 2.0f; [self layoutShadowLayer:layer]; } } - (NSInteger)selectedIndex { return selectedIndex; } - (void)setSelectedIndex:(NSInteger)index { if (index >= scrollLayer.sublayers.count || index < 0 || index == selectedIndex) return; selectedIndex = index; [CATransaction begin]; [CATransaction setAnimationTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; [scrollLayer setNeedsLayout]; [CATransaction commit]; } #pragma mark - NSResponder - (BOOL)acceptsFirstResponder { return YES; } - (void)mouseDown:(NSEvent *)theEvent { CGPoint point = [self convertPoint:theEvent.locationInWindow fromView:nil]; CALayer *layer = [scrollLayer hitTest:point]; NSInteger index = [scrollLayer.sublayers indexOfObjectIdenticalTo:layer]; if (index != NSNotFound) [self selectIndex:index]; } - (void)scrollWheel:(NSEvent *)theEvent { CGFloat delta = theEvent.deltaX ?: theEvent.deltaY; if (fabs(delta) > 1.0f) [self selectIndex:selectedIndex + (delta > 0 ? -1 : 1)]; } - (void)keyDown:(NSEvent *)event { unichar keyChar = [[event charactersIgnoringModifiers] characterAtIndex:0]; if (keyChar == NSLeftArrowFunctionKey) [self moveLeft:nil]; else if (keyChar == NSRightArrowFunctionKey) [self moveRight:nil]; else if (keyChar == NSUpArrowFunctionKey) [self moveUp:nil]; else if (keyChar == NSDownArrowFunctionKey) [self moveDown:nil]; else [super keyDown:event]; } - (void)moveUp:(id)sender { [self moveLeft:sender]; } - (void)moveDown:(id)sender { [self moveRight:sender]; } - (void)moveLeft:(id)sender { [self selectIndex:selectedIndex - 1]; } - (void)moveRight:(id)sender { [self selectIndex:selectedIndex + 1]; } @end