首页 > 移动平台 > 详细

iOS开发——仿Clear纯手势操作的UITableView

时间:2015-02-19 23:00:38      阅读:542      评论:0      收藏:0      [点我收藏+]

前言

在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];
    }
}

注释中也都有详细的解释。这里就不多废话了。

长按移动cell

关于表格中行的编辑,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的位置。

参考

本文为从网络上各处学习参考而得的学习笔记。主要参考网站为

http://www.raywenderlich.com/

iOS开发——仿Clear纯手势操作的UITableView

原文:http://blog.csdn.net/u013604612/article/details/43884039

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!