(iOS)能够这样来玩玩tableViewCell的滑动菜单

系统的tableView是只须要配置几个代理方法, 就能够实现cell的左右侧滑菜单的. 通常会被用来做为编辑,删除等使用. 可是虽然在使用上挺方便的. 不过系统提供的的样式局限性很大, 就像QQ的侧滑样式, 只能显示字符而且动画效果很单一. 不过, 咱们实际开发中会遇到的可能并不只仅是这么简单, 多是上面图片显示的这样本节中就分享给朋友们吧, 也许不久的开发中你就会遇到相似的需求了, 那就再好不过了.javascript

本节中, 咱们将实现自定义的tableViewCell的侧滑菜单, 而且实现四种常见的动画效果, 同时简书炫酷的侧滑效果也一并实现了.java

这个看上去比较小的需求, 笔者最初尝试实现的时候仍然是不知道如何下手去完成, 通过一段时间的考虑后才有一些想法. 后来大概使用了两种方式来实现. 由于在实现这个需求以前笔者本身实现过抽屉菜单的需求(咱们上一节中也已经实现了), 最初想到的就是在每个cell相似抽屉菜单同样, 增长两个左右的抽屉菜单, 而后打开和关闭就和咱们处理抽屉菜单同样, 最终是顺利的实现了这个需求. 用上去仍是比较方便. 后来再次回头研究的时候, 想到了另一种比较方便的实现方法. 下面咱们就使用这种方法来实现了.

1. 首先咱们新建一个ZJSwipeTableViewCell : UITableViewCell来实现滑动菜单的需求, 而后方便使用者直接使用或者继承咱们这个就能够了. 咱们首先很清楚的是cell上面须要添加一个滑动手势UIPanGestureRecognizer,来处理滑动.增长这个属性panGesture,而后重写cell的初始化方法, 添加上这个手势到cell上面, 注意咱们同时但愿支持xib自定义的cell, 因此重写的初始化方法中要包括- (instancetype)initWithCoder:(NSCoder *)aDecoder.
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        [self commonInit];

    }
    return self;
}

- (instancetype)init {
    if (self = [super init]) {
        [self commonInit];
    }
    return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self commonInit];
    }
    return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        [self commonInit];
    }
    return self;
}
- (void)commonInit {
    _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
    self.panGesture.delegate = self;
    [self addGestureRecognizer:self.panGesture];

}复制代码
2. 由于咱们但愿实现的滑动菜单中的按钮能够展现多种样式的内容, 好比只展现图片, 只展现文字, 能够同时展现图片和文字, 不过图片在上方文字在下方. 因此咱们首先自定义一下咱们须要的按钮. 新建一个ZJSwipeButton : UIButton,而后咱们自定义一个初始化的方法便于后面使用, 须要的参数有图片,文字,点击响应的block, 而后咱们在这个方法里面根据文字的长度和图片的尺寸设置好按钮的宽高.
- (instancetype)initWithTitle:(NSString *)title image:(UIImage *)image onClickHandler:(ZJSwipeButtonOnClickHandler)onClickHandler {
    if (self = [super init]) {
        _onClickHandler = [onClickHandler copy];
        [self addTarget:self action:@selector(swipeBtnOnClick:) forControlEvents:UIControlEventTouchUpInside];
        [self setTitle:title forState:UIControlStateNormal];
        [self setImage:image forState:UIControlStateNormal];
        [self setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
        self.backgroundColor = [UIColor greenColor];
        CGFloat margin = 10;
        // 计算文字尺寸
        CGSize textSize = [title boundingRectWithSize:CGSizeMake(MAXFLOAT, 200.f) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: self.titleLabel.font, NSForegroundColorAttributeName: self.titleLabel.textColor } context:nil].size;
        // 计算按钮宽度, 取图片宽度和文字宽度较大者
        CGFloat btnWidth = MAX(image.size.width+margin, textSize.width+margin);
        // 文字居中
        self.titleLabel.textAlignment = NSTextAlignmentCenter;
        // 暂时的, 宽高有效, 其余的会在父控件(ZJSwipeView)中调整
        self.frame = CGRectMake(0.f, 0.f, btnWidth, image.size.height+textSize.height+margin);

    }
    return self;
}复制代码
3. 而后ZJSwipeButton还有一点须要处理的是, 若是须要显示图片的时候,在layoutSubviews中从新设置imageView和titleLabel的frame, 让图片在上面,文字在下面显示, 同时须要处理按钮点击的响应事件, 执行外部传递的block就能够了.
- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.imageView.image) {
        // 设置了图片, 从新调整imageView和titleLabel的frame
        // 让图片在上, 文字在下显示
        CGFloat selfHeight = self.bounds.size.height;
        CGFloat selfWidth = self.bounds.size.width;

        CGSize imageSize = self.imageView.image.size;
        CGFloat imageAndTextMargin = 5.f;

        CGFloat margin = (selfHeight - imageSize.height - self.titleLabel.bounds.size.height - imageAndTextMargin)/2;
        self.imageView.frame = CGRectMake((selfWidth-imageSize.width)/2, margin, imageSize.width, imageSize.height);
        // 计算文本frame
        CGRect titleLabelFrame = self.titleLabel.frame;
        titleLabelFrame.origin.x = 0;
        titleLabelFrame.origin.y = CGRectGetMaxY(self.imageView.frame) + imageAndTextMargin;
        titleLabelFrame.size.width = selfWidth;
        self.titleLabel.frame = titleLabelFrame;
    }
}
// 按钮点击响应事件
- (void)swipeBtnOnClick:(UIButton *)btn {
    if (_onClickHandler) {
        _onClickHandler(btn);
    }
}复制代码
4. 处理好了咱们的侧滑菜单上的按钮, 接下来须要处理咱们的侧滑菜单了, 侧滑菜单分为左右菜单, 上面用来容纳左右的按钮, 因此咱们但愿将这些按钮的frame设置等工做所有交给侧滑菜单来处理, 而不须要咱们在ZJSwipeTableViewCell里面来完成. 因此新建一个ZJSwipeView : UIView, 自定义初始化方法. 咱们须要的参数有, 菜单上须要显示的按钮和高度.
- (instancetype)initWithSwipeButtons:(NSArray<ZJSwipeButton *> *)swipeButtons height:(CGFloat)height {

    if (self = [super init]) {

        CGFloat btnX = 0.f;
        CGFloat allBtnWidth = 0.f;
        // 为每一个按钮设置frame, 同时计算好全部的按钮的宽度之和, 做为swipeView的宽度
        // 注意这里是反向遍历添加的
        for (ZJSwipeButton *button in [swipeButtons reverseObjectEnumerator]) {
            [self addSubview:button];

            button.frame = CGRectMake(btnX, 0, button.bounds.size.width, height);
            btnX += button.bounds.size.width;
            allBtnWidth += button.bounds.size.width;
        }
        // 设置frame 宽高有效, x, y在swipeTableViewCell中还会相应的调整
        self.frame = CGRectMake(0.f, 0.f, allBtnWidth, height);
        self.backgroundColor = [UIColor whiteColor];
    }
    return self;
}复制代码
5. 完成了ZJSwipeView和ZJSwipeButton的处理, 接下来就是正式处理ZJSwipeTableViewCell了. 由于上面提到的第一种方法, 在处理滑动的时候cell上的内容的滚动不是很方便, 因此笔者换了一种实现方式, 那就是咱们常用到的截图. 咱们在开始滑动的时候将cell截图, 而后将这张截图添加到cell上面, 随着手势滚动的时候只须要调整截图的位置就能够了, 这样就不用考虑cell内部的位置调整了. 让咱们的工做量就减少了不少不少.在结合咱们以前完成抽屉菜单的经验, 咱们能够将左右的swipeView添加在同一个overlayerContentView来管理, 而后手势移动的时候只须要改变overlayerContentView的和cell的截图snapView的frame就能够了. 因此天然咱们会添加上这些属性.
// cell的截图
@property (strong, nonatomic) UIView *snapView;
// 全部添加的subviews的容器, 滑动时覆盖在cell上
@property (nonatomic, strong) UIView *overlayerContentView;
// 右边的滑动菜单
@property (nonatomic, strong) ZJSwipeView *rightView;
// 左边的滑动菜单
@property (nonatomic, strong) ZJSwipeView *leftView;复制代码
6. 咱们以前完成了抽屉菜单ZJDrawerController, 那么咱们很清楚, 相似的咱们还须要一些属性来帮助咱们处理在手势滑动的过程当中的滑动方向的判断和滑动的距离的获取.
// 滑动操做的类型
typedef NS_ENUM(NSUInteger, ZJSwipeOperation) {
    ZJSwipeOperationNone,
    ZJSwipeOperationOpenLeft,
    ZJSwipeOperationCloseLeft,
    ZJSwipeOperationOpenRight,
    ZJSwipeOperationCloseRight
};
// 记录手势开始的时候`overlayerContentView`的x
CGFloat _beginContentViewX;
// 记录手势开始的时候`snapView`的x
CGFloat _beginSnapViewX;
// 记录手势开始的时候手指的位置, 便于处理手指松开的时候判断滑动了多远,是否完成滑动
CGFloat _beginX;复制代码
7. 咱们就能够处理滑动手势了, 在手势处理的方法中, 咱们须要处理的是: 手势开始的时候设置好左右侧滑菜单和cell截图而且记录须要的初始数据, 在手指滑动状态的时候, 咱们须要根据滑动操做的类型, 相应的改变滑动菜单的frame和切换动画, 最后是在手指离开的时候, 咱们根据滑动的距离和离开时的滑动速度来判断是否打开和关闭菜单. 手势开始的状态.
case UIGestureRecognizerStateBegan: {
  // 设置左右侧滑菜单和截图
  [self setupSwipeViewWithSwipeVelocityX:velocityX];
  // 记录初始数据
  _beginX = locationX;
  _beginSnapViewX = self.snapView.zj_x;
  _beginContentViewX = self.overlayerContentView.zj_x;
  self.swipeOperation = ZJSwipeOperationNone;

}复制代码
8. 设置左右侧滑菜单和截图, 咱们知道, 若是左右的swipeView没有建立, 咱们首先须要建立他们, 这个时候咱们就须要获取到swipeView上面须要显示的按钮swipeButton, 这些按钮的建立应该是外部的使用者来建立的, 因此咱们可使用代理来完成, 新定义一个协议ZJSwipeTableViewCellDelegate添加两个代理方法来获取咱们这个cell所须要的左右侧滑按钮.
@protocol ZJSwipeTableViewCellDelegate <NSObject>

@required

/** * 左滑cell时显示的button 返回nil表示不建立左边菜单 * * @param indexPath cell的位置 */
- (NSArray<ZJSwipeButton *> *)tableView:(UITableView *)tableView leftSwipeButtonsAtIndexPath:(NSIndexPath *)indexPath;

/** * 右滑cell时显示的button 返回nil表示不建立右边菜单 * * @param indexPath cell的位置 */
- (NSArray<ZJSwipeButton *> *)tableView:(UITableView *)tableView rightSwipeButtonsAtIndexPath:(NSIndexPath *)indexPath;

@end复制代码
9. 能够看到咱们上面定义的代理方法里面须要的参数有tableView和indexPath, 那么咱们swipeTableViewCell怎么获取到它所在的tableView和所在tableView上的indexPath了? 这又是咱们很经常使用的一个处理, 遍历cell的superView便可获取到, 由于咱们其余地方会用到cell所在的tableView, 因此咱们把tableView写成一个属性, 不过要注意的是, 应该使用weak. 获取cell在tableView上的indexPath就使用tableView的一个方法就能够直接获取到了
- (UITableView *)tableView {
    if (!_tableView) {
        UIView *nextView = self.superview;
        while (self.superview) {
            // 遍历cell的superView, 当superView是UITableView的时候, 说明找到了
            // cell所在的tableView
            if ([nextView isKindOfClass:[UITableView class]]) {
                _tableView = (UITableView *)nextView;
                break;
            }
            nextView = nextView.superview;
        }
    }
    return _tableView;
}
// 获取当前cell的indexPath
NSIndexPath *indexPath = [self.tableView indexPathForCell:self];复制代码
10. 而后就能够设置左右侧滑菜单和截图, 咱们将leftView和rightView添加到overlayerContentView上面而且设置frame和咱们在完成ZJDrawerController的时候彻底同样, 因此这里就再也不赘述设置frame的思路了. 若是不清楚的朋友, 能够去阅读书籍对应的章节, 不得不说的是, 你应该要很清楚设置这些frame的思路, 不然咱们在手指改变的处理方法中改变snapView和overlayerContentView的frame你可能就很难明白其中的缘由了. 这里须要注意的是, 咱们应该按需建立, 建立以前必定要判断是否须要建立和添加, 这一部分的代码比较简单和繁琐, 请读者直接阅读源码;
if (self.overlayerContentView == nil) {

    NSArray<ZJSwipeButton *> *leftBtns = [self.delegate tableView:self.tableView leftSwipeButtonsAtIndexPath:[self.tableView indexPathForCell:self]];

    NSArray<ZJSwipeButton *> *rightBtns = [self.delegate tableView:self.tableView rightSwipeButtonsAtIndexPath:[self.tableView indexPathForCell:self]];
    // 不符合条件不建立
    // 左边按钮个数为0 说明不须要建立左边菜单,这个时候向右滑动试图打开左边菜单 直接就返回了
    // 右边按钮个数为0 说明不须要建立右边菜单,这个时候向左滑动试图打开右边菜单 直接就返回了
    if ((leftBtns.count==0 && velocityX>0) || (rightBtns.count==0 && velocityX<0)) {
     return;
    }
    if (self.leftView == nil) {
      //建立leftView而且设置frame和添加到overlayerContentView
    }   
    if (self.righttView == nil) {
      //建立rightView而且设置frame和添加到overlayerContentView
    }        
    // 先添加overlayerContentView 到cell上, 再添加cell截图, 注意顺序
    [self addSubview:self.overlayerContentView];

    // 再添加截图
    if (self.snapView == nil) {
        // 系统提供的方法 iOS7以后就不用咱们本身来绘图实现截图的需求了
        self.snapView = [self snapshotViewAfterScreenUpdates:NO];
        self.snapView.frame = self.bounds;
        // 添加到cell上
        [self addSubview:self.snapView];
    }   
}复制代码
11. 接下来是处理手指滑动过程当中snapView和overlayerContentView的frame的改变了. 这一部分和咱们当时实现ZJDrawerController的时候非缩放效果的处理几乎彻底同样. 若是读者在以前理解的比较好或者本身动手实现过, 那么阅读这一段代码使不会有任何问题的, 这里就简单说起几个地方了. snapView由于是跟随手指同步滚动的, 因此他的frame.x的改变和手指的位置改变彻底同步, 并不受到滚动方向的影响. 而overlayerContentView则须要根据是打开左边, 关闭左边, 打开右边, 关闭右边这四种不一样的操做在对应的设置frame.x. 这里以打开左边菜单为例. 代码较多, 请君仔细阅读.
case UIGestureRecognizerStateChanged: {
    // 始终同步滚动 snapView
    CGFloat tempSnapViewX = _beginSnapViewX;
    tempSnapViewX += transitionX;
    self.snapView.zj_x = tempSnapViewX;

    // 向右滑动说明是 打开左边 或者关闭右边
    if (transitionX>0) {
        // 右边菜单存在, 而且开始滑动时截图的x = 右边菜单宽度的负值
        // 说明此次手势开始的时候右边的菜单是打开的, 正在关闭右边的菜单
        if (self.rightView && _beginSnapViewX == -self.rightView.zj_width) {
            // 记录为正在关闭右边菜单, 便于在手指离开的时候判断
            self.swipeOperation = ZJSwipeOperationCloseRight;
            // 影藏左边菜单 显示右边菜单
            [self hideAndShowSwipeViewNeededWithShowleft:NO];
            // 手指向右移动的距离 >= 右边菜单的宽度, 说明右边菜单已经彻底关闭
            // 手指再继续右移就变成了打开左边菜单的操做了, 这个时候就要
            // 将各个变量设置为打开左边菜单的初始值
            if (transitionX>=self.rightView.zj_width) {
                // 右边关闭完成 --- 变为打开左边
                // 手势设置移动为0
                [panGesture setTranslation:CGPointZero inView:self];
                // 重置开始X
                _beginContentViewX = -self.leftView.zj_width*self.animatedTypePercent;
                _beginX = locationX;
                _beginSnapViewX = 0;
                self.overlayerContentView.zj_x = -self.leftView.zj_width*self.animatedTypePercent;
            }
            else {
                // 正在关闭右边 改变overlayerContentView的x
                CGFloat tempX = _beginContentViewX;
                tempX += transitionX*self.animatedTypePercent;
                self.overlayerContentView.zj_x = tempX;
            }
            // 这是咱们模仿简书的打开和关闭的时候的动画效果进行的frame计算, 须要一点数学能力
            [self animateSwipeButtonsWithPercent:transitionX/self.rightView.zj_width];

        }

    }
}复制代码
12. 最后是手指离开屏幕的时候, 咱们应该根据滚动的距离和手指离开时的速度来判断这一次操做是否完成仍是返回操做前的状态. 这里就以关闭右边菜单为例. 其余状况相似的呢.
case UIGestureRecognizerStateEnded: {
  CGFloat velocityX = [panGesture velocityInView:self].x;
  if (self.swipeOperation == ZJSwipeOperationCloseRight) {
      // 若是手指移动的距离 > 咱们定义的百分比 说明应该执行动画关闭右边菜单
      if (fabs(_beginX - locationX) > self.rightView.zj_width*self.threholdPercent) {
          [self animatedCloseRight];
      }
      else {
          // 若是手指移动的距离较小, 就判断手指离开的速度是否大于咱们定义的最小速度
          // 若是大于证实应该执行动画关闭右边菜单, 不然说明关闭右边失败, 从新打开 右边菜单
          if (fabs(velocityX) > _threholdSpeed)
              [self animatedCloseRight];
          else
              [self animatedOpenRight];

      }
  }
}复制代码
13. 关于咱们定义的ZJSwipeViewAnimatedStyle这个枚举中, 定义了四种动画类型, 其中的三种和咱们实现ZJDrawerController的三种动画方式彻底相同, 第四种模仿简书的动画的代码须要一点点的数学能力去理解, 这里即不在说起了, 请读者直接参考源码, 实现相应的四种动画效果.
14. 完成了上面的工做, 咱们就能够写测试代码了, 在ViewController中添加tableView而后使用咱们的ZJSwipeTableViewCell, 实现对应的返回左右菜单按钮的代理方法, 顺利的话, 就能正常的运行了, 而后能够左右侧滑而且上面的按钮显示正常点击也是正常的还有咱们实现的四种动画效果. 看上去不错. 不过问题就来了, 如今不能滚动tableView了, 由于咱们添加在cell上的手势和系统的手势发生了冲突, 因而咱们, 须要在咱们添加的panGesture的代理方法中判断若是是准备上下滑动就不要开始手势, 就不会和系统的手势冲突了.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {

    if (gestureRecognizer == self.panGesture) {
        CGPoint transion = [self.panGesture translationInView:self];
        return transion.y == 0; // 是不是上下滑动
    }
 }复制代码
15. 如今在运行项目tableView就能正常的滚动了, 可是如今咱们发如今开始滚动和点击其余地方的时候滑动菜单并不会自动关闭. 笔者这里的处理方式是在ZJSwipeTableViewCell所在的tableView上面添加一个tap手势, 当侧滑菜单打开的时候, 点击tableView就关闭滑动菜单,可是, 咱们要注意处理tap手势和tableView点击cell的手势的冲突, 因此咱们在tap手势的代理中判断, 只有在滑动菜单打开的时候才能执行tap手势.
if (gestureRecognizer == self.tapGesture) { // 全部的cell公用这一个tapGesture
        if (self.overlayerContentView) {
            return YES;
        }
        else {
            return NO;
        }
}复制代码
16. 处理tableView开始滚动的时候关闭打开的滑动菜单, 笔者是经过kvo来监听tableView手势状态的改变, 在手势开始的时候就关闭滑动菜单. 同时由于tableView的重用机制, 咱们添加在cell上面的截图和滑动菜单, 咱们应该在关闭完成的时候移除掉, 从而不影响咱们原来的cell的操做.
- (void)resetInitialState {
    // 移除kvo监听者
    [self removeTableViewObserver];
    // 移除tap手势
    [self.tableView removeGestureRecognizer:self.tapGesture];
    // 移除添加的view
    [self.snapView removeFromSuperview];
    self.snapView = nil;
    [self.overlayerContentView removeFromSuperview];
    self.overlayerContentView = nil;
    self.leftView = nil;
    self.rightView = nil;
    self.tapGesture = nil;
}复制代码

到这里咱们实现的使用方便灵活的tableView侧滑菜单就结束了, 那么如今你就可使用咱们实现的这个ZJSwipeTableViewCell来替代系统本来的侧滑效果了, 固然和咱们以前实现的抽屉菜单同样, 你还能够本身实现各类须要的炫酷的动画效果. 我相信充满想象力的你必定实现的比笔者这里的要更炫酷和强大.

注意:
这是书籍内容中的一个章节, 做为试读文章, 应该已经算书中涉及到的demo中有难度的实现效果了. 从这一节试读章节能够看出, 书中的全部demo实现的难度都不大.同时你也能够参考全部demo的源码来判断每一节的实现难度, 从而总体评估这种难度的书籍是否须要去阅读, 同时判断个人写做风格是否适合你阅读. 关于书籍的更多说明在这里, 请仔细评估.git

相关文章
相关标签/搜索