高仿iOS微信侧滑cell效果

先展示三张效果图:

项目代码
  目前,在微信里我能观察到的交互细节,基本都实现了。如果有哪儿效果不对,或者有些细节没有考虑到,还望指出。

以下是我观察到以及实现的细节:

1.将cell向右推,向右推,移动的距离是被限制的.
2.将cell向右推松手之后有一个反弹的动画.

3.将cell向左推至极限之后松手,会回弹部分.

4.侧滑出来的按钮,有一个,有两个,有自定义的

5.当按钮出来后,除了点击按钮操作之外,无论其他什么操作,侧滑按钮都会隐藏起来。比如在松手按钮出来后,
1⃣️继续向左滑 
2⃣️向右滑 
3⃣️滑动其他cell 
4⃣️滑动tableView   5⃣️点击任何个cell。 都是会触发隐藏动画.

实现

  那么该注意的都列好之后,接下来就一步一步实现。

1.实现cell能滑动

  首先让cell能侧滑先,那是用scrollView还是pan移动手势来实现呢? 
  下面动画图是系统自带的侧滑效果,当你很用力向左边甩的时候,是有弹性动画,滑动的速度不同,反弹的距离也不同。然而你再回去看微信上面,无论你多么用力向左边甩,当你手抬起来的一瞬间,cell都是按照固定的速度在做移动。所以从这可以得出结论,微信是用手势来实现的侧滑.

  那么手势是添加在self上,还是contentView上还是自定义视图?本文项目是自定义了视图,因为第六感告诉我,加在self或者contentView上也许可以实现效果,不过目测可能将会是一条崎岖的道路。

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        [self setupSideslipCell];
    }
    return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        [self setupSideslipCell];
    }
    return self;
}
- (void)setupSideslipCell {
    _containView = [UIView new];
    _containView.backgroundColor = [UIColor lightGrayColor];  
    NSArray *cellSubViews = self.subviews;
    [self insertSubview:_containView atIndex:0];
    for (UIView *subView in cellSubViews) {
        [_containView addSubview:subView];
    }
    _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(containViewPan:)];
    _panGesture.delegate = self;
    [_containView addGestureRecognizer:_panGesture];
}
- (void)containViewPan:(UIPanGestureRecognizer *)pan {
    NSLog(@"containViewPan");
}
- (void)layoutSubviews {
    [super layoutSubviews];
    _containView.frame = self.bounds;
}

  重写initWithStyle:reuseIdentifier:initWithCoder:方法,再统一调用setupSideslipCell是因为,使用我们写的cell的开发者,可能是用代码创建cell,也可能用xib、storyboard来创建。那么initWithStyle:reuseIdentifier:这个方法代码创建之后必须会调用,而initWithCoder:是非代码创建cell时会调用的。(好吧,说的有点初级了)   接下来创建个_containView视图用来容纳cell上的所有视图,遍历cell目前的所有子视图都添加到_containView上,再添加_panGesture手势,移动_containView时,所有的内容跟着移动。   但是加完手势,你会发现上下左右的手势事件都被cell的获取了,使得tableView上下滚不了,一动不动。解决的方式就是实现手势代理方法gestureRecognizerShouldBegin:,做手势是否接收的判断.

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer == _panGesture) {
        UIPanGestureRecognizer *gesture = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint translation = [gesture translationInView:gesture.view];
        return fabs(translation.y) <= fabs(translation.x);
    }
    return YES;
}

  fabs是去绝对值,fabs(translation.y) <= fabs(translation.x)用来判断,手势是上下滑,还是左右滑。如果是上下滑,就return NO;不激活cell的pan手势,tableView就可以拿到移动事件了。

  接着实现手势方法containViewPan,让cell动起来吧!

- (void)containViewPan:(UIPanGestureRecognizer *)pan {
    CGPoint point = [pan translationInView:pan.view];
    [pan setTranslation:CGPointZero inView:pan.view];
    CGRect frame = _containView.frame;
    frame.origin.x += point.x;
    _containView.frame = frame;
}

2.侧滑按钮的出现于消失

  跟着应该要说创建侧面出现的侧滑按钮了。这边说起来有点复杂,我是用的autolayout约束来实现的控制侧滑按钮的宽。建议看下代码,不懂autolayout的先去补补知识。
  侧滑按钮主要是用了_sideslipContainView_sideslipView视图。_sideslipView是加在_sideslipContainView上面的。_sideslipView的右边是紧贴_sideslipContainView的右边。_sideslipView是用来容纳侧滑按钮,_sideslipView的宽度是由里面按钮数量、按钮的宽度决定的,_sideslipContainView的右边是紧贴cell的右边,其中最重要的是_sideslipContainView的左边是紧贴着_containView的右边,所以当_containView没有侧滑的时候,它的右边也是紧贴着cell右边的,_sideslipContainView受左右约束的挤压而使得宽度为0,就看不见了。随着_containView因手势而像左移的时候,_sideslipContainView的宽度就变大,里面的子视图_sideslipView也就漏出来了。(这段文字显得有点惨白无力,图文又不好去创造,靠自己理解啦。如果你考虑用frame来实现,当然也是可以的,就自行发挥撒。)

3.滑动范围限制

  下面做他的滑动范围限制。

#define LYSideslipCellLeftLimitScrollMargin 15
#define LYSideslipCellRightLimitScrollMargin 30
if (pan.state == UIGestureRecognizerStateChanged) {
    CGRect frame = _containView.frame;
    if (frame.origin.x > LYSideslipCellLeftLimitScrollMargin) {
        frame.origin.x = LYSideslipCellLeftLimitScrollMargin;
    } else if (frame.origin.x < -LYSideslipCellRightLimitScrollMargin - CGRectGetWidth(_sideslipView.frame)) {
        frame.origin.x = -LYSideslipCellRightLimitScrollMargin - CGRectGetWidth(_sideslipView.frame);
    }
    _containView.frame = frame;
}

  LYSideslipCellLeftLimitScrollMargin是左边能移动的最大距离,LYSideslipCellRightLimitScrollMargin是右边侧滑按钮出来后再接着能移动的最大距离。CGRectGetWidth(_sideslipView.frame)获取按钮的总宽度。

4.手势结束之后的判断

  限制移动范围之后,在pan.state == UIGestureRecognizerStateEnded手势状态结束的时候判断,此时是什么状态,应该做什么样的操作及动画,动画实现都比较简单。
  反弹动画就是动画之后再一个反方向动画,代码如下:

_containLeftConstraint.constant = _sideslipLeftConstraint.constant = -10;
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    [self layoutIfNeeded];
} completion:^(BOOL finished) {
    _containLeftConstraint.constant = _sideslipLeftConstraint.constant = 0;
    [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
        [self layoutIfNeeded];
    } completion:^(BOOL finished) {
    }];
}];

  要注意cell如果向右移动的距离不是很大的话,微信是没有反弹动画的。而且在动画的过程中,所有的cell,tableView上面的交互都是关闭不处理的。
  还有个细节可能也要在意一下,如果在做手势侧滑按钮漏出一半的过程中,突然电话或者其他事件打断之后,手势状态不一定传UIGestureRecognizerStateEnded,也可能是UIGestureRecognizerStateCancelled或其他状态,也要做相应的处理判断。

5.侧滑按钮显示后的处理

1⃣️当按钮显示出来的动画

  一旦触发的时候,就应该让所有cell以及tableView的交互关闭[self closeAllOperation];。动画结束后再开启交互cell.userInteractionEnabled = YES;,以便接收pan手势。

- (void)showSideslipButton {
    [self closeAllOperation];
    _containLeftConstraint.constant = _sideslipLeftConstraint.constant = -CGRectGetWidth(_sideslipView.frame);
    [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
        [self layoutIfNeeded];
    } completion:^(BOOL finished) {
        for (LYSideslipCell *cell in self.tableView.visibleCells)
            if ([cell isKindOfClass:LYSideslipCell.class]) {
                cell.userInteractionEnabled = YES;
            }
    }];
}
- (void)closeAllOperation {
    self.tableView.scrollEnabled = NO;
    self.tableView.allowsSelection = NO;
    for (LYSideslipCell *cell in self.tableView.visibleCells)
        if ([cell isKindOfClass:LYSideslipCell.class]) {
            cell.sideslip = YES;
            cell.tapGesture.enabled = YES;
            cell.userInteractionEnabled = NO;
        }
}

  在自定义cell中获取tableView的方法:

- (UITableView *)tableView {
    if (!_tableView) {
        id view = self.superview;
        while (view && [view isKindOfClass:[UITableView class]] == NO) {
            view = [view superview];
        }
        _tableView = (UITableView *)view;
    }
    return _tableView;
}
2⃣️继续向左滑&向右滑&滑动其他cell

  侧滑按钮出来后,此时应该添加一个BOOL值sideslip,用来记录当前是否侧滑按钮是显示状态。且要遍历tableView可视的所有cell,将这个值都设为YES。所以一旦pan手势触发的时候,在pan.state == UIGestureRecognizerStateBegan的时候,判断如果sideslip为YES,如果是再判断手势是在侧滑按钮显示的cell上还是不在当前手势的cell上。

if (_containLeftConstraint.constant == 0) {
        for (LYSideslipCell *cell in self.tableView.visibleCells)
            if ([cell isKindOfClass:LYSideslipCell.class])
                [cell hiddenSideslipButton];
    } else {
        [self hiddenSideslipButton];
    }

  如果手势在当前cell上,那么直接调用隐藏动画。如果不在,则遍历可视cell都调用隐藏动画。为防止没有显示出侧滑按钮的cell做动画,所以在[self hiddenSideslipButton];方法里做判断if (_containLeftConstraint.constant == 0) return;

4⃣️滑动tableView

  要使滑动tableView,就触发隐藏动画,简单的方式就是将self.tableView.scrollEnabled = NO;,另外在cell的pan手势代理里判断加上一句if (!self.tableView.scrollEnabled) return YES;,如果tableView不能滚动,cell的pan上下滑的手势也可以触发。所以把这事就扔给pan手势吧。不然还需要在内部设置tableView.contentOffsetobserver,怪麻烦。

5⃣️点击任何个cell

  在初始化cell的时候,就添加个tap手势,平时enabled = NO;不让使用。侧滑按钮出来后再设为YES。

END

  自此,比较重要的点都说的差不多了。有什么疑问或建议欢迎评论里与我交流。

最近的文章

Periscope和映客直播页面的综合体

原版Periscope效果图(颜色比较多所以图片比较多,不过效果还不是很好,建议下个Periscope,开个VPN看看)本demo效果图:代码地址实现手势下拉  模态弹出时需要设置下弹出的模式和风格modalPresentationStyle。是为了回头手势下拉漏出上一个控制器的view时是有内容的,而不会是漆黑一片。因为一个控制器切换到另一个控制器时,系统为了节约资源,会将上一个页面暂时从屏幕上移除,需要的时候再加回来。LYLiveViewController *liveVC = [LY...…

Periscope iOS 映客 继续阅读
更早的文章

用Jekyll搭建的Github Pages个人博客

  近半年一直忙于项目开发,不烙得空。近期慢慢清闲了,终于有机会可以写写属于自己的技术博客。  之前有打算在CSDN、cnblogs等博客平台上写的,不过个人觉得界面设计、页面效果比较low,不符合我的审美观<( ̄▽ ̄)>。也有考虑过在简书上面写,因为界面看着简约大方上档次的赶脚,平时也看到挺多简书里的技术文章。不过,简书首页写着“一个基于内容分析的社区”,也就是啥啥啥文章都有,不只是技术博客,额,给我感觉在上面写自己的技术博客不是很正宗...…

Jekyll Github继续阅读