| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- //
- // PhotoStackView.m
- //
- // Created by Tom Longo on 16/08/12.
- // - Twitter: @tomlongo
- // - GitHub: github.com/tomlongo
- //
- #import <QuartzCore/QuartzCore.h>
- #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<PhotoStackViewDataSource>)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
|