首先,先祝博友们新年快乐!鸡年大吉! 在写这篇博文的时候,已经是1月25日,最后一天上班了,闲来无事,写下之前封装的拖动效果,希望能帮助有需要的博友们~
先上效果图。
1 、 全部 Cell 参与拖动效果。
2 、 禁止部分的 Cell 参与拖动。 (第一行的3个 cell 禁止)
3 、 跨 Cell 拖动。 (将一个 Cell 中的 UICollectionViewCell 拖动到另一个 Cell)
代码及类的简单说明。
1、 第一种拖动重排的方式
将Demo中的 UICollectionView+TLCellRearrange.h 拖到项目中,这是对 UICollectionView 做的一个扩展功能。
接口文件展示
#import <UIKit/UIKit.h>
@protocol UICollectionViewCellRearrangeDelegate <NSObject>
@optional
- (BOOL)collectionView:(nonnull UICollectionView *)collectionView
shouldMoveCell:(nonnull UICollectionViewCell *)cell
atIndexPath:(nonnull NSIndexPath *)indexPath;
- (void)collectionView:(nonnull UICollectionView *)collectionView
putDownCell:(nonnull UICollectionViewCell *)cell
atIndexPath:(nonnull NSIndexPath *)indexPath;
- (BOOL)collectionView:(nonnull UICollectionView *)collectionView
shouldMoveCell:(nonnull UICollectionViewCell *)cell
fromIndexPath:(nonnull NSIndexPath *)fromIndexPath
toIndexPath:(nonnull NSIndexPath *)toIndexPath;
- (void)collectionView:(nonnull UICollectionView *)collectionView
didMoveCell:(nonnull UICollectionViewCell *)cell
fromIndexPath:(nonnull NSIndexPath *)fromIndexPath
toIndexPath:(nonnull NSIndexPath *)toIndexPath;
@end
@interface UICollectionView (TLCellRearrange)
@property (nonatomic, weak) id<UICollectionViewCellRearrangeDelegate> _Nullable rearrangeDelegate;
@property (nonatomic, assign) BOOL enableRearrangement;
/**
* 长按cell接触边缘时collectionView自动滚动的速率,每1/60秒移动的距离
*/
@property (nonatomic, assign) CGFloat autoScrollSpeed;
/**
* 长按放大倍数
* 默认为1.2
*/
@property (nonatomic, assign) CGFloat longPressMagnificationFactor;
@end
然后在使用的 VC 中, 创建 UICollectionView ,初始化之后,如下设置,加入扩展的委托 UICollectionViewCellRearrangeDelegate
- (void)setupUI {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
[layout setScrollDirection:UICollectionViewScrollDirectionVertical];
[_collectionView setCollectionViewLayout:layout];
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.showsVerticalScrollIndicator = NO;
_collectionView.alwaysBounceVertical = YES;
[_collectionView registerClass:[WBAreaCollectionViewCell class] forCellWithReuseIdentifier:[WBAreaCollectionViewCell reuseIdentifier]];
_collectionView.rearrangeDelegate = self;
_collectionView.enableRearrangement = YES;
_collectionView.autoScrollSpeed = 3;
_collectionView.longPressMagnificationFactor = 1.2;
[self updateContent];
}
这样就能实现拖动的重排效果了。 然后拖动后的回调在对应的委托方法里
#pragma mark - UICollectionViewCellRearrangeDelegate methods
- (BOOL)collectionView:(UICollectionView *)collectionView shouldMoveCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0 || indexPath.row == 1 || indexPath.row == 2) {
// 如果不需要参与 长按拖动 和 重排效果 的cell, 在这里判断返回NO 即可。
return NO;
}
NSLog(@"开始位置:%ld", indexPath.row);
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView putDownCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
// 最终停留在的
NSLog(@"最终到:%ld", indexPath.row);
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldMoveCell:(UICollectionViewCell *)cell fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
id tempObject = self.areaModels[fromIndexPath.row];
[self.areaModels removeObjectAtIndex:fromIndexPath.row];
[self.areaModels insertObject:tempObject atIndex:toIndexPath.row];
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView didMoveCell:(UICollectionViewCell *)cell fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
// 途径的位置
NSLog(@"从:%ld, 到x:%ld", fromIndexPath.row, toIndexPath.row);
}
2 、 禁止部分 Cell 参与重排的方式,只需要在对应的委托方法里设置返回 NO 即可, 如下 不想要 row = 0, 1, 2 的 Cell 参与重排。
- (BOOL)collectionView:(UICollectionView *)collectionView shouldMoveCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0 || indexPath.row == 1 || indexPath.row == 2) {
// 如果不需要参与 长按拖动 和 重排效果 的cell, 在这里判断返回NO 即可。
return NO;
}
NSLog(@"开始位置:%ld", indexPath.row);
return YES;
}
3 、 跨 Cell 拖动重排。 这里主要讲下实现的思路,有了思路才能更好的自由扩展成自己的需要。
我们先看下最内部 设备 Cell 的接口文件。
#import <UIKit/UIKit.h>
@class WBDeviceModel;
@interface WBDeviceCollectionViewCell : UICollectionViewCell
+ (NSString *)reuseIdentifier;
- (void)setupWithModel:(WBDeviceModel *)deviceModel;
@property (nonatomic, assign) BOOL isDisableLongPress; // 是否禁用自定义长按手势
@property (nonatomic, assign) BOOL isSelect;
@property (nonatomic, copy) void (^longPressDragBeganBlock)(CGPoint centerPoint); // 长按开始
@property (nonatomic, copy) void (^longPressDragChangeBlock)(CGPoint centerPoint); // 长按拖动过程
@property (nonatomic, copy) void (^longPressDragEndedBlock)(WBDeviceModel *currentDeviceModel, CGPoint centerPoint); // 长按结束
@end
红色部分是我们自己提供出来的 长按手势 的过程接口,开始,过程和结束都是长按手势的一些基本阶段,我们可以按照自己的需要修改接口。
内部的实现,在长按的开始,将长按的 Cell 绘制成快照 self.snapshotView ,并且放大1.1倍 ,然后计算当前处于 Window 上的Frame,并且添加在 Window 上。
然后在拖动过程中,实时的计算基于 window 的位置,并且更新 Frame,并且实时回调最新的 Frame ( 如果你需要这个需求的话) 。
在拖动结束时,可以回调结束时的Frame ,以及你选中的数据。
因为这里的 UI 是有两层 Cell 之后才回调到 VC 的, WBDeviceCollectionViewCell 的数据是回调到了 WBAreaDetaliCollectionViewCell 中, WBAreaDetaliCollectionViewCell 再回调到 WBAreaDetailVC 。
#pragma mark - UIGestureRecognizer
- (void)selfContentViewLongPressed:(UILongPressGestureRecognizer *)sender {
UIView *view = (UIView *)sender.view;
UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
if (sender.state == UIGestureRecognizerStateBegan) {
_startPoint = [sender locationInView:sender.view];
_originPoint = view.center;
// iphone7及以上 或者 模拟器,自己绘制快照
if ([[GetDeviceInfo() getCurrentDeviceModel] isEqualToString:@"iPhone7"] || [[GetDeviceInfo() getCurrentDeviceModel] isEqualToString:@"iPhone7Plus"] || [[GetDeviceInfo() getCurrentDeviceModel] isEqualToString:@"iPhoneSimulator"] ) {
self.snapshotView = [[UIImageView alloc] initWithImage:[UIImage imageFromView:view]];
} else {
self.snapshotView = [view snapshotViewAfterScreenUpdates:NO];
}
// 添加在 window 上
[window addSubview:self.snapshotView];
CGRect toWindowFrame = [view convertRect:view.bounds toView:window];
self.snapshotView.frame = toWindowFrame;
[UIView animateWithDuration:Duration animations:^{
// 放大1.1倍
self.snapshotView.transform = CGAffineTransformMakeScale(1.1, 1.1);
self.snapshotView.alpha = 0.9;
}];
BLOCK_SAFE_CALLS(self.longPressDragBeganBlock, [self fetchCenterPointWithRect:toWindowFrame]);
} else if (sender.state == UIGestureRecognizerStateChanged) {
CGPoint newPoint = [sender locationInView:sender.view];
CGFloat deltaX = newPoint.x - _startPoint.x;
CGFloat deltaY = newPoint.y - _startPoint.y;
view.center = CGPointMake(view.center.x + deltaX, view.center.y + deltaY);
CGRect toWindowFrame = [view convertRect:view.bounds toView:window];
self.snapshotView.frame = toWindowFrame;
BLOCK_SAFE_CALLS(self.longPressDragChangeBlock, [self fetchCenterPointWithRect:toWindowFrame]);
} else if (sender.state == UIGestureRecognizerStateEnded) {
[UIView animateWithDuration:Duration animations:^{
self.snapshotView.transform = CGAffineTransformIdentity;
self.snapshotView.alpha = 1.0;
}];
[self.snapshotView removeFromSuperview];
self.snapshotView = nil;
CGRect toWindowFrame = [view convertRect:view.bounds toView:window];
BLOCK_SAFE_CALLS(self.longPressDragEndedBlock, self.currentDeviceModel, [self fetchCenterPointWithRect:toWindowFrame]);
}
}
回调到 VC 之后,我们要做一种状态, 就是拖动到某个 Cell 上的响应状态, Demo 中是将背景色 更改为 浅白色 。
我们 Cell 返回的 centerPoint
是自身中心点基于 window 的位置,然后就有下面这个方法, 利用这个返回的 centerPoint判断一个点是否在某一个区域内,即 是否在某个 Cell 上, 用中心点类判断,是为了确定这个Cell拖动的某个Cell上的唯一性,故不能用Cell的其他四点作为判断点。
// 选中项更新高亮
- (void)judgeDragPositionWithCenterPoint:(CGPoint)centerPoint indexPath:(NSIndexPath *)indexPath {
NSArray<NSIndexPath *> *indexPaths = [self.collectionView indexPathsForVisibleItems];
UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (indexPath.row != obj.row) {
WBAreaDetaliCollectionViewCell *cell = (WBAreaDetaliCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:obj];
CGRect toWindowFrame = [cell convertRect:cell.bounds toView:window];
cell.isDragCurrent = CGRectContainsPoint(toWindowFrame, centerPoint) ? YES : NO;
}
}];
}
VC 上,Cell 的回调情况如下。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
WEAK_SELF();
WBAreaDetaliCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[WBAreaDetaliCollectionViewCell reuseIdentifier] forIndexPath:indexPath];
// 初始化重用处理 ***
[cell setupWithModel:self.areasDetail[indexPath.row]];
// 内部collectionView的offset
[cell setupCollectionViewOffsetWitnPointValue:self.pointsValue[INT_TO_STRING(indexPath.row)]];
// ***
// 长按拖动效果 ***
cell.isDragCurrent = NO;
[cell setPointChangeBlock:^(NSValue *pointValue) {
[weakSelf.pointsValue setObject:pointValue forKey:INT_TO_STRING(indexPath.row)];
}];
[cell setLongPressDragBeganBlock:^(CGPoint centerPoint) {
[weakSelf judgeDragPositionWithCenterPoint:centerPoint indexPath:indexPath];
}];
[cell setLongPressDragChangeBlock:^(CGPoint centerPoint) {
[weakSelf judgeDragPositionWithCenterPoint:centerPoint indexPath:indexPath];
}];
[cell setLongPressDragEndedBlock:^(WBAreaDetailModel *currentAreaDetailModel, WBDeviceModel *currentDeviceModel, CGPoint centerPoint) {
// 先判断一下拖动到的位置是在哪个 IndexPath 上 (可能为空)
NSIndexPath *selectedIndexPath = [weakSelf returnSelectedItemWithCenterPoint:centerPoint indexPath:indexPath];
[weakSelf.collectionView reloadItemsAtIndexPaths:@[ indexPath ]];
if (!selectedIndexPath) {
return ;
} else {
/*
* selectedAreaDetailModel 将要移动到的位置
* currentAreaDetailModel 当前数据源的位置
* currentDeviceModel 当前设备数据 Model
*/
// 调用自己的重排 API
WBAreaDetailModel *selectedAreaDetailModel = self.areasDetail[selectedIndexPath.row];
NSString *str = [NSString stringWithFormat:@"%@ - %@\n\n移动到\n\n%@", currentAreaDetailModel.roomName, currentDeviceModel.deviceName, selectedAreaDetailModel.roomName];
ShowTips(str);
}
}];
return cell;
}