1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-12 09:29:55 +01:00
Telegram/Telegraph/TGDraggableCollectionView.m

499 lines
18 KiB
Mathematica
Raw Normal View History

2015-10-01 18:19:52 +02:00
#import "TGDraggableCollectionView.h"
#import "TGDraggableCollectionViewFlowLayout.h"
#import "TGImageUtils.h"
NSString * const TGDraggableCollectionViewLayoutKey = @"collectionViewLayout";
const UIEdgeInsets TGDraggableDefaultScrollingTriggerEdgeInsets = { 60, 60, 60, 60 };
const CGFloat TGDraggableDefaultScrollingSpeed = 300.0f;
typedef enum
{
TGDraggableCollectionScrollingDirectionUnknown,
TGDraggableCollectionScrollingDirectionUp,
TGDraggableCollectionScrollingDirectionLeft,
TGDraggableCollectionScrollingDirectionRight,
TGDraggableCollectionScrollingDirectionDown
} TGDraggableCollectionScrollingDirection;
@interface TGDraggableCollectionView () <UIGestureRecognizerDelegate>
{
UIView *_draggedView;
UIView *_insideSnapshotView;
UIView *_outsideSnapshotView;
NSIndexPath *_lastIndexPath;
UIPanGestureRecognizer *_panGestureRecognizer;
UILongPressGestureRecognizer *_pressGestureRecognizer;
CADisplayLink *_scrollingDisplayLink;
TGDraggableCollectionScrollingDirection _scrollingDirection;
}
@end
@implementation TGDraggableCollectionView
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
NSAssert([layout isKindOfClass:[TGDraggableCollectionViewFlowLayout class]], @"collectionViewLayout must be an instance of TGDraggableCollectionViewFlowLayout");
self = [super initWithFrame:frame collectionViewLayout:layout];
if (self != nil)
{
_scrollingTriggerEdgeInsets = TGDraggableDefaultScrollingTriggerEdgeInsets;
_scrollingSpeed = TGDraggableDefaultScrollingSpeed;
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDragPan:)];
_panGestureRecognizer.delegate = self;
[self addGestureRecognizer:_panGestureRecognizer];
_pressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleDragPress:)];
_pressGestureRecognizer.minimumPressDuration = 0.25f;
_pressGestureRecognizer.delegate = self;
for (UIGestureRecognizer *gestureRecognizer in self.gestureRecognizers)
{
if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
[gestureRecognizer requireGestureRecognizerToFail:_pressGestureRecognizer];
}
[self addGestureRecognizer:_pressGestureRecognizer];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleApplicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationWillResignActiveNotification
object:nil];
}
- (void)handleApplicationWillResignActive:(NSNotification *)__unused notification
{
_pressGestureRecognizer.enabled = false;
_pressGestureRecognizer.enabled = true;
}
- (void)setDraggable:(bool)draggable
{
_draggable = draggable;
_pressGestureRecognizer.enabled = draggable;
_panGestureRecognizer.enabled = draggable;
}
#pragma mark - Items
- (TGDraggableCollectionViewFlowLayout *)draggableLayout
{
return (TGDraggableCollectionViewFlowLayout *)self.draggableLayout;
}
- (NSIndexPath *)indexPathForItemNearPoint:(CGPoint)point
{
NSIndexPath *destinationIndexPath = self.draggableLayout.destinationIndexPath;
self.draggableLayout.destinationIndexPath = nil;
NSArray *layoutAttributes = [self.draggableLayout layoutAttributesForElementsInRect:self.bounds];
self.draggableLayout.destinationIndexPath = destinationIndexPath;
NSIndexPath *nearestItemIndexPath = nil;
CGFloat minDistance = FLT_MAX;
for (UICollectionViewLayoutAttributes *attributes in layoutAttributes)
{
CGFloat deltaX = attributes.center.x - point.x;
CGFloat deltaY = attributes.center.y - point.y;
CGFloat distance = (CGFloat)sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < minDistance)
{
minDistance = distance;
nearestItemIndexPath = attributes.indexPath;
}
}
return nearestItemIndexPath;
}
#pragma mark - Dragged View
- (UIView *)_draggedViewSuperview
{
return (self.draggedViewSuperview != nil) ? self.draggedViewSuperview : self;
}
- (UIView *)_prepareDraggedViewForCell:(UICollectionViewCell *)cell
{
UIView *view = [[UIView alloc] initWithFrame:[self convertRect:cell.frame toView:[self _draggedViewSuperview]]];
view.layer.rasterizationScale = TGScreenScaling();
_outsideSnapshotView = [cell snapshotViewAfterScreenUpdates:false];
[view addSubview:_outsideSnapshotView];
_insideSnapshotView = [cell snapshotViewAfterScreenUpdates:false];
_outsideSnapshotView.layer.shadowOffset = CGSizeZero;
_outsideSnapshotView.layer.shadowColor = [UIColor blackColor].CGColor;
_outsideSnapshotView.layer.shadowRadius = 3.0f;
_outsideSnapshotView.layer.shadowOpacity = 0.87f;
_outsideSnapshotView.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;
return view;
}
- (void)_performDraggedViewHighlightTransitionWithDuration:(CGFloat)duration completion:(void (^)(void))completion
{
[self addSubview:_insideSnapshotView];
_insideSnapshotView.center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]];
_draggedView.layer.shouldRasterize = true;
_draggedView.alpha = 0.0f;
[UIView animateWithDuration:duration delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^
{
_draggedView.alpha = 1.0f;
_draggedView.transform = CGAffineTransformMakeScale(1.12f, 1.12f);
_insideSnapshotView.transform = _draggedView.transform;
} completion:^(BOOL finished)
{
if (finished)
{
_insideSnapshotView.hidden = true;
if (completion != nil)
completion();
}
}];
}
- (void)_performDraggedViewUnhighlightTransitionWithDuration:(CGFloat)duration targetCellCenter:(CGPoint)targetCellCenter completion:(void (^)(void))completion
{
_insideSnapshotView.center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]];
_insideSnapshotView.hidden = false;
[UIView animateWithDuration:duration delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^
{
_draggedView.alpha = 0.0f;
_draggedView.transform = CGAffineTransformIdentity;
_insideSnapshotView.transform = _draggedView.transform;
_draggedView.center = [self convertPoint:targetCellCenter toView:[self _draggedViewSuperview]];
_insideSnapshotView.center = targetCellCenter;
} completion:^(__unused BOOL finished)
{
if (completion != nil)
completion();
}];
}
- (void)_performDraggedViewDropToIndexPath:(NSIndexPath *)destinationIndexPath completion:(void (^)(void))completion
{
UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:destinationIndexPath];
[self _performDraggedViewUnhighlightTransitionWithDuration:0.25f targetCellCenter:layoutAttributes.center completion:^
{
[_draggedView removeFromSuperview];
_draggedView = nil;
[_outsideSnapshotView removeFromSuperview];
_outsideSnapshotView = nil;
[_insideSnapshotView removeFromSuperview];
_insideSnapshotView = nil;
if (completion != nil)
completion();
}];
}
- (void)updateDestinationIndexPath:(NSIndexPath *)indexPath
{
if (indexPath == nil || [_lastIndexPath isEqual:indexPath])
return;
_lastIndexPath = indexPath;
id<TGDraggableCollectionViewDataSource> dataSource = (id<TGDraggableCollectionViewDataSource>)self.dataSource;
if ([dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:toIndexPath:)] &&
![dataSource collectionView:self canMoveItemAtIndexPath:self.draggableLayout.sourceIndexPath toIndexPath:indexPath])
{
return;
}
[self performBatchUpdates:^
{
self.draggableLayout.hiddenIndexPath = indexPath;
self.draggableLayout.destinationIndexPath = indexPath;
} completion:nil];
}
#pragma mark - Gesture Handling
- (void)handleDragPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
{
CGPoint location = [gestureRecognizer locationInView:self];
NSIndexPath *indexPath = [self indexPathForItemAtPoint:location];
if (indexPath == nil)
return;
id<TGDraggableCollectionViewDataSource> dataSource = (id<TGDraggableCollectionViewDataSource>)self.dataSource;
if ([dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)] &&
![dataSource collectionView:self canMoveItemAtIndexPath:indexPath])
{
return;
}
[_draggedView removeFromSuperview];
_draggedView = [self _prepareDraggedViewForCell:[self cellForItemAtIndexPath:indexPath]];
[[self _draggedViewSuperview] addSubview:_draggedView];
[self _performDraggedViewHighlightTransitionWithDuration:0.25f completion:nil];
_lastIndexPath = indexPath;
self.draggableLayout.sourceIndexPath = indexPath;
self.draggableLayout.destinationIndexPath = indexPath;
self.draggableLayout.hiddenIndexPath = indexPath;
[self.draggableLayout invalidateLayout];
}
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
{
if (self.draggableLayout.sourceIndexPath == nil)
return;
NSIndexPath *sourceIndexPath = self.draggableLayout.sourceIndexPath;
NSIndexPath *destinationIndexPath = self.draggableLayout.destinationIndexPath;
id<TGDraggableCollectionViewDataSource> dataSource = (id<TGDraggableCollectionViewDataSource>)self.dataSource;
[dataSource collectionView:self itemAtIndexPath:sourceIndexPath willMoveToIndexPath:destinationIndexPath];
[self performBatchUpdates:^
{
[self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
self.draggableLayout.sourceIndexPath = nil;
self.draggableLayout.destinationIndexPath = nil;
} completion:nil];
_pressGestureRecognizer.enabled = false;
[self _performDraggedViewDropToIndexPath:destinationIndexPath completion:^
{
_pressGestureRecognizer.enabled = true;
if ([dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)])
[dataSource collectionView:self itemAtIndexPath:sourceIndexPath didMoveToIndexPath:destinationIndexPath];
self.draggableLayout.hiddenIndexPath = nil;
[self.draggableLayout invalidateLayout];
}];
_lastIndexPath = nil;
[self invalidateScrolling];
}
break;
default:
break;
}
}
- (void)handleDragPan:(UIPanGestureRecognizer *)gestureRecognizer
{
if (self.draggableLayout.sourceIndexPath == nil)
return;
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
{
CGPoint translation = [gestureRecognizer translationInView:self];
[gestureRecognizer setTranslation:CGPointZero inView:self];
_draggedView.center = CGPointMake(_draggedView.center.x + translation.x,
_draggedView.center.y + translation.y);
CGPoint center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]];
_insideSnapshotView.center = center;
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.draggableLayout;
switch (layout.scrollDirection)
{
case UICollectionViewScrollDirectionHorizontal:
{
if (center.x < (CGRectGetMinX(self.bounds) + self.scrollingTriggerEdgeInsets.left))
{
[self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionLeft];
}
else
{
if (center.x > (CGRectGetMaxX(self.bounds) - self.scrollingTriggerEdgeInsets.right))
[self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionRight];
else
[self invalidateScrolling];
}
}
break;
case UICollectionViewScrollDirectionVertical:
{
if (center.y < (CGRectGetMinY(self.bounds) + self.scrollingTriggerEdgeInsets.top))
{
[self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionUp];
}
else
{
if (center.y > (CGRectGetMaxY(self.bounds) - self.scrollingTriggerEdgeInsets.bottom))
[self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionDown];
else
[self invalidateScrolling];
}
}
break;
default:
break;
}
}
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
{
[self invalidateScrolling];
}
break;
default:
break;
}
if (_scrollingDirection != TGDraggableCollectionScrollingDirectionUnknown)
return;
CGPoint location = [gestureRecognizer locationInView:self];
NSIndexPath *indexPath = [self indexPathForItemNearPoint:location];
[self updateDestinationIndexPath:indexPath];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == _panGestureRecognizer)
return (self.draggableLayout.sourceIndexPath != nil);
return true;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if (gestureRecognizer == _pressGestureRecognizer && otherGestureRecognizer == _panGestureRecognizer)
return true;
else if (gestureRecognizer == _panGestureRecognizer && otherGestureRecognizer == _pressGestureRecognizer)
return true;
return false;
}
#pragma mark - Scrolling
- (void)setupScrollForDraggingInDirection:(TGDraggableCollectionScrollingDirection)direction
{
if (_scrollingDirection == direction)
return;
[self invalidateScrolling];
_scrollingDirection = direction;
_scrollingDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDragScroll:)];
[_scrollingDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)handleDragScroll:(CADisplayLink *)displayLink
{
if (_scrollingDirection == TGDraggableCollectionScrollingDirectionUnknown)
return;
CGSize frameSize = self.bounds.size;
CGSize contentSize = self.contentSize;
CGPoint contentOffset = self.contentOffset;
UIEdgeInsets contentInset = self.contentInset;
CGFloat distance = (CGFloat)rint(self.scrollingSpeed * displayLink.duration);
CGPoint translation = CGPointZero;
switch (_scrollingDirection)
{
case TGDraggableCollectionScrollingDirectionUp:
{
distance = -distance;
if (contentOffset.y + distance <= -contentInset.top)
distance = -contentOffset.y - contentInset.top;
translation = CGPointMake(0, distance);
}
break;
case TGDraggableCollectionScrollingDirectionDown:
{
CGFloat maxY = contentSize.height - frameSize.height + contentInset.bottom;
if ((contentOffset.y + distance) >= maxY)
distance = maxY - contentOffset.y;
translation = CGPointMake(0, distance);
}
break;
case TGDraggableCollectionScrollingDirectionLeft:
{
distance = -distance;
if (contentOffset.x + distance <= -contentInset.left)
distance = -contentOffset.x - contentInset.left;
translation = CGPointMake(distance, 0);
}
break;
case TGDraggableCollectionScrollingDirectionRight:
{
CGFloat maxX = contentSize.width - frameSize.width + contentInset.right;
if ((contentOffset.x + distance) >= maxX)
distance = maxX - contentOffset.x;
translation = CGPointMake(distance, 0);
}
break;
default:
break;
}
self.contentOffset = CGPointMake(contentOffset.x + translation.x,
contentOffset.y + translation.y);
NSIndexPath *indexPath = [self indexPathForItemNearPoint:_draggedView.center];
[self updateDestinationIndexPath:indexPath];
}
- (void)invalidateScrolling
{
[_scrollingDisplayLink invalidate];
_scrollingDisplayLink = nil;
_scrollingDirection = TGDraggableCollectionScrollingDirectionUnknown;
}
@end