VLCFolderCollectionViewFlowLayout.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. /*****************************************************************************
  2. * VLCFolderCollectionViewFlowLayout.m
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2014 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors: Carola Nitz <nitz.carola # googlemail.com>
  9. *
  10. * Refer to the COPYING file of the official project for license.
  11. *****************************************************************************/
  12. #import "VLCFolderCollectionViewFlowLayout.h"
  13. #import <objc/runtime.h>
  14. #import "VLCLibraryViewController.h"
  15. //framrate were motion appears fluent
  16. #define LX_FRAMES_PER_SECOND 60.0
  17. #ifndef CGGEOMETRY_LXSUPPORT_H_
  18. CG_INLINE CGPoint
  19. LXS_CGPointAdd(CGPoint point1, CGPoint point2) {
  20. return CGPointMake(point1.x + point2.x, point1.y + point2.y);
  21. }
  22. #endif
  23. typedef NS_ENUM(NSInteger, LXScrollingDirection) {
  24. LXScrollingDirectionUnknown = 0,
  25. LXScrollingDirectionUp,
  26. LXScrollingDirectionDown,
  27. LXScrollingDirectionLeft,
  28. LXScrollingDirectionRight
  29. };
  30. static NSString * const kLXScrollingDirectionKey = @"LXScrollingDirection";
  31. static NSString * const kLXCollectionViewKeyPath = @"collectionView";
  32. @interface CADisplayLink (LX_userInfo)
  33. @property (nonatomic, copy) NSDictionary *LX_userInfo;
  34. @end
  35. @implementation CADisplayLink (LX_userInfo)
  36. - (void) setLX_userInfo:(NSDictionary *) LX_userInfo {
  37. objc_setAssociatedObject(self, "LX_userInfo", LX_userInfo, OBJC_ASSOCIATION_COPY);
  38. }
  39. - (NSDictionary *) LX_userInfo {
  40. return objc_getAssociatedObject(self, "LX_userInfo");
  41. }
  42. @end
  43. @interface UICollectionViewCell (VLCFolderCollectionViewLayout)
  44. - (UIImage *)LX_rasterizedImage;
  45. @end
  46. @implementation UICollectionViewCell (VLCFolderCollectionViewLayout)
  47. - (UIImage *)LX_rasterizedImage {
  48. UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0f);
  49. [self.layer renderInContext:UIGraphicsGetCurrentContext()];
  50. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  51. UIGraphicsEndImageContext();
  52. return image;
  53. }
  54. @end
  55. @interface VLCFolderCollectionViewFlowLayout ()
  56. {
  57. NSIndexPath *_selectedItemIndexPath;
  58. UIView *_currentView;
  59. CGPoint _currentViewCenter;
  60. CGPoint _panTranslationInCollectionView;
  61. CADisplayLink *_displayLink;
  62. UIView *_folderView;
  63. BOOL _didPan;
  64. }
  65. @end
  66. @implementation VLCFolderCollectionViewFlowLayout
  67. - (void)setDefaults {
  68. _scrollingSpeed = 300.0f;
  69. _scrollingTriggerEdgeInsets = UIEdgeInsetsMake(50.0f, 50.0f, 50.0f, 50.0f);
  70. }
  71. - (void)setupCollectionView {
  72. _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
  73. action:@selector(handleLongPressGesture:)];
  74. _longPressGestureRecognizer.delegate = self;
  75. // Links the default long press gesture recognizer to the custom long press gesture recognizer we are creating now
  76. // by enforcing failure dependency so that they doesn't clash.
  77. for (UIGestureRecognizer *gestureRecognizer in self.collectionView.gestureRecognizers) {
  78. if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
  79. [gestureRecognizer requireGestureRecognizerToFail:_longPressGestureRecognizer];
  80. }
  81. }
  82. [self.collectionView addGestureRecognizer:_longPressGestureRecognizer];
  83. _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
  84. action:@selector(handlePanGesture:)];
  85. _panGestureRecognizer.delegate = self;
  86. [self.collectionView addGestureRecognizer:_panGestureRecognizer];
  87. // Useful in multiple scenarios: one common scenario being when the Notification Center drawer is pulled down
  88. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleApplicationWillResignActive:) name: UIApplicationWillResignActiveNotification object:nil];
  89. }
  90. - (id)init {
  91. self = [super init];
  92. if (self) {
  93. [self setDefaults];
  94. [self addObserver:self forKeyPath:kLXCollectionViewKeyPath options:NSKeyValueObservingOptionNew context:nil];
  95. }
  96. return self;
  97. }
  98. - (id)initWithCoder:(NSCoder *)aDecoder {
  99. self = [super initWithCoder:aDecoder];
  100. if (self) {
  101. [self setDefaults];
  102. [self addObserver:self forKeyPath:kLXCollectionViewKeyPath options:NSKeyValueObservingOptionNew context:nil];
  103. }
  104. return self;
  105. }
  106. - (void)dealloc {
  107. [self invalidatesScrollTimer];
  108. [self removeObserver:self forKeyPath:kLXCollectionViewKeyPath];
  109. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
  110. }
  111. - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
  112. if ([layoutAttributes.indexPath isEqual:_selectedItemIndexPath])
  113. layoutAttributes.hidden = YES;
  114. }
  115. - (id<VLCFolderCollectionViewDelegateFlowLayout>)delegate {
  116. return (id<VLCFolderCollectionViewDelegateFlowLayout>)self.collectionView.delegate;
  117. }
  118. - (void)invalidateLayoutIfNecessary {
  119. NSIndexPath *newIndexPath = [self.collectionView indexPathForItemAtPoint:_currentView.center];
  120. NSIndexPath *previousIndexPath = _selectedItemIndexPath;
  121. if ((newIndexPath == nil) || [newIndexPath isEqual:previousIndexPath]) {
  122. _currentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
  123. [_folderView removeFromSuperview];
  124. return;
  125. }
  126. UICollectionViewCell *cell = [self.collectionView.dataSource collectionView:self.collectionView cellForItemAtIndexPath:newIndexPath];
  127. if (!_folderView) {
  128. _folderView = [[UIView alloc] initWithFrame:cell.frame];
  129. _folderView.backgroundColor = [UIColor VLCOrangeTintColor];
  130. _folderView.layer.cornerRadius = 8;
  131. }
  132. [self.collectionView insertSubview:_folderView atIndex:0];
  133. if (!CGPointEqualToPoint(_folderView.center,cell.center))
  134. _folderView.frame = cell.frame;
  135. [UIView
  136. animateWithDuration:0.3
  137. delay:0.0
  138. options:UIViewAnimationOptionBeginFromCurrentState
  139. animations:^{
  140. _currentView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
  141. }
  142. completion:nil];
  143. }
  144. - (void)invalidatesScrollTimer {
  145. if (!_displayLink.paused)
  146. [_displayLink invalidate];
  147. _displayLink = nil;
  148. }
  149. - (void)setupScrollTimerInDirection:(LXScrollingDirection)direction {
  150. if (!_displayLink.paused) {
  151. LXScrollingDirection oldDirection = [_displayLink.LX_userInfo[kLXScrollingDirectionKey] integerValue];
  152. if (direction == oldDirection)
  153. return;
  154. }
  155. [self invalidatesScrollTimer];
  156. _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleScroll:)];
  157. _displayLink.LX_userInfo = @{ kLXScrollingDirectionKey : @(direction) };
  158. [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
  159. }
  160. #pragma mark - Target/Action methods
  161. // Tight loop, allocate memory sparely, even if they are stack allocation.
  162. - (void)handleScroll:(CADisplayLink *)displayLink {
  163. LXScrollingDirection direction = (LXScrollingDirection)[displayLink.LX_userInfo[kLXScrollingDirectionKey] integerValue];
  164. if (direction == LXScrollingDirectionUnknown)
  165. return;
  166. CGSize frameSize = self.collectionView.bounds.size;
  167. CGSize contentSize = self.collectionView.contentSize;
  168. CGPoint contentOffset = self.collectionView.contentOffset;
  169. // Important to have an integer `distance` as the `contentOffset` property automatically gets rounded
  170. // and it would diverge from the view's center resulting in a "cell is slipping away under finger"-bug.
  171. CGFloat distance = rint(self.scrollingSpeed / LX_FRAMES_PER_SECOND);
  172. CGPoint translation = CGPointZero;
  173. switch(direction) {
  174. case LXScrollingDirectionUp: {
  175. distance = -distance;
  176. CGFloat minY = 0.0f;
  177. if ((contentOffset.y + distance) <= minY)
  178. distance = -contentOffset.y;
  179. translation = CGPointMake(0.0f, distance);
  180. } break;
  181. case LXScrollingDirectionDown: {
  182. CGFloat maxY = MAX(contentSize.height, frameSize.height) - frameSize.height;
  183. if ((contentOffset.y + distance) >= maxY)
  184. distance = maxY - contentOffset.y;
  185. translation = CGPointMake(0.0f, distance);
  186. } break;
  187. case LXScrollingDirectionLeft: {
  188. distance = -distance;
  189. CGFloat minX = 0.0f;
  190. if ((contentOffset.x + distance) <= minX)
  191. distance = -contentOffset.x;
  192. translation = CGPointMake(distance, 0.0f);
  193. } break;
  194. case LXScrollingDirectionRight: {
  195. CGFloat maxX = MAX(contentSize.width, frameSize.width) - frameSize.width;
  196. if ((contentOffset.x + distance) >= maxX)
  197. distance = maxX - contentOffset.x;
  198. translation = CGPointMake(distance, 0.0f);
  199. } break;
  200. default: {
  201. // Do nothing...
  202. } break;
  203. }
  204. _currentViewCenter = LXS_CGPointAdd(_currentViewCenter, translation);
  205. _currentView.center = LXS_CGPointAdd(_currentViewCenter, _panTranslationInCollectionView);
  206. self.collectionView.contentOffset = LXS_CGPointAdd(contentOffset, translation);
  207. }
  208. - (void)handleLongPressGesture:(UILongPressGestureRecognizer *)gestureRecognizer {
  209. //keeps the controller from dragging while not in editmode
  210. if (!((VLCLibraryViewController *)self.delegate).isEditing) return;
  211. switch(gestureRecognizer.state) {
  212. case UIGestureRecognizerStateBegan: {
  213. NSIndexPath *currentIndexPath = [self.collectionView indexPathForItemAtPoint:[gestureRecognizer locationInView:self.collectionView]];
  214. _selectedItemIndexPath = currentIndexPath;
  215. UICollectionViewCell *collectionViewCell = [self.collectionView cellForItemAtIndexPath:_selectedItemIndexPath];
  216. _currentView = [[UIView alloc] initWithFrame:collectionViewCell.frame];
  217. collectionViewCell.highlighted = YES;
  218. UIImageView *highlightedImageView = [[UIImageView alloc] initWithImage:[collectionViewCell LX_rasterizedImage]];
  219. highlightedImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  220. highlightedImageView.alpha = 1.0f;
  221. collectionViewCell.highlighted = NO;
  222. UIImageView *imageView = [[UIImageView alloc] initWithImage:[collectionViewCell LX_rasterizedImage]];
  223. imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  224. imageView.alpha = 0.0f;
  225. [_currentView addSubview:imageView];
  226. [_currentView addSubview:highlightedImageView];
  227. [self.collectionView addSubview:_currentView];
  228. _currentViewCenter = _currentView.center;
  229. [UIView
  230. animateWithDuration:0.3
  231. delay:0.0
  232. options:UIViewAnimationOptionBeginFromCurrentState
  233. animations:^{
  234. _currentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
  235. highlightedImageView.alpha = 0.0f;
  236. imageView.alpha = 1.0f;
  237. }
  238. completion:^(BOOL finished) {
  239. [highlightedImageView removeFromSuperview];
  240. }];
  241. [self invalidateLayout];
  242. } break;
  243. case UIGestureRecognizerStateCancelled:
  244. case UIGestureRecognizerStateEnded: {
  245. if (_didPan) return;
  246. NSIndexPath *currentIndexPath = _selectedItemIndexPath;
  247. if (currentIndexPath) {
  248. _selectedItemIndexPath = nil;
  249. _currentViewCenter = CGPointZero;
  250. UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:currentIndexPath];
  251. [UIView
  252. animateWithDuration:0.3
  253. delay:0.0
  254. options:UIViewAnimationOptionBeginFromCurrentState
  255. animations:^{
  256. _currentView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
  257. _currentView.center = layoutAttributes.center;
  258. }
  259. completion:^(BOOL finished) {
  260. [_currentView removeFromSuperview];
  261. _currentView = nil;
  262. [self invalidateLayout];
  263. }];
  264. }
  265. } break;
  266. default: break;
  267. }
  268. }
  269. - (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer {
  270. switch (gestureRecognizer.state) {
  271. case UIGestureRecognizerStateBegan:
  272. _didPan = YES;
  273. case UIGestureRecognizerStateChanged: {
  274. _panTranslationInCollectionView = [gestureRecognizer translationInView:self.collectionView];
  275. CGPoint viewCenter = _currentView.center = LXS_CGPointAdd(_currentViewCenter, _panTranslationInCollectionView);
  276. [self invalidateLayoutIfNecessary];
  277. switch (self.scrollDirection) {
  278. case UICollectionViewScrollDirectionVertical: {
  279. if (viewCenter.y < (CGRectGetMinY(self.collectionView.bounds) + self.scrollingTriggerEdgeInsets.top)) {
  280. [self setupScrollTimerInDirection:LXScrollingDirectionUp];
  281. } else {
  282. if (viewCenter.y > (CGRectGetMaxY(self.collectionView.bounds) - self.scrollingTriggerEdgeInsets.bottom)) {
  283. [self setupScrollTimerInDirection:LXScrollingDirectionDown];
  284. } else {
  285. [self invalidatesScrollTimer];
  286. }
  287. }
  288. } break;
  289. case UICollectionViewScrollDirectionHorizontal: {
  290. if (viewCenter.x < (CGRectGetMinX(self.collectionView.bounds) + self.scrollingTriggerEdgeInsets.left)) {
  291. [self setupScrollTimerInDirection:LXScrollingDirectionLeft];
  292. } else {
  293. if (viewCenter.x > (CGRectGetMaxX(self.collectionView.bounds) - self.scrollingTriggerEdgeInsets.right)) {
  294. [self setupScrollTimerInDirection:LXScrollingDirectionRight];
  295. } else {
  296. [self invalidatesScrollTimer];
  297. }
  298. }
  299. } break;
  300. }
  301. } break;
  302. case UIGestureRecognizerStateCancelled:
  303. case UIGestureRecognizerStateEnded: {
  304. _didPan = NO;
  305. [_folderView removeFromSuperview];
  306. _folderView = nil;
  307. NSIndexPath *newIndexPath = [self.collectionView indexPathForItemAtPoint:_currentView.center];
  308. NSIndexPath *currentIndexPath = _selectedItemIndexPath;
  309. if (newIndexPath != nil && ![currentIndexPath isEqual:newIndexPath] && ((VLCLibraryViewController *)self.delegate).isEditing) {
  310. [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
  311. _currentView.transform = CGAffineTransformMakeScale(0.1f, 0.1f);
  312. _currentView.center = [self layoutAttributesForItemAtIndexPath:newIndexPath].center;
  313. } completion:^(BOOL finished) {
  314. [self.delegate collectionView:self.collectionView requestToMoveItemAtIndexPath:currentIndexPath intoFolderAtIndexPath:newIndexPath];
  315. _selectedItemIndexPath = nil;
  316. _currentViewCenter = CGPointZero;
  317. [_currentView removeFromSuperview];
  318. _currentView = nil;
  319. }];
  320. } else if (currentIndexPath) {
  321. [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
  322. _currentView.center = [self layoutAttributesForItemAtIndexPath:currentIndexPath].center;
  323. } completion:^(BOOL finished) {
  324. _selectedItemIndexPath = nil;
  325. _currentViewCenter = CGPointZero;
  326. [_currentView removeFromSuperview];
  327. _currentView = nil;
  328. [self invalidateLayout];
  329. }];
  330. }
  331. [self invalidatesScrollTimer];
  332. } break;
  333. default: {
  334. // Do nothing...
  335. } break;
  336. }
  337. }
  338. #pragma mark - UICollectionViewLayout overridden methods
  339. - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
  340. NSArray *layoutAttributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
  341. for (UICollectionViewLayoutAttributes *layoutAttributes in layoutAttributesForElementsInRect) {
  342. switch (layoutAttributes.representedElementCategory) {
  343. case UICollectionElementCategoryCell: {
  344. [self applyLayoutAttributes:layoutAttributes];
  345. } break;
  346. default: {
  347. // Do nothing...
  348. } break;
  349. }
  350. }
  351. return layoutAttributesForElementsInRect;
  352. }
  353. - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
  354. UICollectionViewLayoutAttributes *layoutAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
  355. switch (layoutAttributes.representedElementCategory) {
  356. case UICollectionElementCategoryCell: {
  357. [self applyLayoutAttributes:layoutAttributes];
  358. } break;
  359. default: {
  360. // Do nothing...
  361. } break;
  362. }
  363. return layoutAttributes;
  364. }
  365. #pragma mark - UIGestureRecognizerDelegate methods
  366. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
  367. if ([self.panGestureRecognizer isEqual:gestureRecognizer]) {
  368. return (_selectedItemIndexPath != nil);
  369. }
  370. return YES;
  371. }
  372. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  373. if ([self.longPressGestureRecognizer isEqual:gestureRecognizer]) {
  374. return [self.panGestureRecognizer isEqual:otherGestureRecognizer];
  375. }
  376. if ([self.panGestureRecognizer isEqual:gestureRecognizer]) {
  377. return [self.longPressGestureRecognizer isEqual:otherGestureRecognizer];
  378. }
  379. return NO;
  380. }
  381. #pragma mark - Key-Value Observing methods
  382. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  383. if ([keyPath isEqualToString:kLXCollectionViewKeyPath]) {
  384. if (self.collectionView != nil) {
  385. [self setupCollectionView];
  386. } else {
  387. [self invalidatesScrollTimer];
  388. }
  389. }
  390. }
  391. #pragma mark - Notifications
  392. - (void)handleApplicationWillResignActive:(NSNotification *)notification {
  393. self.panGestureRecognizer.enabled = NO;
  394. self.panGestureRecognizer.enabled = YES;
  395. }
  396. @end