系统的UICollectionView和UITableView有提供在编辑态拖拽排序的功能,但是需要指定的EditStyle,而且还只能拖动排序按钮区域,大部分时候效果很难满足业务需求,因此我们需要定制可拖拽Cell的SDMovableCellCollectionView,拖拽效果类似于系统Reminder。文章最后附有Demo的Git地址,可查看完整代码,如有疑问和不妥,评论区交流。
实现思路
- 首先要创建CollectionView,继承UICollectionView,声明移动事件触发代理方法,添加长按手势;
@protocol SDMovableCellCollectionViewDelegate <UICollectionViewDelegate>
@optional
/**
* The cell that will start moving the indexPath location
* 长按拖拽cell将要开始移动
*/
- (void)collectionView:(SDMovableCellCollectionView *)collectionView willMoveCellAtIndexPath:(NSIndexPath *)indexPath;
/**
* Move cell `fromIndexPath` to `toIndexPath` completed
* 移动cell换了位置下标
*/
- (void)collectionView:(SDMovableCellCollectionView *)collectionView didMoveCellFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath;
/**
* Move cell ended
* 移动cell结束
*/
- (void)collectionView:(SDMovableCellCollectionView *)collectionView endMoveCellAtIndexPath:(NSIndexPath *)indexPath;
/**
* The user tries to move a cell that is not allowed to move. You can make some prompts to inform the user.
* 尝试长按拖拽不能移动的cell 这个代理方法可以加个toast或者其他处理 与设置`canHintWhenCannotMove`不冲突
*/
- (void)collectionView:(SDMovableCellCollectionView *)collectionView tryMoveUnmovableCellAtIndexPath:(NSIndexPath *)indexPath;
/**
* Customize the screenshot style of the movable cell
* 自定义移动的cell截图的样式 加阴影啥的
*/
- (void)collectionView:(SDMovableCellCollectionView *)collectionView customizeMovalbeCell:(UIImageView *)movableCellsnapshot;
/**
* Custom start moving cell animation
* 自定义cell拖拽移动的动画
*/
- (void)collectionView:(SDMovableCellCollectionView *)collectionView customizeStartMovingAnimation:(UIImageView *)movableCellsnapshot fingerPoint:(CGPoint)fingerPoint;
@end
- (void)addGesture {
_longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(processGesture:)];
_longPressGesture.minimumPressDuration = 0.5f;
[self addGestureRecognizer:_longPressGesture];
}
- 然后手势开始时,找到当前手势坐标所在的startIndexPath,对cell生成snapshot,并隐藏此cell,接下来手势改变时都是改变snapshot在CollectionView中的位置;
- (void)gestureBegan:(UILongPressGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:gesture.view];
NSIndexPath *selectedIndexPath = [self indexPathForItemAtPoint:point];
if (!selectedIndexPath) {
return;
}
UICollectionViewCell *cell = [self cellForItemAtIndexPath:selectedIndexPath];
cell.alpha = 1.0;
if (self.dataSource && [self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)]) {
if (![self.dataSource collectionView:self canMoveItemAtIndexPath:selectedIndexPath]) {
//It is not allowed to move the cell, then shake it to prompt the user.
if (self.canHintWhenCannotMove) {
CAKeyframeAnimation *shakeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"];
shakeAnimation.duration = 0.25;
shakeAnimation.values = @[@(-20), @(20), @(-10), @(10), @(0)];
[cell.layer addAnimation:shakeAnimation forKey:@"shake"];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:tryMoveUnmovableCellAtIndexPath:)]) {
[self.delegate collectionView:self tryMoveUnmovableCellAtIndexPath:selectedIndexPath];
}
UICollectionViewCell *cell = [self cellForItemAtIndexPath:selectedIndexPath];
cell.alpha = 0.7;
return;
}
}
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:willMoveCellAtIndexPath:)]) {
[self.delegate collectionView:self willMoveCellAtIndexPath:selectedIndexPath];
}
if (_canEdgeScroll) {
[self startEdgeScroll];
}
//Get a data source every time you move
if (self.dataSource && [self.dataSource respondsToSelector:@selector(dataSourceArrayInCollectionView:)]) {
_tempDataSource = [self.dataSource dataSourceArrayInCollectionView:self];
}
_selectedIndexPath = selectedIndexPath;
_startIndexPath = selectedIndexPath;
if (self.canFeedback) {
[self.generator prepare];
[self.generator impactOccurred];
}
if (self.dataSource && [self.dataSource respondsToSelector:@selector(snapshotViewWithCell:)]) {
UIView *snapView = [self.dataSource snapshotViewWithCell:cell];
_snapshot = [self snapshotViewWithInputView:snapView];
} else {
_snapshot = [self snapshotViewWithInputView:cell];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:customizeMovalbeCell:)]) {
[self.delegate collectionView:self customizeMovalbeCell:_snapshot];
} else {
_snapshot.layer.shadowColor = [UIColor grayColor].CGColor;
_snapshot.layer.masksToBounds = NO;
_snapshot.layer.cornerRadius = 0;
_snapshot.layer.shadowOffset = CGSizeMake(-5, 0);
_snapshot.layer.shadowOpacity = 0.4;
_snapshot.layer.shadowRadius = 5;
}
// _snapshot.frame = CGRectMake((cell.frame.size.width - _snapshot.frame.size.width)/2.0f, cell.frame.origin.y + (cell.frame.size.height - _snapshot.frame.size.height)/2.0, _snapshot.frame.size.width, _snapshot.frame.size.height);
_snapshot.frame = cell.frame;
[self addSubview:_snapshot];
// 记录手势中心偏移
self.gesturePointOffsetX = point.x - _snapshot.center.x;
self.gesturePointOffsetY = point.y - _snapshot.center.y;
cell.hidden = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:customizeStartMovingAnimation:fingerPoint:)]) {
[self.delegate collectionView:self customizeStartMovingAnimation:_snapshot fingerPoint:point];
} else {
[UIView animateWithDuration:0.3 animations:^{
self.snapshot.transform = CGAffineTransformScale(self.snapshot.transform, 1.1, 1.1);
}];
}
}
- 手势改变时,snapshot的center跟随手势坐标变化,因为手势触发位置不一定是cell的center,所以要计算中心偏移,获取手势坐标所在的indexPath,并和startIndexPath交换位置;
- (void)gestureChanged:(UILongPressGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:gesture.view];
point.x -= self.gesturePointOffsetX;
point.y -= self.gesturePointOffsetY;
point = CGPointMake([self limitSnapshotCenterX:point.x], [self limitSnapshotCenterY:point.y]);
//point = CGPointMake(_snapshot.center.x, [self limitSnapshotCenterY:point.y]);
//Let the screenshot follow the gesture
_snapshot.center = point;
NSIndexPath *currentIndexPath = [self indexPathForItemAtPoint:point];
if (!currentIndexPath) {
return;
}
UICollectionViewCell *selectedCell = [self cellForItemAtIndexPath:_selectedIndexPath];
selectedCell.hidden = YES;
if (self.dataSource && [self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)]) {
if (![self.dataSource collectionView:self canMoveItemAtIndexPath:currentIndexPath]) {
return;
}
}
if (currentIndexPath && ![_selectedIndexPath isEqual:currentIndexPath]) {
//Exchange data source and cell
[self updateDataSourceAndCellFromIndexPath:_selectedIndexPath toIndexPath:currentIndexPath];
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:didMoveCellFromIndexPath:toIndexPath:)]) {
[self.delegate collectionView:self didMoveCellFromIndexPath:_selectedIndexPath toIndexPath:currentIndexPath];
}
_selectedIndexPath = currentIndexPath;
}
}
- 手势结束时,cell显示,snapshot从父视图移除;
- (void)gestureEndedOrCancelled:(UILongPressGestureRecognizer *)gesture {
if (_canEdgeScroll) {
[self stopEdgeScroll];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:endMoveCellAtIndexPath:)]) {
[self.delegate collectionView:self endMoveCellAtIndexPath:_selectedIndexPath];
}
UICollectionViewCell *cell = [self cellForItemAtIndexPath:_selectedIndexPath];
cell.alpha = 0.7;
[UIView animateWithDuration:0.3 animations:^{
self.snapshot.transform = CGAffineTransformIdentity;
self.snapshot.frame = cell.frame;
// self.snapshot.frame = CGRectMake((cell.frame.size.width - self.snapshot.frame.size.width)/2.0f, cell.frame.origin.y + (cell.frame.size.height - self.snapshot.frame.size.height)/2.0, self.snapshot.frame.size.width, self.snapshot.frame.size.height);
} completion:^(BOOL finished) {
cell.hidden = NO;
[self.snapshot removeFromSuperview];
self.snapshot = nil;
}];
// 延时后更新整个数据源
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self reloadData];
});
}
- 考虑拖拽边界情况,内容过多可滑动时,拖拽到边缘,调整contentOffset;
#pragma mark EdgeScroll
- (void)startEdgeScroll {
_edgeScrollLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(processEdgeScroll)];
[_edgeScrollLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)processEdgeScroll {
CGFloat minOffsetY = self.contentOffset.y + _edgeScrollTriggerRange;
CGFloat maxOffsetY = self.contentOffset.y + self.bounds.size.height - _edgeScrollTriggerRange;
CGPoint touchPoint = _snapshot.center;
if (touchPoint.y < minOffsetY) {
//Cell is moving up
CGFloat moveDistance = (minOffsetY - touchPoint.y)/_edgeScrollTriggerRange*_maxScrollSpeedPerFrame;
_currentScrollSpeedPerFrame = moveDistance;
self.contentOffset = CGPointMake(self.contentOffset.x, [self limitContentOffsetY:self.contentOffset.y - moveDistance]);
} else if (touchPoint.y > maxOffsetY) {
//Cell is moving down
CGFloat moveDistance = (touchPoint.y - maxOffsetY)/_edgeScrollTriggerRange*_maxScrollSpeedPerFrame;
_currentScrollSpeedPerFrame = moveDistance;
self.contentOffset = CGPointMake(self.contentOffset.x, [self limitContentOffsetY:self.contentOffset.y + moveDistance]);
}
[self setNeedsLayout];
[self layoutIfNeeded];
[self gestureChanged:_longPressGesture];
}
- (void)stopEdgeScroll {
_currentScrollSpeedPerFrame = 0;
if (_edgeScrollLink) {
[_edgeScrollLink invalidate];
_edgeScrollLink = nil;
}
}