在Clear应用中,用户无需任何按钮,纯靠不同的手势就可以完成对ToDoItem的删除、完成、添加、移动。具体来说,功能上有左划删除,右划完成,点击编辑,下拉添加、捏合添加、长按移动。这里将这些功能实现并记录。
所谓的左右滑动,就是自定义一个cell然后在上面添加滑动手势。在处理方法中计算偏移量,如果滑动距离超过cell宽度一半,就删除它,或者是为文本添加删除线等来完成它;如果没有超过一半,那么就用动画把cell归位。
效果图如下:
关键代码如下:
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
//关闭键盘,结束编辑状态
if ([self.textField canResignFirstResponder]) {
[self.textField resignFirstResponder];
}
//计算偏移量
CGPoint trans = [recognizer translationInView:self];
//该alpha值用于改变左右两遍提示用label的透明度
float alpha = fabsf(self.frame.origin.x) / (self.frame.size.width / 2);
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
//起点在cell原来的位置
self.originalPoint = self.center;
break;
case UIGestureRecognizerStateChanged:
self.tickLabel.alpha = alpha;
self.crossLabel.alpha = alpha;
//根据偏移量决定cell的位置
self.center = CGPointMake(self.originalPoint.x + trans.x, self.originalPoint.y);
//左划超过一半后删除
if (self.frame.origin.x <= -self.frame.size.width / 2) {
self.deleteOnDragRelease = YES;
self.crossLabel.textColor = [UIColor redColor];
} else {
self.deleteOnDragRelease = NO;
self.crossLabel.textColor = [UIColor whiteColor];
}
//右划超过一半后完成
if (self.frame.origin.x >= self.frame.size.width / 2) {
self.completeOnDragRelease = YES;
self.tickLabel.textColor = [UIColor greenColor];
} else {
self.completeOnDragRelease = NO;
self.tickLabel.textColor = [UIColor whiteColor];
}
break;
case UIGestureRecognizerStateEnded:
//如果没有达到要求,那么用动画归位
if (self.deleteOnDragRelease == NO) {
[UIView animateWithDuration:0.3f animations:^{
self.center = self.originalPoint;
}];
} else {
if ([self.delegate respondsToSelector:@selector(toDoItemDeleted:)]) {
[self.delegate toDoItemDeleted:self.item];
}
}
//如果是完成操作,那么添加属性字符串并添加删除线功能
if (self.completeOnDragRelease) {
self.completeLayer.hidden = NO;
NSMutableAttributedString *itemText = [[NSMutableAttributedString alloc] initWithString:self.item.text];
[itemText addAttribute:NSStrikethroughColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, itemText.length)];
[itemText addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInt:kCTUnderlineStyleSingle] range:NSMakeRange(0, itemText.length)];
self.textField.attributedText = itemText;
self.item.isCompleted = YES;
}
break;
default:
break;
}
}
类似于下拉刷新,这里监听scrollview的滑动,当scrollview顶部继续往下滑动,即contentOffset.y < 0时便在最顶端新添加一个cell,为其添加适当的透明度和比例变换。当用户松开手指时,根据偏移量计算出是否应该添加上新的cell,如果超过一定值,则通知代理添加新item。
效果:
关键代码:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
//when user pull down while at the top of the table, set _pullDownInProgress to YES
_pullDownInProgress = (scrollView.contentOffset.y <= 0.0f);
if (_pullDownInProgress) {
//add placeholder
[_tableView insertSubview:_placeholderCell atIndex:0];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (_pullDownInProgress && scrollView.contentOffset.y <= 0) {
_placeholderCell.frame = CGRectMake(0, -scrollView.contentOffset.y - ROW_HEIGHT, _tableView.frame.size.width, ROW_HEIGHT);
_placeholderCell.textField.text = -scrollView.contentOffset.y > ROW_HEIGHT ? @"Release to Add New Item" : @"Pull to Add New Item";
_placeholderCell.alpha = MIN(1.0f, -scrollView.contentOffset.y / ROW_HEIGHT);
NSLog(@"%f", -scrollView.contentOffset.y / ROW_HEIGHT);
} else {
_pullDownInProgress = NO;
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
//check whether the user pull down far enough
if (_pullDownInProgress && -scrollView.contentOffset.y > ROW_HEIGHT) {
//add a new item
[_tableView.dataSource itemAdded];
} else {
_pullDownInProgress = NO;
[_placeholderCell removeFromSuperview];
}
}
通过捏合手势添加cell应该算是相当优秀的设计了,配合一定的动画,可以让效果非常逼真.
对于捏合,API中仅仅会给出scale和velocity两个数值。我们这里需要的是获得两个碰触点的位置,分清上下手指,并对每个手指各自的方向获取其移动距离,这期间让两手指操纵的两个cell以及各自一边的cells都向两边移动,同时在中间展露出placeholder cell。最后,同样的,根据距离决定是否添加该cell。
应用中还有一道判断——如果两个手所在的cells差值不为一,即“跨行捏合”,则不响应这样的操作。我这里没有加上这个限制,而是取中间值的方法,这样操作(测试)起来更方便。
效果图:
注:图中最后下面一片空白的原因是,这里为了防止在编辑时键盘遮挡而向上移动了UITableView,但是模拟器中没有露出键盘,真机时(或者设置模拟器键盘后)不会出现这种现象。
关键代码:
首先为了方便得分清上下手指,我们定义一个结构体:
typedef struct _PinchTouchPoints {
CGPoint upper;
CGPoint lower;
} PinchTouchPoints;
另外,添加两个Utility方法
- (PinchTouchPoints)normalisedPinchTouchPoints:(UIGestureRecognizer *)recognizer {
CGPoint pointOne = [recognizer locationOfTouch:0 inView:_tableView];
CGPoint pointTwo = [recognizer locationOfTouch:1 inView:_tableView];
pointOne.y += _tableView.scrollView.contentOffset.y;
pointTwo.y += _tableView.scrollView.contentOffset.y;
if (pointOne.y > pointTwo.y) {
CGPoint temp = pointTwo;
pointTwo = pointOne;
pointOne = temp;
}
PinchTouchPoints pinchPoints = {pointOne, pointTwo};
return pinchPoints;
}
- (BOOL)view:(UIView *)view containsPoint:(CGPoint)point {
return (view.frame.origin.y < point.y) && (view.frame.origin.y + view.frame.size.height > point.y);
}
第一个方法是根据捏合手势构建一个自定义的结构体的变量,保存着两根手指的位置,这里把contentOffset也要考虑上。第二个方法是判断点是否在view中用的。
接下来就是手势处理方法了:
#pragma mark - UIGestureRecognizer
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self pinchStart:recognizer];
} else if (recognizer.state == UIGestureRecognizerStateChanged && _pinchInProgress && recognizer.numberOfTouches == 2) {
[self pinchChanged:recognizer];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
[self pinchEnded:recognizer];
}
}
- (void)pinchStart:(UIPinchGestureRecognizer *)recognizer {
_initialTouchPoints = [self normalisedPinchTouchPoints:recognizer];
//找到对应的两个cell,这里采用取中间值的方法,无需判断两个cell是否间隔为一
_pointOneIndex = -100;
_pointTwoIndex = -100;
for (int i = 0; i < _tableView.visibleCells.count; i++) {
UIView *view = (UIView *)(_tableView.visibleCells)[i];
if ([self view:view containsPoint:_initialTouchPoints.upper]) {
_pointOneIndex = i;
NSLog(@"pinch point one : %d", _pointOneIndex);
}
if ([self view:view containsPoint:_initialTouchPoints.lower]) {
_pointTwoIndex = i;
NSLog(@"pinch point two : %d", _pointTwoIndex);
}
}
_pointOneIndex = (_pointOneIndex + _pointTwoIndex) / 2;
_pointTwoIndex = _pointOneIndex + 1;
//该行判断无需加上,这里保留仅仅是懒得改:]
if (abs(_pointTwoIndex - _pointOneIndex) == 1) {
_pinchExceededRequiredDistance = NO;
_pinchInProgress = YES;
CustomTableViewCell *upperCell = (CustomTableViewCell *)(_tableView.visibleCells)[_pointOneIndex];
_placeholderCell.frame = CGRectOffset(upperCell.frame, 0, ROW_HEIGHT / 2);
[_tableView.scrollView insertSubview:_placeholderCell atIndex:0];
}
}
- (void)pinchChanged:(UIPinchGestureRecognizer *)recognizer {
PinchTouchPoints currentTouchPoints = [self normalisedPinchTouchPoints:recognizer];
//找出上下手指偏移量中较小者
CGFloat upperDistance = currentTouchPoints.upper.y - _initialTouchPoints.upper.y;
CGFloat lowerDistance = _initialTouchPoints.lower.y - currentTouchPoints.lower.y;
CGFloat delta = -MIN(0, MIN(lowerDistance, upperDistance));
//两边的cell分别向两边移动
for (int i = 0; i < _tableView.visibleCells.count; i++) {
UIView *view = (UIView *)(_tableView.visibleCells)[i];
if (i <= _pointOneIndex) {
view.transform = CGAffineTransformMakeTranslation(0.0f, -delta);
}
if (i >= _pointTwoIndex) {
view.transform = CGAffineTransformMakeTranslation(0.0f, delta);
}
}
//设置scale值和alpha值得创造一种动画效果
CGFloat scaleY = MIN(delta * 2, ROW_HEIGHT) / ROW_HEIGHT;
_placeholderCell.transform = CGAffineTransformMakeScale(1.0f, scaleY);
_placeholderCell.textField.text = (delta > ROW_HEIGHT / 2) ? @"Release to Add New Item" : @"Pinch to Add New Item";
_placeholderCell.alpha = MIN(1.0f, delta * 2 / ROW_HEIGHT);
_pinchExceededRequiredDistance = (delta * 2 > ROW_HEIGHT);
}
- (void)pinchEnded:(UIPinchGestureRecognizer *)recognizer {
if (_pinchInProgress == NO) {
return;
}
_placeholderCell.transform = CGAffineTransformIdentity;
[_placeholderCell removeFromSuperview];
_pinchInProgress = NO;
if (_pinchExceededRequiredDistance) {
//找到添加的下标
NSInteger indexOffset = floor(_tableView.scrollView.contentOffset.y / ROW_HEIGHT);
[_tableView.dataSource itemAddedAtIndex:_pointTwoIndex + indexOffset];
} else {
//归位
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^{
for (UIView *view in _tableView.visibleCells) {
view.transform = CGAffineTransformIdentity;
}
} completion:nil];
}
}
注释中也都有详细的解释。这里就不多废话了。
关于表格中行的编辑,tableView为我们提供了一个方法
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
使用这个方法就可以实现行的交换。现在我们要做的是处理手指按下时的所有过程动画的实现。
首先,tableView并没有一个方法可以让cell跟随着手指一起移动。另外,cell还要承担着通过上述方法实现行交换的任务。因此我们不能轻易改变cell的位置,所以可以新建一个view,完全“模仿”选中位置的cell,将被选中的那个cell隐藏。整个移动过程中都是对这个新建的view进行操作。这样就可以实现了。
效果图:
主要代码:
首先依然是一个Util方法,获得我们整个过程中操作的那个view
- (UIView *)customSnapShotForView:(UIView *)view {
//对传入的view截屏
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//设置阴影效果等
UIImageView *snapShot = [[UIImageView alloc] initWithImage:image];
snapShot.layer.masksToBounds = NO;
snapShot.layer.cornerRadius = 0.0f;
snapShot.layer.shadowOffset = CGSizeMake(-5.0, 0.0);
snapShot.layer.shadowOpacity = 0.4f;
snapShot.layer.shadowRadius = 5.0f;
return snapShot;
}
接下来添加长按手势
_longPressGR = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
[self.tableView addGestureRecognizer:_longPressGR];
然后是处理方法。详见注释:
#pragma mark - Gesture Recognizer
- (void)handleLongPress:(UILongPressGestureRecognizer *)recognizer {
//获取点击位置坐标和点击的cell的indexPath
CGPoint location = [recognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
static NSIndexPath *sourceIndexPath;
static UIView *snapShot;
switch (recognizer.state) {
//有效性验证
case UIGestureRecognizerStateBegan: {
if (!indexPath) {
return;
}
//记录起始位置
sourceIndexPath = indexPath;
//将要操作的view定位并暂时隐藏
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
snapShot = [self customSnapShotForView:cell];
snapShot.center = cell.center;
snapShot.alpha = 0.0f;
[self.tableView addSubview:snapShot];
[UIView animateWithDuration:0.25f animations:^{
//显现、放大、稍作移动。创造一种cell弹出的动画效果
snapShot.center = CGPointMake(snapShot.center.x, location.y);
snapShot.alpha = 0.98f;
snapShot.transform = CGAffineTransformMakeScale(1.05, 1.05);
cell.alpha = 0.0f;
} completion:^(BOOL finished) {
//隐藏将要操作的cell
cell.hidden = YES;
}];
}
break;
case UIGestureRecognizerStateChanged:
snapShot.center = CGPointMake(snapShot.center.x, location.y);
if (indexPath && ![indexPath isEqual:sourceIndexPath]) {
//根据手指的位置移动操作的view,交互表格中行的同时还要更新数据源
[self.items exchangeObjectAtIndex:indexPath.row withObjectAtIndex:sourceIndexPath.row];
[self.tableView moveRowAtIndexPath:indexPath toIndexPath:sourceIndexPath];
//再次记录新的起始位置
sourceIndexPath = indexPath;
}
break;
default: {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:sourceIndexPath];
cell.alpha = 0.0f;
cell.hidden = NO;
[UIView animateWithDuration:0.25f animations:^{
//同Begin相反,创造cell落下的动画效果
snapShot.alpha = 0.0f;
snapShot.transform = CGAffineTransformIdentity;
snapShot.center = cell.center;
cell.alpha = 1.0f;
} completion:^(BOOL finished) {
//清理工作
[snapShot removeFromSuperview];
snapShot = nil;
sourceIndexPath = nil;
}];
}
break;
}
}
除去最后的移动cell之外,以上所有操作都是手势操作类型,总结其思想就是监听手势位置、计算相关数值、改变视图的属性以达到动画效果,手势结束时计算数值判断是否达到一定的要求。cell的移动主要通过新建一个view,截屏,让其充当被移动的cell,同时隐藏真正的cell并通过tableView更新真正的cell的位置。
本文为从网络上各处学习参考而得的学习笔记。主要参考网站为
iOS开发——仿Clear纯手势操作的UITableView
原文:http://blog.csdn.net/u013604612/article/details/43884039