LxGridViewFlowLayout.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. //
  2. // LxGridViewFlowLayout.m
  3. // LxGridView
  4. //
  5. #import "LxGridViewFlowLayout.h"
  6. #import "TZTestCell.h"
  7. #import "UIView+Layout.h"
  8. #define stringify __STRING
  9. static CGFloat const PRESS_TO_MOVE_MIN_DURATION = 0.1;
  10. static CGFloat const MIN_PRESS_TO_BEGIN_EDITING_DURATION = 0.6;
  11. CG_INLINE CGPoint CGPointOffset(CGPoint point, CGFloat dx, CGFloat dy)
  12. {
  13. return CGPointMake(point.x + dx, point.y + dy);
  14. }
  15. /*
  16. 此类来源于DeveloperLx的优秀开源项目:LxGridView
  17. github链接:https://github.com/DeveloperLx/LxGridView
  18. 我对这个类的代码做了一些修改;
  19. 感谢DeveloperLx的优秀代码~
  20. */
  21. @interface LxGridViewFlowLayout () <UIGestureRecognizerDelegate>
  22. @property (nonatomic,readonly) id<LxGridViewDataSource> dataSource;
  23. @property (nonatomic,readonly) id<LxGridViewDelegateFlowLayout> delegate;
  24. @end
  25. @implementation LxGridViewFlowLayout
  26. {
  27. UILongPressGestureRecognizer * _longPressGestureRecognizer;
  28. UIPanGestureRecognizer * _panGestureRecognizer;
  29. NSIndexPath * _movingItemIndexPath;
  30. UIView * _beingMovedPromptView;
  31. CGPoint _sourceItemCollectionViewCellCenter;
  32. CADisplayLink * _displayLink;
  33. CFTimeInterval _remainSecondsToBeginEditing;
  34. }
  35. #pragma mark - setup
  36. - (void)dealloc
  37. {
  38. [_displayLink invalidate];
  39. [self removeGestureRecognizers];
  40. [self removeObserver:self forKeyPath:@stringify(collectionView)];
  41. }
  42. - (instancetype)init
  43. {
  44. if (self = [super init]) {
  45. [self setup];
  46. }
  47. return self;
  48. }
  49. - (instancetype)initWithCoder:(NSCoder *)coder
  50. {
  51. if (self = [super initWithCoder:coder]) {
  52. [self setup];
  53. }
  54. return self;
  55. }
  56. - (void)setup
  57. {
  58. [self addObserver:self forKeyPath:@stringify(collectionView) options:NSKeyValueObservingOptionNew context:nil];
  59. }
  60. - (void)addGestureRecognizers
  61. {
  62. self.collectionView.userInteractionEnabled = YES;
  63. _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressGestureRecognizerTriggerd:)];
  64. _longPressGestureRecognizer.cancelsTouchesInView = NO;
  65. _longPressGestureRecognizer.minimumPressDuration = PRESS_TO_MOVE_MIN_DURATION;
  66. _longPressGestureRecognizer.delegate = self;
  67. for (UIGestureRecognizer * gestureRecognizer in self.collectionView.gestureRecognizers) {
  68. if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
  69. [gestureRecognizer requireGestureRecognizerToFail:_longPressGestureRecognizer];
  70. }
  71. }
  72. [self.collectionView addGestureRecognizer:_longPressGestureRecognizer];
  73. _panGestureRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRecognizerTriggerd:)];
  74. _panGestureRecognizer.delegate = self;
  75. [self.collectionView addGestureRecognizer:_panGestureRecognizer];
  76. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
  77. }
  78. - (void)removeGestureRecognizers
  79. {
  80. if (_longPressGestureRecognizer) {
  81. if (_longPressGestureRecognizer.view) {
  82. [_longPressGestureRecognizer.view removeGestureRecognizer:_longPressGestureRecognizer];
  83. }
  84. _longPressGestureRecognizer = nil;
  85. }
  86. if (_panGestureRecognizer) {
  87. if (_panGestureRecognizer.view) {
  88. [_panGestureRecognizer.view removeGestureRecognizer:_panGestureRecognizer];
  89. }
  90. _panGestureRecognizer = nil;
  91. }
  92. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
  93. }
  94. #pragma mark - getter and setter implementation
  95. - (id<LxGridViewDataSource>)dataSource
  96. {
  97. return (id<LxGridViewDataSource>)self.collectionView.dataSource;
  98. }
  99. - (id<LxGridViewDelegateFlowLayout>)delegate
  100. {
  101. return (id<LxGridViewDelegateFlowLayout>)self.collectionView.delegate;
  102. }
  103. #pragma mark - override UICollectionViewLayout methods
  104. - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
  105. {
  106. NSArray * layoutAttributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
  107. for (UICollectionViewLayoutAttributes * layoutAttributes in layoutAttributesForElementsInRect) {
  108. if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
  109. layoutAttributes.hidden = [layoutAttributes.indexPath isEqual:_movingItemIndexPath];
  110. }
  111. }
  112. return layoutAttributesForElementsInRect;
  113. }
  114. - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
  115. {
  116. UICollectionViewLayoutAttributes * layoutAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
  117. if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
  118. layoutAttributes.hidden = [layoutAttributes.indexPath isEqual:_movingItemIndexPath];
  119. }
  120. return layoutAttributes;
  121. }
  122. #pragma mark - gesture
  123. - (void)setPanGestureRecognizerEnable:(BOOL)panGestureRecognizerEnable
  124. {
  125. _panGestureRecognizer.enabled = panGestureRecognizerEnable;
  126. }
  127. - (BOOL)panGestureRecognizerEnable
  128. {
  129. return _panGestureRecognizer.enabled;
  130. }
  131. - (void)longPressGestureRecognizerTriggerd:(UILongPressGestureRecognizer *)longPress
  132. {
  133. switch (longPress.state) {
  134. case UIGestureRecognizerStatePossible:
  135. break;
  136. case UIGestureRecognizerStateBegan:
  137. {
  138. if (_displayLink == nil) {
  139. _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTriggered:)];
  140. _displayLink.frameInterval = 6;
  141. [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  142. _remainSecondsToBeginEditing = MIN_PRESS_TO_BEGIN_EDITING_DURATION;
  143. }
  144. _movingItemIndexPath = [self.collectionView indexPathForItemAtPoint:[longPress locationInView:self.collectionView]];
  145. if ([self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)] && [self.dataSource collectionView:self.collectionView canMoveItemAtIndexPath:_movingItemIndexPath] == NO) {
  146. _movingItemIndexPath = nil;
  147. return;
  148. }
  149. if ([self.delegate respondsToSelector:@selector(collectionView:layout:willBeginDraggingItemAtIndexPath:)]) {
  150. [self.delegate collectionView:self.collectionView layout:self willBeginDraggingItemAtIndexPath:_movingItemIndexPath];
  151. }
  152. UICollectionViewCell *sourceCollectionViewCell = [self.collectionView cellForItemAtIndexPath:_movingItemIndexPath];
  153. TZTestCell *sourceCell = (TZTestCell *)sourceCollectionViewCell;
  154. _beingMovedPromptView = [[UIView alloc]initWithFrame:CGRectOffset(sourceCollectionViewCell.frame, -10, -10)];
  155. _beingMovedPromptView.tz_width += 20;
  156. _beingMovedPromptView.tz_height += 20;
  157. sourceCollectionViewCell.highlighted = YES;
  158. UIView * highlightedSnapshotView = [sourceCell snapshotView];
  159. highlightedSnapshotView.frame = _beingMovedPromptView.bounds;
  160. highlightedSnapshotView.alpha = 1;
  161. sourceCollectionViewCell.highlighted = NO;
  162. UIView * snapshotView = [sourceCell snapshotView];
  163. snapshotView.frame = _beingMovedPromptView.bounds;
  164. snapshotView.alpha = 0;
  165. [_beingMovedPromptView addSubview:snapshotView];
  166. [_beingMovedPromptView addSubview:highlightedSnapshotView];
  167. [self.collectionView addSubview:_beingMovedPromptView];
  168. _sourceItemCollectionViewCellCenter = sourceCollectionViewCell.center;
  169. typeof(self) __weak weakSelf = self;
  170. [UIView animateWithDuration:0
  171. delay:0
  172. options:UIViewAnimationOptionBeginFromCurrentState
  173. animations:^{
  174. typeof(self) __strong strongSelf = weakSelf;
  175. if (strongSelf) {
  176. highlightedSnapshotView.alpha = 0;
  177. snapshotView.alpha = 1;
  178. }
  179. }
  180. completion:^(BOOL finished) {
  181. typeof(self) __strong strongSelf = weakSelf;
  182. if (strongSelf) {
  183. [highlightedSnapshotView removeFromSuperview];
  184. if ([strongSelf.delegate respondsToSelector:@selector(collectionView:layout:didBeginDraggingItemAtIndexPath:)]) {
  185. [strongSelf.delegate collectionView:strongSelf.collectionView layout:strongSelf didBeginDraggingItemAtIndexPath:_movingItemIndexPath];
  186. }
  187. }
  188. }];
  189. [self invalidateLayout];
  190. }
  191. break;
  192. case UIGestureRecognizerStateChanged:
  193. break;
  194. case UIGestureRecognizerStateEnded:
  195. case UIGestureRecognizerStateCancelled:
  196. {
  197. [_displayLink invalidate];
  198. _displayLink = nil;
  199. NSIndexPath * movingItemIndexPath = _movingItemIndexPath;
  200. if (movingItemIndexPath) {
  201. if ([self.delegate respondsToSelector:@selector(collectionView:layout:willEndDraggingItemAtIndexPath:)]) {
  202. [self.delegate collectionView:self.collectionView layout:self willEndDraggingItemAtIndexPath:movingItemIndexPath];
  203. }
  204. _movingItemIndexPath = nil;
  205. _sourceItemCollectionViewCellCenter = CGPointZero;
  206. UICollectionViewLayoutAttributes * movingItemCollectionViewLayoutAttributes = [self layoutAttributesForItemAtIndexPath:movingItemIndexPath];
  207. _longPressGestureRecognizer.enabled = NO;
  208. typeof(self) __weak weakSelf = self;
  209. [UIView animateWithDuration:0.2
  210. delay:0
  211. options:UIViewAnimationOptionBeginFromCurrentState
  212. animations:^{
  213. typeof(self) __strong strongSelf = weakSelf;
  214. if (strongSelf) {
  215. _beingMovedPromptView.center = movingItemCollectionViewLayoutAttributes.center;
  216. }
  217. }
  218. completion:^(BOOL finished) {
  219. _longPressGestureRecognizer.enabled = YES;
  220. typeof(self) __strong strongSelf = weakSelf;
  221. if (strongSelf) {
  222. [_beingMovedPromptView removeFromSuperview];
  223. _beingMovedPromptView = nil;
  224. [strongSelf invalidateLayout];
  225. if ([strongSelf.delegate respondsToSelector:@selector(collectionView:layout:didEndDraggingItemAtIndexPath:)]) {
  226. [strongSelf.delegate collectionView:strongSelf.collectionView layout:strongSelf didEndDraggingItemAtIndexPath:movingItemIndexPath];
  227. }
  228. }
  229. }];
  230. }
  231. }
  232. break;
  233. case UIGestureRecognizerStateFailed:
  234. break;
  235. default:
  236. break;
  237. }
  238. }
  239. - (void)panGestureRecognizerTriggerd:(UIPanGestureRecognizer *)pan
  240. {
  241. switch (pan.state) {
  242. case UIGestureRecognizerStatePossible:
  243. break;
  244. case UIGestureRecognizerStateBegan:
  245. case UIGestureRecognizerStateChanged:
  246. {
  247. CGPoint panTranslation = [pan translationInView:self.collectionView];
  248. _beingMovedPromptView.center = CGPointOffset(_sourceItemCollectionViewCellCenter, panTranslation.x, panTranslation.y);
  249. NSIndexPath * sourceIndexPath = _movingItemIndexPath;
  250. NSIndexPath * destinationIndexPath = [self.collectionView indexPathForItemAtPoint:_beingMovedPromptView.center];
  251. if ((destinationIndexPath == nil) || [destinationIndexPath isEqual:sourceIndexPath]) {
  252. return;
  253. }
  254. if ([self.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:canMoveToIndexPath:)] && [self.dataSource collectionView:self.collectionView itemAtIndexPath:sourceIndexPath canMoveToIndexPath:destinationIndexPath] == NO) {
  255. return;
  256. }
  257. if ([self.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:willMoveToIndexPath:)]) {
  258. [self.dataSource collectionView:self.collectionView itemAtIndexPath:sourceIndexPath willMoveToIndexPath:destinationIndexPath];
  259. }
  260. _movingItemIndexPath = destinationIndexPath;
  261. typeof(self) __weak weakSelf = self;
  262. [self.collectionView performBatchUpdates:^{
  263. typeof(self) __strong strongSelf = weakSelf;
  264. if (strongSelf) {
  265. if (sourceIndexPath && destinationIndexPath) {
  266. [strongSelf.collectionView deleteItemsAtIndexPaths:@[sourceIndexPath]];
  267. [strongSelf.collectionView insertItemsAtIndexPaths:@[destinationIndexPath]];
  268. }
  269. }
  270. } completion:^(BOOL finished) {
  271. typeof(self) __strong strongSelf = weakSelf;
  272. if ([strongSelf.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)]) {
  273. [strongSelf.dataSource collectionView:strongSelf.collectionView itemAtIndexPath:sourceIndexPath didMoveToIndexPath:destinationIndexPath];
  274. }
  275. }];
  276. }
  277. break;
  278. case UIGestureRecognizerStateEnded:
  279. break;
  280. case UIGestureRecognizerStateCancelled:
  281. break;
  282. case UIGestureRecognizerStateFailed:
  283. break;
  284. default:
  285. break;
  286. }
  287. }
  288. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
  289. {
  290. if ([_panGestureRecognizer isEqual:gestureRecognizer]) {
  291. return _movingItemIndexPath != nil;
  292. }
  293. return YES;
  294. }
  295. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
  296. {
  297. // only _longPressGestureRecognizer and _panGestureRecognizer can recognize simultaneously
  298. if ([_longPressGestureRecognizer isEqual:gestureRecognizer]) {
  299. return [_panGestureRecognizer isEqual:otherGestureRecognizer];
  300. }
  301. if ([_panGestureRecognizer isEqual:gestureRecognizer]) {
  302. return [_longPressGestureRecognizer isEqual:otherGestureRecognizer];
  303. }
  304. return NO;
  305. }
  306. #pragma mark - displayLink
  307. - (void)displayLinkTriggered:(CADisplayLink *)displayLink
  308. {
  309. if (_remainSecondsToBeginEditing <= 0) {
  310. [_displayLink invalidate];
  311. _displayLink = nil;
  312. }
  313. _remainSecondsToBeginEditing = _remainSecondsToBeginEditing - 0.1;
  314. }
  315. #pragma mark - KVO and notification
  316. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  317. {
  318. if ([keyPath isEqualToString:@stringify(collectionView)]) {
  319. if (self.collectionView) {
  320. [self addGestureRecognizers];
  321. }
  322. else {
  323. [self removeGestureRecognizers];
  324. }
  325. }
  326. }
  327. - (void)applicationWillResignActive:(NSNotification *)notificaiton
  328. {
  329. _panGestureRecognizer.enabled = NO;
  330. _panGestureRecognizer.enabled = YES;
  331. }
  332. @end