前言
阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
本系列将对一些著名的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的使用。阅读这个类库的实现方式也让我受益匪浅,我也会在今后继续用这种方式阅读和仿写其它的著名类库,希望大家多多支持。
文章中难免有错误有不足,希望大家多多指正。
网友评论