// // PhotoStackView.m // // Created by Tom Longo on 16/08/12. // - Twitter: @tomlongo // - GitHub: github.com/tomlongo // #import #import "PhotoStackView.h" static BOOL const BorderVisibilityDefault = YES; static CGFloat const BorderWidthDefault = 5.0f; static CGFloat const PhotoRotationOffsetDefault = 4.0f; @interface PhotoStackView() @property (nonatomic, strong) NSArray *photoViews; @end @implementation PhotoStackView @synthesize borderImage = _borderImage; @synthesize borderWidth = _borderWidth; #pragma mark - #pragma mark Getters and Setters -(void)setDataSource:(id)dataSource { if(dataSource != _dataSource) { _dataSource = dataSource; [self reloadData]; } } -(void)setPhotoViews:(NSArray *)photoViews { // Remove current photo views, ready to be replaced with the fresh batch for(UIView *view in self.photoViews) { [view removeFromSuperview]; } for (UIView *view in photoViews) { // If there is already a view at this index position, use that photo's transform // rather than setting a new random rotation (this is to stop the stack from shifting // when new photos are added after the fact) if([photoViews indexOfObject:view] < [_photoViews count]) { UIView *existingViewAtIndex = [_photoViews objectAtIndex:[photoViews indexOfObject:view]]; view.transform = existingViewAtIndex.transform; } else { [self makeCrooked:view animated:NO]; } [self insertSubview:view atIndex:0]; } _photoViews = photoViews; } -(UIImage *)borderImage { if(!_borderImage) { _borderImage = [UIImage imageNamed:@"PhotoBorder"]; } return _borderImage; } -(void)setBorderImage:(UIImage *)borderImage { if(borderImage != _borderImage) { _borderImage = borderImage; [self reloadData]; } } -(void)setShowBorder:(BOOL)showBorder { if(showBorder != _showBorder) { _showBorder = showBorder; [self reloadData]; } } -(CGFloat)borderWidth { return (self.showBorder) ? _borderWidth : 0; } -(void)setBorderWidth:(CGFloat)borderWidth { if(borderWidth != _borderWidth) { _borderWidth = borderWidth; [self reloadData]; } } -(void)setRotationOffset:(CGFloat)rotationOffset { if(rotationOffset != _rotationOffset) { _rotationOffset = rotationOffset; [self reloadData]; } } -(UIColor *)highlightColor { if(!_highlightColor) { _highlightColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.15]; } return _highlightColor; } -(void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; UIImageView *topPhoto = [[self topPhoto].subviews lastObject]; if(highlighted) { UIView *view = [[UIView alloc] initWithFrame:topPhoto.bounds]; view.backgroundColor = self.highlightColor; [topPhoto addSubview:view]; [topPhoto bringSubviewToFront:view]; } else { [[topPhoto.subviews lastObject] removeFromSuperview]; } } #pragma mark - #pragma mark Animation -(void)returnToCenter:(UIView *)photo { [UIView animateWithDuration:0.2 animations:^{ photo.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); }]; } -(void)flickAway:(UIView *)photo withVelocity:(CGPoint)velocity { if ([self.delegate respondsToSelector:@selector(photoStackView:willFlickAwayPhotoFromIndex:toIndex:)]) { NSUInteger fromIndex = [self indexOfTopPhoto]; NSUInteger toIndex = [self indexOfTopPhoto]+1; NSUInteger numberOfPhotos = [self.dataSource numberOfPhotosInPhotoStackView:self]; if (toIndex >= numberOfPhotos) { toIndex = 0; } [self.delegate photoStackView:self willFlickAwayPhotoFromIndex:fromIndex toIndex:toIndex]; } CGFloat width = CGRectGetWidth(self.bounds); CGFloat xPos = (velocity.x < 0) ? CGRectGetMidX(self.bounds)-width : CGRectGetMidY(self.bounds)+width; [UIView animateWithDuration:0.1 animations:^{ photo.center = CGPointMake(xPos, CGRectGetMidY(self.bounds)); } completion:^(BOOL finished){ [self makeCrooked:photo animated:YES]; [self sendSubviewToBack:photo]; [self makeStraight:[self topPhoto] animated:YES]; [self returnToCenter:photo]; if ([self.delegate respondsToSelector:@selector(photoStackView:didRevealPhotoAtIndex:)]) { [self.delegate photoStackView:self didRevealPhotoAtIndex:[self indexOfTopPhoto]]; } }]; } -(void)rotatePhoto:(UIView *)photo degrees:(NSInteger)degrees animated:(BOOL)animated { CGFloat radians = M_PI * degrees / 180.0; CGAffineTransform transform = CGAffineTransformMakeRotation(radians); if(animated) { [UIView animateWithDuration:0.2 animations:^{ photo.transform = transform; }]; } else { photo.transform = transform; } } -(void)makeCrooked:(UIView *)photo animated:(BOOL)animated { NSInteger min = -(self.rotationOffset); NSInteger max = self.rotationOffset; NSInteger degrees = (arc4random_uniform(max-min+1)) + min; [self rotatePhoto:photo degrees:degrees animated:animated]; } -(void)makeStraight:(UIView *)photo animated:(BOOL)animated { [self rotatePhoto:photo degrees:0 animated:animated]; } #pragma mark - #pragma mark Gesture Handlers -(void)photoPanned:(UIPanGestureRecognizer *)gesture { if([self.dataSource numberOfPhotosInPhotoStackView:self]<=1) return; UIView *topPhoto = [self topPhoto]; CGPoint velocity = [gesture velocityInView:self]; CGPoint translation = [gesture translationInView:self]; if(gesture.state == UIGestureRecognizerStateBegan) { [self sendActionsForControlEvents:UIControlEventTouchCancel]; if ([self.delegate respondsToSelector:@selector(photoStackView:willStartMovingPhotoAtIndex:)]) { [self.delegate photoStackView:self willStartMovingPhotoAtIndex:[self indexOfTopPhoto]]; } } if(gesture.state == UIGestureRecognizerStateChanged) { CGFloat xPos = topPhoto.center.x + translation.x; CGFloat yPos = topPhoto.center.y + translation.y; topPhoto.center = CGPointMake(xPos, yPos); [gesture setTranslation:CGPointMake(0, 0) inView:self]; } else if(gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) { if(abs(velocity.x) > 200) { [self flickAway:topPhoto withVelocity:velocity]; } else { [self returnToCenter:topPhoto]; } } } -(void)photoTapped:(UITapGestureRecognizer *)gesture { [self sendActionsForControlEvents:UIControlEventTouchUpInside]; if ([self.delegate respondsToSelector:@selector(photoStackView:didSelectPhotoAtIndex:)]) { [self.delegate photoStackView:self didSelectPhotoAtIndex:[self indexOfTopPhoto]]; } } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; if ([self.delegate respondsToSelector:@selector(photoStackView:didSelectPhotoAtIndex:)]) { // No need to highlight the photo if delegate does not implement a // selection handler (ie. nothing happens when they tap it) [self sendActionsForControlEvents:UIControlStateHighlighted]; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; [self sendActionsForControlEvents:UIControlEventTouchDragInside]; } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; [self sendActionsForControlEvents:UIControlEventTouchCancel]; } #pragma mark - #pragma mark Other Methods -(void)flipToNextPhoto{ [self flickAway:[self topPhoto] withVelocity:CGPointMake(-400, 0)]; } -(void)goToPhotoAtIndex:(NSUInteger)index { for (UIView *view in self.photoViews) { if([self.photoViews indexOfObject:view] < index) { [self sendSubviewToBack:view]; } } [self makeStraight:[self topPhoto] animated:NO]; } -(void)hidePhotoAtIndex:(NSUInteger)index { if(index < [self.photoViews count]) { [[self.photoViews objectAtIndex:index] removeFromSuperview]; } } -(NSUInteger)indexOfTopPhoto { return (self.photoViews.count > 0) ? [self.photoViews indexOfObject:[self topPhoto]] : 0; } -(UIView *)topPhoto { if(self.subviews.count==0) return nil; return [self.subviews objectAtIndex:[self.subviews count]-1]; } -(void)sendActionsForControlEvents:(UIControlEvents)controlEvents { [super sendActionsForControlEvents:controlEvents]; self.highlighted = (controlEvents == UIControlStateHighlighted) ? YES : NO; } #pragma mark - #pragma mark Setup -(void)reloadData { if (!self.dataSource) { //exit if data source has not been set up yet self.photoViews = nil; return; } NSInteger numberOfPhotos = [self.dataSource numberOfPhotosInPhotoStackView:self]; NSInteger topPhotoIndex = [self indexOfTopPhoto]; // Keeping track of current photo's top index so that it remains on top if new photos are added if(numberOfPhotos >= 0) { NSMutableArray *photoViewsMutable = [[NSMutableArray alloc] initWithCapacity:numberOfPhotos]; UIImage *borderImage = [self.borderImage resizableImageWithCapInsets:UIEdgeInsetsMake(self.borderWidth, self.borderWidth, self.borderWidth, self.borderWidth)]; for (NSUInteger index = 0; index < numberOfPhotos; index++) { UIImage *image = [self.dataSource photoStackView:self photoForIndex:index]; CGSize imageSize = image.size; if([self.dataSource respondsToSelector:@selector(photoStackView:photoSizeForIndex:)]){ imageSize = [self.dataSource photoStackView:self photoSizeForIndex:index]; } UIImageView *photoImageView = [[UIImageView alloc] initWithFrame:(CGRect){CGPointZero, self.frame.size}]; photoImageView.contentMode = UIViewContentModeScaleAspectFit; photoImageView.image = image; UIView *view = [[UIView alloc] initWithFrame:photoImageView.frame]; view.layer.rasterizationScale = [[UIScreen mainScreen] scale]; view.layer.shouldRasterize = YES; // rasterize the view for faster drawing and smooth edges if (self.showBorder) { // Add the background image if (borderImage) { // If there is a border image, we need to add a background image view, and add some padding around the photo for the border CGRect photoFrame = photoImageView.frame; photoFrame.origin = CGPointMake(self.borderWidth, self.borderWidth); photoImageView.frame = photoFrame; view.frame = CGRectMake(0, 0, photoImageView.frame.size.width+(self.borderWidth*2), photoImageView.frame.size.height+(self.borderWidth*2)); UIImageView *backgroundImageView = [[UIImageView alloc] initWithFrame:view.frame]; backgroundImageView.image = borderImage; [view addSubview:backgroundImageView]; } else { // if there is no boarder image draw one with the CALayer view.layer.borderWidth = self.borderWidth; view.layer.borderColor = [[UIColor whiteColor] CGColor]; view.layer.shadowOffset = CGSizeMake(0, 0); view.layer.shadowOpacity = 0.5; } } [view addSubview:photoImageView]; view.tag = index; view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); [photoViewsMutable addObject:view]; } // Photo views are added to subview in the photoView setter self.photoViews = photoViewsMutable; //photoViewsMutable = nil; [self goToPhotoAtIndex:topPhotoIndex]; } } -(void)setup { //Defaults self.showBorder = BorderVisibilityDefault; self.borderWidth = BorderWidthDefault; self.rotationOffset = PhotoRotationOffsetDefault; // Add Pan Gesture UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(photoPanned:)]; [panGesture setMaximumNumberOfTouches:1]; panGesture.delegate = self; [self addGestureRecognizer:panGesture]; // Add Tap Gesture UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(photoTapped:)]; [tapGesture setNumberOfTapsRequired:1]; tapGesture.delegate = self; [self addGestureRecognizer:tapGesture]; [self reloadData]; } -(id)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { [self setup]; } return self; } -(id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self setup]; } return self; } -(void)dealloc { [self setPhotoViews:nil]; [self setBorderImage:nil]; [self setHighlightColor:nil]; } @end