PhotoStackView.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. //
  2. // PhotoStackView.m
  3. //
  4. // Created by Tom Longo on 16/08/12.
  5. // - Twitter: @tomlongo
  6. // - GitHub: github.com/tomlongo
  7. //
  8. #import <QuartzCore/QuartzCore.h>
  9. #import "PhotoStackView.h"
  10. static BOOL const BorderVisibilityDefault = YES;
  11. static CGFloat const BorderWidthDefault = 5.0f;
  12. static CGFloat const PhotoRotationOffsetDefault = 4.0f;
  13. @interface PhotoStackView()
  14. @property (nonatomic, strong) NSArray *photoViews;
  15. @end
  16. @implementation PhotoStackView
  17. @synthesize borderImage = _borderImage;
  18. @synthesize borderWidth = _borderWidth;
  19. #pragma mark -
  20. #pragma mark Getters and Setters
  21. -(void)setDataSource:(id<PhotoStackViewDataSource>)dataSource {
  22. if(dataSource != _dataSource) {
  23. _dataSource = dataSource;
  24. [self reloadData];
  25. }
  26. }
  27. -(void)setPhotoViews:(NSArray *)photoViews {
  28. // Remove current photo views, ready to be replaced with the fresh batch
  29. for(UIView *view in self.photoViews) {
  30. [view removeFromSuperview];
  31. }
  32. for (UIView *view in photoViews) {
  33. // If there is already a view at this index position, use that photo's transform
  34. // rather than setting a new random rotation (this is to stop the stack from shifting
  35. // when new photos are added after the fact)
  36. if([photoViews indexOfObject:view] < [_photoViews count]) {
  37. UIView *existingViewAtIndex = [_photoViews objectAtIndex:[photoViews indexOfObject:view]];
  38. view.transform = existingViewAtIndex.transform;
  39. } else {
  40. [self makeCrooked:view animated:NO];
  41. }
  42. [self insertSubview:view atIndex:0];
  43. }
  44. _photoViews = photoViews;
  45. }
  46. -(UIImage *)borderImage {
  47. if(!_borderImage) {
  48. _borderImage = [UIImage imageNamed:@"PhotoBorder"];
  49. }
  50. return _borderImage;
  51. }
  52. -(void)setBorderImage:(UIImage *)borderImage {
  53. if(borderImage != _borderImage) {
  54. _borderImage = borderImage;
  55. [self reloadData];
  56. }
  57. }
  58. -(void)setShowBorder:(BOOL)showBorder {
  59. if(showBorder != _showBorder) {
  60. _showBorder = showBorder;
  61. [self reloadData];
  62. }
  63. }
  64. -(CGFloat)borderWidth {
  65. return (self.showBorder) ? _borderWidth : 0;
  66. }
  67. -(void)setBorderWidth:(CGFloat)borderWidth {
  68. if(borderWidth != _borderWidth) {
  69. _borderWidth = borderWidth;
  70. [self reloadData];
  71. }
  72. }
  73. -(void)setRotationOffset:(CGFloat)rotationOffset {
  74. if(rotationOffset != _rotationOffset) {
  75. _rotationOffset = rotationOffset;
  76. [self reloadData];
  77. }
  78. }
  79. -(UIColor *)highlightColor {
  80. if(!_highlightColor) {
  81. _highlightColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.15];
  82. }
  83. return _highlightColor;
  84. }
  85. -(void)setHighlighted:(BOOL)highlighted {
  86. [super setHighlighted:highlighted];
  87. UIImageView *topPhoto = [[self topPhoto].subviews lastObject];
  88. if(highlighted) {
  89. UIView *view = [[UIView alloc] initWithFrame:topPhoto.bounds];
  90. view.backgroundColor = self.highlightColor;
  91. [topPhoto addSubview:view];
  92. [topPhoto bringSubviewToFront:view];
  93. } else {
  94. [[topPhoto.subviews lastObject] removeFromSuperview];
  95. }
  96. }
  97. #pragma mark -
  98. #pragma mark Animation
  99. -(void)returnToCenter:(UIView *)photo {
  100. [UIView animateWithDuration:0.2
  101. animations:^{
  102. photo.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  103. }];
  104. }
  105. -(void)flickAway:(UIView *)photo withVelocity:(CGPoint)velocity {
  106. if ([self.delegate respondsToSelector:@selector(photoStackView:willFlickAwayPhotoFromIndex:toIndex:)]) {
  107. NSUInteger fromIndex = [self indexOfTopPhoto];
  108. NSUInteger toIndex = [self indexOfTopPhoto]+1;
  109. NSUInteger numberOfPhotos = [self.dataSource numberOfPhotosInPhotoStackView:self];
  110. if (toIndex >= numberOfPhotos) {
  111. toIndex = 0;
  112. }
  113. [self.delegate photoStackView:self willFlickAwayPhotoFromIndex:fromIndex toIndex:toIndex];
  114. }
  115. CGFloat width = CGRectGetWidth(self.bounds);
  116. CGFloat xPos = (velocity.x < 0) ? CGRectGetMidX(self.bounds)-width : CGRectGetMidY(self.bounds)+width;
  117. [UIView animateWithDuration:0.1
  118. animations:^{
  119. photo.center = CGPointMake(xPos, CGRectGetMidY(self.bounds));
  120. }
  121. completion:^(BOOL finished){
  122. [self makeCrooked:photo animated:YES];
  123. [self sendSubviewToBack:photo];
  124. [self makeStraight:[self topPhoto] animated:YES];
  125. [self returnToCenter:photo];
  126. if ([self.delegate respondsToSelector:@selector(photoStackView:didRevealPhotoAtIndex:)]) {
  127. [self.delegate photoStackView:self didRevealPhotoAtIndex:[self indexOfTopPhoto]];
  128. }
  129. }];
  130. }
  131. -(void)rotatePhoto:(UIView *)photo degrees:(NSInteger)degrees animated:(BOOL)animated {
  132. CGFloat radians = M_PI * degrees / 180.0;
  133. CGAffineTransform transform = CGAffineTransformMakeRotation(radians);
  134. if(animated) {
  135. [UIView animateWithDuration:0.2
  136. animations:^{
  137. photo.transform = transform;
  138. }];
  139. } else {
  140. photo.transform = transform;
  141. }
  142. }
  143. -(void)makeCrooked:(UIView *)photo animated:(BOOL)animated {
  144. NSInteger min = -(self.rotationOffset);
  145. NSInteger max = self.rotationOffset;
  146. NSInteger degrees = (arc4random_uniform(max-min+1)) + min;
  147. [self rotatePhoto:photo degrees:degrees animated:animated];
  148. }
  149. -(void)makeStraight:(UIView *)photo animated:(BOOL)animated {
  150. [self rotatePhoto:photo degrees:0 animated:animated];
  151. }
  152. #pragma mark -
  153. #pragma mark Gesture Handlers
  154. -(void)photoPanned:(UIPanGestureRecognizer *)gesture {
  155. if([self.dataSource numberOfPhotosInPhotoStackView:self]<=1)
  156. return;
  157. UIView *topPhoto = [self topPhoto];
  158. CGPoint velocity = [gesture velocityInView:self];
  159. CGPoint translation = [gesture translationInView:self];
  160. if(gesture.state == UIGestureRecognizerStateBegan) {
  161. [self sendActionsForControlEvents:UIControlEventTouchCancel];
  162. if ([self.delegate respondsToSelector:@selector(photoStackView:willStartMovingPhotoAtIndex:)]) {
  163. [self.delegate photoStackView:self willStartMovingPhotoAtIndex:[self indexOfTopPhoto]];
  164. }
  165. }
  166. if(gesture.state == UIGestureRecognizerStateChanged) {
  167. CGFloat xPos = topPhoto.center.x + translation.x;
  168. CGFloat yPos = topPhoto.center.y + translation.y;
  169. topPhoto.center = CGPointMake(xPos, yPos);
  170. [gesture setTranslation:CGPointMake(0, 0) inView:self];
  171. } else if(gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {
  172. if(abs(velocity.x) > 200) {
  173. [self flickAway:topPhoto withVelocity:velocity];
  174. } else {
  175. [self returnToCenter:topPhoto];
  176. }
  177. }
  178. }
  179. -(void)photoTapped:(UITapGestureRecognizer *)gesture {
  180. [self sendActionsForControlEvents:UIControlEventTouchUpInside];
  181. if ([self.delegate respondsToSelector:@selector(photoStackView:didSelectPhotoAtIndex:)]) {
  182. [self.delegate photoStackView:self didSelectPhotoAtIndex:[self indexOfTopPhoto]];
  183. }
  184. }
  185. -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  186. [super touchesBegan:touches withEvent:event];
  187. if ([self.delegate respondsToSelector:@selector(photoStackView:didSelectPhotoAtIndex:)]) {
  188. // No need to highlight the photo if delegate does not implement a
  189. // selection handler (ie. nothing happens when they tap it)
  190. [self sendActionsForControlEvents:UIControlStateHighlighted];
  191. }
  192. }
  193. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  194. [super touchesMoved:touches withEvent:event];
  195. [self sendActionsForControlEvents:UIControlEventTouchDragInside];
  196. }
  197. -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  198. [super touchesEnded:touches withEvent:event];
  199. [self sendActionsForControlEvents:UIControlEventTouchCancel];
  200. }
  201. #pragma mark -
  202. #pragma mark Other Methods
  203. -(void)flipToNextPhoto{
  204. [self flickAway:[self topPhoto] withVelocity:CGPointMake(-400, 0)];
  205. }
  206. -(void)goToPhotoAtIndex:(NSUInteger)index {
  207. for (UIView *view in self.photoViews) {
  208. if([self.photoViews indexOfObject:view] < index) {
  209. [self sendSubviewToBack:view];
  210. }
  211. }
  212. [self makeStraight:[self topPhoto] animated:NO];
  213. }
  214. -(void)hidePhotoAtIndex:(NSUInteger)index {
  215. if(index < [self.photoViews count]) {
  216. [[self.photoViews objectAtIndex:index] removeFromSuperview];
  217. }
  218. }
  219. -(NSUInteger)indexOfTopPhoto {
  220. return (self.photoViews) ? [self.photoViews indexOfObject:[self topPhoto]] : 0;
  221. }
  222. -(UIView *)topPhoto {
  223. if(self.subviews.count==0)
  224. return nil;
  225. return [self.subviews objectAtIndex:[self.subviews count]-1];
  226. }
  227. -(void)sendActionsForControlEvents:(UIControlEvents)controlEvents {
  228. [super sendActionsForControlEvents:controlEvents];
  229. self.highlighted = (controlEvents == UIControlStateHighlighted) ? YES : NO;
  230. }
  231. #pragma mark -
  232. #pragma mark Setup
  233. -(void)reloadData {
  234. if (!self.dataSource) {
  235. //exit if data source has not been set up yet
  236. self.photoViews = nil;
  237. return;
  238. }
  239. NSInteger numberOfPhotos = [self.dataSource numberOfPhotosInPhotoStackView:self];
  240. NSInteger topPhotoIndex = [self indexOfTopPhoto]; // Keeping track of current photo's top index so that it remains on top if new photos are added
  241. if(numberOfPhotos >= 0) {
  242. NSMutableArray *photoViewsMutable = [[NSMutableArray alloc] initWithCapacity:numberOfPhotos];
  243. UIImage *borderImage = [self.borderImage resizableImageWithCapInsets:UIEdgeInsetsMake(self.borderWidth, self.borderWidth, self.borderWidth, self.borderWidth)];
  244. for (NSUInteger index = 0; index < numberOfPhotos; index++) {
  245. UIImage *image = [self.dataSource photoStackView:self photoForIndex:index];
  246. CGSize imageSize = image.size;
  247. if([self.dataSource respondsToSelector:@selector(photoStackView:photoSizeForIndex:)]){
  248. imageSize = [self.dataSource photoStackView:self photoSizeForIndex:index];
  249. }
  250. UIImageView *photoImageView = [[UIImageView alloc] initWithFrame:(CGRect){CGPointZero, self.frame.size}];
  251. photoImageView.image = image;
  252. UIView *view = [[UIView alloc] initWithFrame:photoImageView.frame];
  253. view.layer.rasterizationScale = [[UIScreen mainScreen] scale];
  254. view.layer.shouldRasterize = YES; // rasterize the view for faster drawing and smooth edges
  255. if (self.showBorder) {
  256. // Add the background image
  257. if (borderImage) {
  258. // If there is a border image, we need to add a background image view, and add some padding around the photo for the border
  259. CGRect photoFrame = photoImageView.frame;
  260. photoFrame.origin = CGPointMake(self.borderWidth, self.borderWidth);
  261. photoImageView.frame = photoFrame;
  262. view.frame = CGRectMake(0, 0, photoImageView.frame.size.width+(self.borderWidth*2), photoImageView.frame.size.height+(self.borderWidth*2));
  263. UIImageView *backgroundImageView = [[UIImageView alloc] initWithFrame:view.frame];
  264. backgroundImageView.image = borderImage;
  265. [view addSubview:backgroundImageView];
  266. } else {
  267. // if there is no boarder image draw one with the CALayer
  268. view.layer.borderWidth = self.borderWidth;
  269. view.layer.borderColor = [[UIColor whiteColor] CGColor];
  270. view.layer.shadowOffset = CGSizeMake(0, 0);
  271. view.layer.shadowOpacity = 0.5;
  272. }
  273. }
  274. [view addSubview:photoImageView];
  275. view.tag = index;
  276. view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  277. [photoViewsMutable addObject:view];
  278. }
  279. // Photo views are added to subview in the photoView setter
  280. self.photoViews = photoViewsMutable;
  281. //photoViewsMutable = nil;
  282. [self goToPhotoAtIndex:topPhotoIndex];
  283. }
  284. }
  285. -(void)setup {
  286. //Defaults
  287. self.showBorder = BorderVisibilityDefault;
  288. self.borderWidth = BorderWidthDefault;
  289. self.rotationOffset = PhotoRotationOffsetDefault;
  290. // Add Pan Gesture
  291. UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(photoPanned:)];
  292. [panGesture setMaximumNumberOfTouches:1];
  293. panGesture.delegate = self;
  294. [self addGestureRecognizer:panGesture];
  295. // Add Tap Gesture
  296. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(photoTapped:)];
  297. [tapGesture setNumberOfTapsRequired:1];
  298. tapGesture.delegate = self;
  299. [self addGestureRecognizer:tapGesture];
  300. [self reloadData];
  301. }
  302. -(id)initWithCoder:(NSCoder *)aDecoder {
  303. if ((self = [super initWithCoder:aDecoder])) {
  304. [self setup];
  305. }
  306. return self;
  307. }
  308. -(id)initWithFrame:(CGRect)frame {
  309. if ((self = [super initWithFrame:frame])) {
  310. [self setup];
  311. }
  312. return self;
  313. }
  314. -(void)dealloc {
  315. [self setPhotoViews:nil];
  316. [self setBorderImage:nil];
  317. [self setHighlightColor:nil];
  318. }
  319. @end