SWTableViewCell-源码分析与仿写(一)

作者: 潇潇潇潇潇潇潇 | 来源:发表于2016-08-25 11:26 被阅读1108次

    前言

    阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
    本系列将对一些著名的iOS开源类库进行深入阅读及分析,并仿写这些类库的基本实现,加深我们对底层实现的理解和认识,提升我们iOS开发的编程技能。

    SWTableViewCell

    SWTableViewCell是UITableViewCell的子类,它具有左右滑动显示操作菜单的功能。很多APP都有这个功能,比如微信列表页往左侧滑动显示操作菜单,可以删除或标为未读。我们看一下它的效果:


    SWTableViewCell的地址是:https://github.com/CEWendel/SWTableViewCell,为了更好理解它的实现过程,避免被一些细节干扰,仍然使用简单的早期版本,v0.1.1版,接下来我们看看它是如何实现的。

    实现原理

    SWTableViewCell是一个继承自UITableViewCell的自定义Cell,它上面放了一个UIScrollerView,这个滚动视图上放了Cell内容、左侧操作菜单和右侧操作菜单。正常情况下,显示cell内容,当往左侧滑动时,滚动视图往左移动,显示右侧的操作菜单,右滑同理。
    左、右操作菜单上放置一些操作按钮,由使用者配置,包括按钮的数量,样式,位置等。这些按钮的事件统一回调给使用者,由使用者指定具体实现。

    实现过程

    SWUtilityButtonView

    左右两侧的操作菜单类,管理操作按钮的布局、事件回调。
    在SWUtilityButtonView类中,有以下属性

    @property (nonatomic, strong) NSArray *utilityButtons;//存放操作按钮的数组
    @property (nonatomic) CGFloat utilityButtonWidth;//操作按钮的宽度
    @property (nonatomic, weak) SWTableViewCell *parentCell;//操作按钮所在的cell
    @property (nonatomic) SEL utilityButtonSelector;//操作按钮点击事件
    

    其中utilityButtonWidth表示操作按钮的宽度,默认是90。当操作菜单有过多的按钮时,该值将重新计算取均分值,避免按钮太多撑满整个cell。 utilityButtonSelector是操作按钮的点击事件,该事件不在SWUtilityButtonView处理,而是要传递到parentCell中,即操作按钮的点击事件传递到上层cell中。parentCell还有个作用,取得cell的高度给SWUtilityButtonView。
    计算每个操作按钮的实际宽度
    - (CGFloat)calculateUtilityButtonWidth {
    CGFloat buttonWidth = kUtilityButtonWidthDefault;
    if (buttonWidth * _utilityButtons.count > kUtilityButtonsWidthMax) {
    CGFloat buffer = (buttonWidth * _utilityButtons.count) - kUtilityButtonsWidthMax;
    buttonWidth -= (buffer / _utilityButtons.count);
    }
    return buttonWidth;
    }
    操作按钮在页面上布局,以及配置事件响应方,通过tag属性标识每个按钮
    - (void)populateUtilityButtons {
    NSUInteger utilityButtonsCounter = 0;
    for (UIButton *utilityButton in _utilityButtons) {
    CGFloat utilityButtonXCord = 0;
    if (utilityButtonsCounter >= 1) utilityButtonXCord = _utilityButtonWidth * utilityButtonsCounter;
    [utilityButton setFrame:CGRectMake(utilityButtonXCord, 0, _utilityButtonWidth, CGRectGetHeight(self.bounds))];
    [utilityButton setTag:utilityButtonsCounter];
    [utilityButton addTarget:_parentCell action:_utilityButtonSelector forControlEvents:UIControlEventTouchDown];
    [self addSubview: utilityButton];
    utilityButtonsCounter++;
    }
    }

    NSMutableArray+SWUtilityButtons

    可变数组的扩展,提供了生成指定样式的操作按钮的功能 。
    NSMutableArray+SWUtilityButtons类提供了两个初始化操作按钮的方法
    @interface NSMutableArray (SWUtilityButtons)

    - (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title;
    - (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon;
    
    @end
    

    其实很简单
    - (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.backgroundColor = color;
    [button setTitle:title forState:UIControlStateNormal];
    [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [self addObject:button];
    }
    需要注意的是SWUtilityButtonView初始化和调用过程有一定的顺序,不能搞反了。

        //1,初始化
        [scrollViewButtonViewRight setFrame:CGRectMake(CGRectGetWidth(self.bounds), 0, [self rightUtilityButtonsWidth], _height)];
        //2,添加到滚动视图上
        [self.cellScrollView addSubview:scrollViewButtonViewRight];
    
        //3,操作按钮布局与事件回调设置
        [scrollViewButtonViewLeft populateUtilityButtons];
        [scrollViewButtonViewRight populateUtilityButtons];
    

    SWTableViewCell

    滑动显示菜单cell,它统一管理操作菜单的生成、事件处理、响应回调等。
    cell上的滚动视图的初始化,它的contentSize是左侧操作菜单的加cell的宽度加右侧操作菜单的宽度

        UIScrollView *cellScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), _height)];
        cellScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.bounds) + [self utilityButtonsPadding], _height);
        cellScrollView.contentOffset = [self scrollViewContentOffset];
        cellScrollView.delegate = self;
        cellScrollView.showsHorizontalScrollIndicator = NO;
        cellScrollView.scrollsToTop = NO;
    

    将原来cell上的内容添加到滚动视图上

        UIView *contentViewParent = self;
        if (![NSStringFromClass([[self.subviews objectAtIndex:0] class]) isEqualToString:kTableViewCellContentView]) {
            // iOS 7
            contentViewParent = [self.subviews objectAtIndex:0];
        }
        NSArray *cellSubviews = [contentViewParent subviews];
        [self insertSubview:cellScrollView atIndex:0];
        for (UIView *subview in cellSubviews) {
            [self.scrollViewContentView addSubview:subview];
        }
    

    当左右滑动cell时,实际上是根据滑动范围控制显示相应左右侧操作菜单
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView.contentOffset.x > [self leftUtilityButtonsWidth]) {
    // 显示右侧操作菜单
    self.scrollViewButtonViewRight.frame = CGRectMake(scrollView.contentOffset.x + (CGRectGetWidth(self.bounds) - [self rightUtilityButtonsWidth]), 0.0f, [self rightUtilityButtonsWidth], _height);
    } else {
    // 显示左侧操作菜单
    self.scrollViewButtonViewLeft.frame = CGRectMake(scrollView.contentOffset.x, 0.0f, [self leftUtilityButtonsWidth], _height);
    }
    }
    根据用户滑动的力度,显示相应的操作菜单。如果用户滑动范围不足操作菜单宽度的一半,cell回到正常状态,超过时,则滑动到相应的操作菜单
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    switch (_cellState) {
    case kCellStateCenter:
    if (velocity.x >= 0.5f) {//滑动力度
    [self scrollToRight:targetContentOffset];
    } else if (velocity.x <= -0.5f) {
    [self scrollToLeft:targetContentOffset];
    } else {
    CGFloat rightThreshold = [self utilityButtonsPadding] - ([self rightUtilityButtonsWidth] / 2);
    CGFloat leftThreshold = [self leftUtilityButtonsWidth] / 2;
    if (targetContentOffset->x > rightThreshold)//滑动范围超过操作菜单宽度的一半,显示操作菜单栏
    [self scrollToRight:targetContentOffset];
    else if (targetContentOffset->x < leftThreshold)
    [self scrollToLeft:targetContentOffset];
    else
    [self scrollToCenter:targetContentOffset];
    }
    break;
    case kCellStateLeft:
    if (velocity.x >= 0.5f) {
    [self scrollToCenter:targetContentOffset];
    } else if (velocity.x <= -0.5f) {
    // No-op
    } else {
    if (targetContentOffset->x >= ([self utilityButtonsPadding] - [self rightUtilityButtonsWidth] / 2))
    [self scrollToRight:targetContentOffset];
    else if (targetContentOffset->x > [self leftUtilityButtonsWidth] / 2)
    [self scrollToCenter:targetContentOffset];
    else
    [self scrollToLeft:targetContentOffset];
    }
    break;
    case kCellStateRight:
    if (velocity.x >= 0.5f) {
    // No-op
    } else if (velocity.x <= -0.5f) {
    [self scrollToCenter:targetContentOffset];
    } else {
    if (targetContentOffset->x <= [self leftUtilityButtonsWidth] / 2)
    [self scrollToLeft:targetContentOffset];
    else if (targetContentOffset->x < ([self utilityButtonsPadding] - [self rightUtilityButtonsWidth] / 2))
    [self scrollToCenter:targetContentOffset];
    else
    [self scrollToRight:targetContentOffset];
    }
    break;
    default:
    break;
    }
    }
    操作按钮的响应事件传递到cell中,通过tag判断当前点击的按钮。
    - (void)rightUtilityButtonHandler:(id)sender {
    UIButton *utilityButton = (UIButton *)sender;
    NSInteger utilityButtonTag = [utilityButton tag];
    if ([_delegate respondsToSelector:@selector(swippableTableViewCell:didTriggerRightUtilityButtonWithIndex:)]) {
    [_delegate swippableTableViewCell:self didTriggerRightUtilityButtonWithIndex:utilityButtonTag];
    }
    }
    整个类库的实现过程大致就是这些,基本实现思路就是在scrollView上滑动显示菜单区。下面我们自己仿写一下这个类库,以加深我们对它内部实现的理解和掌握。为了简单起见,我们只实现基本的功能,一些细节都忽略掉了。

    仿写SWTableViewCell

    首先创建ZCJButtonView,存放操作按钮,设置这些按钮的回调,主要方法如下:
    - (id)initWithFrame:(CGRect)frame Buttons:(NSArray *)buttons parentCell:(ZCJTableViewCell *)parentCell buttonSelector:(SEL)buttonSelector{
    self = [super initWithFrame:frame];
    if (self) {
    _buttons = buttons;
    _parentCell = parentCell;
    _buttonSelector = buttonSelector;
    }
    return self;
    }

    //计算自身的总宽度
    - (CGFloat)getWidth {
        return KButtonWidthDefault * _buttons.count;
    }
    
    - (void)layoutButtons {
        NSInteger buttonCount = 1000;
        for (UIButton *button in _buttons) {
            button.frame = CGRectMake((buttonCount-1000)*KButtonWidthDefault, 0, KButtonWidthDefault, self.bounds.size.height);
            button.tag = buttonCount;
            [button addTarget:_parentCell action:_buttonSelector forControlEvents:UIControlEventTouchUpInside];
            [self addSubview:button];
            buttonCount++;
        }
    }
    

    NSMutableArray+ZCJButtons类,我们只写一个初始化操作按钮的方法
    - (void)addButtonWithBackgroundColor:(UIColor *)color withTitle:(NSString *)title {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    [btn setTitle:title forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [btn setBackgroundColor:color];
    [self addObject:btn];
    }
    ZCJTableViewCell简化了很多功能,这里只处理了右侧滑动,类的初始化方法如下
    - (void)initializar {
    //右侧操作视图初始化
    ZCJButtonView *rightButtonView = [[ZCJButtonView alloc] initWithButtons:_rightButtons parentCell:self buttonSelector:@selector(buttonAction:)];
    rightButtonView.frame = CGRectMake(self.bounds.size.width, 0, [_rightButtonView getWidth], _height);
    [rightButtonView layoutButtons];
    _rightButtonView = rightButtonView;

        //滚动视图初始化
        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, _height)];
        scrollView.contentSize = CGSizeMake(self.bounds.size.width + [rightButtonView getWidth], self.height);
        scrollView.delegate = self;
        scrollView.showsHorizontalScrollIndicator = NO;
        scrollView.scrollsToTop = NO;
        _cellScrollView = scrollView;
    
        [_cellScrollView addSubview:_rightButtonView];
    
        UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, _height)];
        contentView.backgroundColor = [UIColor whiteColor];
        [_cellScrollView addSubview:contentView];
        _cellContentView = contentView;
    
        //将原来cell上的内容添加到滚动视图上
        UIView *contentViewParent = self;
        if (![NSStringFromClass([[self.subviews objectAtIndex:0] class]) isEqualToString:kTableViewCellContentView]) {
            // iOS 7
            contentViewParent = [self.subviews objectAtIndex:0];
        }
        NSArray *cellSubviews = [contentViewParent subviews];
        [self insertSubview:_cellScrollView atIndex:0];
        for (UIView *subview in cellSubviews) {
            [_cellContentView addSubview:subview];
        }
    }
    

    滑动cell时,操作菜单栏开始慢慢出现

    -(void)scrollViewDidScroll:(UIScrollView *)scrollView {
        if (scrollView.contentOffset.x > 0) {
            self.rightButtonView.frame = CGRectMake(scrollView.contentOffset.x + self.bounds.size.width - [self.rightButtonView getWidth], 0, [self.rightButtonView getWidth], _height);
        }
    }
    

    快速滑动cell时完成显示操作菜单栏。当只少量滑动时,恢复到cell正常状态
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    switch (_state) {
    case ZCJCellStateCenter:
    if (velocity.x >= 0.5) {
    [self scrollToRight:targetContentOffset];
    }
    else {
    if (targetContentOffset->x >= [self.rightButtonView getWidth] / 2) {
    [self scrollToRight:targetContentOffset];
    }
    else {
    [self scrollToCenter:targetContentOffset];
    }
    }
    break;

            case ZCJCellStateRight:
                if (velocity.x >= 0.5) {
                }
                else if (velocity.x <= -0.5) {
                    [self scrollToCenter:targetContentOffset];
                }
                else {
                    if (targetContentOffset->x <= [self.rightButtonView getWidth] / 2) {
                        [self scrollToCenter:targetContentOffset];
                    }
                    else {
                        [self scrollToRight:targetContentOffset];
                    }
                }
                break;
            default:
                break;
        }
    }
    

    操作按钮的点击事件传递到cell上进行处理
    - (void)buttonAction:(id)sender {
    UIButton *btn = sender;
    NSInteger index = btn.tag - 1000;
    if ([self.delegate respondsToSelector:@selector(swippableTableViewCell:didTriggerRightButtonViewWithIndex:)]) {
    [self.delegate swippableTableViewCell:self didTriggerRightButtonViewWithIndex:index];
    }
    }
    仿写的ZCJTableViewCell源码在这里:https://github.com/superzcj/ZCJTableViewCell

    总结

    SWTableViewCell是一个很棒的自定义cell,它的实现给我们很多启发,在我们日常编写自定义view中有很多可以学习的地方,比如SEL事件往上层传递,scrollView的使用。阅读这个类库的实现方式也让我受益匪浅,我也会在今后继续用这种方式阅读和仿写其它的著名类库,希望大家多多支持。
    文章中难免有错误有不足,希望大家多多指正。

    相关文章

      网友评论

      本文标题:SWTableViewCell-源码分析与仿写(一)

      本文链接:https://www.haomeiwen.com/subject/gmaosttx.html