之前寫了一篇有關於UICollectionViewCell的長按移動的文章:iOS 長按移動UICollectionView中Cell的位置。講述的是iOS端的UICollectionViewCell的長按移動方式。同樣的方法,在UITableViewCell上面是不能實現的,因爲蘋果並沒有提供:
-(void) beginInteractiveMovementForItemAtIndexPath:(NSIndexPath *)indexPath
這個方法給UITableView。
所以我們要換一種思路去完成這個功能,其實在實現原理上和UICollectionView很相似:
1.我們需要記錄長按的Cell的NSIndexPath,然後對其截圖,並且將Cell隱藏,之後的移動動作全是對這個截圖完成的。
2.在移動的過程中,不停地刷新手勢的位置。通過手勢位置獲取新的NSIndexPath,並且不斷地更新數據源,修改UITableViewCell的位置。這時,可以開一個CADisplayLink(定時器,和屏幕刷新率相同的頻率調用的。),作用是,當手勢滑動到最頂部或者最底部的時候,動態的改變UITableView的contentOffset。
3.結束定時器,銷燬截圖,顯示Cell。
思路其實就是這麼簡單,先上圖:
ok,接下來給大家演示代碼:
#import "ViewController.h"
#define kScreenWidth [[UIScreen mainScreen] bounds].size.width
#define kScreenHeight [[UIScreen mainScreen] bounds].size.height
typedef NS_ENUM(NSInteger, LYFTableViewType) {
/// 頂部
LYFTableViewTypeTop,
/// 底部
LYFTableViewTypeBottom
};
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
/// 列表
@property (nonatomic, strong) UITableView *tableView;
/// 數據
@property (nonatomic, strong) NSMutableArray *datas;
/// 記錄手指所在的位置
@property (nonatomic, assign) CGPoint longLocation;
/// 對被選中的cell的截圖
@property (nonatomic, strong) UIView *snapshotView;
/// 被選中的cell的原始位置
@property (nonatomic, strong) NSIndexPath *oldIndexPath;
/// 被選中的cell的新位置
@property (nonatomic, strong) NSIndexPath *newestIndexPath;
/// 定時器
@property (nonatomic, strong) CADisplayLink *scrollTimer;
/// 滾動方向
@property (nonatomic, assign) LYFTableViewType scrollType;
@end
static NSString *cellId = @"cell";
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.datas = [NSMutableArray arrayWithArray:@[[NSMutableArray arrayWithArray:@[@"老大", @"老二", @"老三", @"老四", @"老五", @"老六", @"老七", @"老八", @"老九", @"老十"]], [NSMutableArray arrayWithArray:@[@"老1", @"老2", @"老3", @"老4", @"老5", @"老6", @"老7", @"老8", @"老9", @"老10"]]]];
[self.tableView reloadData];
}
#pragma mark - 對cell進行截圖,並且隱藏
-(void)snapshotCellAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
/// 截圖
UIView *snapshot = [self snapshotView:cell];
/// 添加在UITableView上
[self.tableView addSubview:snapshot];
self.snapshotView = snapshot;
/// 隱藏cell
cell.hidden = YES;
CGPoint center = self.snapshotView.center;
center.y = self.longLocation.y;
/// 移動截圖
[UIView animateWithDuration:0.2 animations:^{
self.snapshotView.transform = CGAffineTransformMakeScale(1.03, 1.03);
self.snapshotView.alpha = 0.98;
self.snapshotView.center = center;
}];
}
#pragma mark - 截圖對應的cell
- (UIView *)snapshotView:(UIView *)inputView {
// Make an image from the input view.
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, NO, 0);
[inputView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Create an image view.
UIView *snapshot = [[UIImageView alloc] initWithImage:image];
snapshot.center = inputView.center;
snapshot.layer.masksToBounds = NO;
snapshot.layer.cornerRadius = 0.0;
snapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0);
snapshot.layer.shadowRadius = 5.0;
snapshot.layer.shadowOpacity = 0.4;
return snapshot;
}
#pragma mark - 長按手勢
-(void)longPressGestureRecognized:(UILongPressGestureRecognizer *)longPress {
UIGestureRecognizerState longPressState = longPress.state;
//長按的cell在tableView中的位置
self.longLocation = [longPress locationInView:self.tableView];
//手指按住位置對應的indexPath,可能爲nil
self.newestIndexPath = [self.tableView indexPathForRowAtPoint:self.longLocation];
switch (longPressState) {
case UIGestureRecognizerStateBegan:{
//手勢開始,對被選中cell截圖,隱藏原cell
self.oldIndexPath = [self.tableView indexPathForRowAtPoint:self.longLocation];
if (self.oldIndexPath) {
[self snapshotCellAtIndexPath:self.oldIndexPath];
}
break;
}
case UIGestureRecognizerStateChanged:{//點擊位置移動,判斷手指按住位置是否進入其它indexPath範圍,若進入則更新數據源並移動cell
//截圖跟隨手指移動
CGPoint center = _snapshotView.center;
center.y = self.longLocation.y;
self.snapshotView.center = center;
if ([self checkIfSnapshotMeetsEdge]) {
[self startAutoScrollTimer];
}else{
[self stopAutoScrollTimer];
}
//手指按住位置對應的indexPath,可能爲nil
self.newestIndexPath = [self.tableView indexPathForRowAtPoint:self.longLocation];
if (self.newestIndexPath && ![self.newestIndexPath isEqual:self.oldIndexPath]) {
[self cellRelocatedToNewIndexPath:self.newestIndexPath];
}
break;
}
default: {
//長按手勢結束或被取消,移除截圖,顯示cell
[self stopAutoScrollTimer];
[self didEndDraging];
break;
}
}
}
#pragma mark - 檢查截圖是否到達邊緣,並作出響應
- (BOOL)checkIfSnapshotMeetsEdge{
CGFloat minY = CGRectGetMinY(self.snapshotView.frame);
CGFloat maxY = CGRectGetMaxY(self.snapshotView.frame);
if (minY < self.tableView.contentOffset.y) {
self.scrollType = LYFTableViewTypeTop;
return YES;
}
if (maxY > self.tableView.bounds.size.height + self.tableView.contentOffset.y) {
self.scrollType = LYFTableViewTypeBottom;
return YES;
}
return NO;
}
#pragma mark - 當截圖到了新的位置,先改變數據源,然後將cell移動過去
- (void)cellRelocatedToNewIndexPath:(NSIndexPath *)indexPath{
//更新數據源並返回給外部
[self updateData];
//交換移動cell位置
[self.tableView moveRowAtIndexPath:self.oldIndexPath toIndexPath:indexPath];
//更新cell的原始indexPath爲當前indexPath
self.oldIndexPath = indexPath;
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:_oldIndexPath];
cell.hidden = YES;
}
#pragma mark - 更新數據源
-(void)updateData {
//通過DataSource代理獲得原始數據源數組
NSMutableArray *tempArray = self.datas;
//判斷原始數據源是否爲多重數組
if ([self arrayCheck:tempArray]) {//是嵌套數組
if (self.oldIndexPath.section == self.newestIndexPath.section) {//在同一個section內
[self moveObjectInMutableArray:tempArray[self.oldIndexPath.section] fromIndex:self.oldIndexPath.row toIndex:self.newestIndexPath.row];
}else{ //不在同一個section內
id originalObj = tempArray[self.oldIndexPath.section][self.oldIndexPath.item];
[tempArray[self.newestIndexPath.section] insertObject:originalObj atIndex:self.newestIndexPath.item];
[tempArray[self.oldIndexPath.section] removeObjectAtIndex:self.oldIndexPath.item];
}
}else{ //不是嵌套數組
[self moveObjectInMutableArray:tempArray fromIndex:self.oldIndexPath.row toIndex:self.newestIndexPath.row];
}
}
#pragma mark - UITableViewDataSource / UITableViewDelegate
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.datas.count;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSMutableArray *data = self.datas[section];
return data.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressGestureRecognized:)];
[cell.contentView addGestureRecognizer:longPress];
}
NSMutableArray *data = self.datas[indexPath.section];
cell.textLabel.text = data[indexPath.row];
return cell;
}
#pragma mark - 檢測是否是多重數組
- (BOOL)arrayCheck:(NSArray *)array{
for (id obj in array) {
if ([obj isKindOfClass:[NSArray class]]) {
return YES;
}
}
return NO;
}
#pragma mark - 將可變數組中的一個對象移動到該數組中的另外一個位置
- (void)moveObjectInMutableArray:(NSMutableArray *)array fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex{
if (fromIndex < toIndex) {
for (NSInteger i = fromIndex; i < toIndex; i ++) {
[array exchangeObjectAtIndex:i withObjectAtIndex:i + 1];
}
}else{
for (NSInteger i = fromIndex; i > toIndex; i --) {
[array exchangeObjectAtIndex:i withObjectAtIndex:i - 1];
}
}
}
#pragma mark - 開始自動滾動
- (void)startAutoScroll {
CGFloat pixelSpeed = 4;
if (self.scrollType == LYFTableViewTypeTop) {//向下滾動
if (self.tableView.contentOffset.y > 0) {//向下滾動最大範圍限制
[self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y - pixelSpeed)];
self.snapshotView.center = CGPointMake(self.snapshotView.center.x, self.snapshotView.center.y - pixelSpeed);
}
}else{ //向上滾動
if (self.tableView.contentOffset.y + self.tableView.bounds.size.height < self.tableView.contentSize.height) {//向下滾動最大範圍限制
[self.tableView setContentOffset:CGPointMake(0, self.tableView.contentOffset.y + pixelSpeed)];
self.snapshotView.center = CGPointMake(self.snapshotView.center.x, self.snapshotView.center.y + pixelSpeed);
}
}
/// 當把截圖拖動到邊緣,開始自動滾動,如果這時手指完全不動,則不會觸發‘UIGestureRecognizerStateChanged’,對應的代碼就不會執行,導致雖然截圖在tableView中的位置變了,但並沒有移動那個隱藏的cell,用下面代碼可解決此問題,cell會隨着截圖的移動而移動
self.newestIndexPath = [self.tableView indexPathForRowAtPoint:self.snapshotView.center];
if (self.newestIndexPath && ![self.newestIndexPath isEqual:self.oldIndexPath]) {
[self cellRelocatedToNewIndexPath:self.newestIndexPath];
}
}
#pragma mark - 拖拽結束,顯示cell,並移除截圖
- (void)didEndDraging{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:self.oldIndexPath];
cell.hidden = NO;
cell.alpha = 0;
[UIView animateWithDuration:0.2 animations:^{
self.snapshotView.center = cell.center;
self.snapshotView.alpha = 0;
self.snapshotView.transform = CGAffineTransformIdentity;
cell.alpha = 1;
} completion:^(BOOL finished) {
cell.hidden = NO;
[self.snapshotView removeFromSuperview];
self.snapshotView = nil;
self.oldIndexPath = nil;
self.newestIndexPath = nil;
[self.tableView reloadData];
}];
}
#pragma mark - 創建定時器
- (void)startAutoScrollTimer {
if (!self.scrollTimer) {
self.scrollTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(startAutoScroll)];
[self.scrollTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
}
#pragma mark - 銷燬定時器
- (void)stopAutoScrollTimer {
if (self.scrollTimer) {
[self.scrollTimer invalidate];
self.scrollTimer = nil;
}
}
#pragma mark - Get方法
-(UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight) style:UITableViewStyleGrouped];
_tableView.dataSource = self;
_tableView.delegate = self;
[self.view addSubview:_tableView];
}
return _tableView;
}
@end
代碼很簡單,迎大家提出意見、建議。
這是github鏈接:傳送門
喜歡的朋友可以點個贊啦!😘