系统的tableView是只须要配置几个代理方法, 就能够实现cell的左右侧滑菜单的. 通常会被用来做为编辑,删除等使用. 可是虽然在使用上挺方便的. 不过系统提供的的样式局限性很大, 就像QQ的侧滑样式, 只能显示字符而且动画效果很单一. 不过, 咱们实际开发中会遇到的可能并不只仅是这么简单, 多是上面图片显示的这样本节中就分享给朋友们吧, 也许不久的开发中你就会遇到相似的需求了, 那就再好不过了.javascript
本节中, 咱们将实现自定义的tableViewCell的侧滑菜单, 而且实现四种常见的动画效果, 同时简书炫酷的侧滑效果也一并实现了.java
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];
}复制代码
按钮
能够展现多种样式的内容, 好比只展现图片, 只展现文字, 能够同时展现图片和文字, 不过图片在上方文字在下方. 因此咱们首先自定义一下咱们须要的按钮. 新建一个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;
}复制代码
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);
}
}复制代码
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;
}复制代码
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;复制代码
ZJDrawerController
, 那么咱们很清楚, 相似的咱们还须要一些属性来帮助咱们处理在手势滑动的过程当中的滑动方向的判断和滑动的距离的获取.// 滑动操做的类型
typedef NS_ENUM(NSUInteger, ZJSwipeOperation) {
ZJSwipeOperationNone,
ZJSwipeOperationOpenLeft,
ZJSwipeOperationCloseLeft,
ZJSwipeOperationOpenRight,
ZJSwipeOperationCloseRight
};
// 记录手势开始的时候`overlayerContentView`的x
CGFloat _beginContentViewX;
// 记录手势开始的时候`snapView`的x
CGFloat _beginSnapViewX;
// 记录手势开始的时候手指的位置, 便于处理手指松开的时候判断滑动了多远,是否完成滑动
CGFloat _beginX;复制代码
case UIGestureRecognizerStateBegan: {
// 设置左右侧滑菜单和截图
[self setupSwipeViewWithSwipeVelocityX:velocityX];
// 记录初始数据
_beginX = locationX;
_beginSnapViewX = self.snapView.zj_x;
_beginContentViewX = self.overlayerContentView.zj_x;
self.swipeOperation = ZJSwipeOperationNone;
}复制代码
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复制代码
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];复制代码
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];
}
}复制代码
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];
}
}
}复制代码
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];
}
}
}复制代码
ZJSwipeViewAnimatedStyle
这个枚举中, 定义了四种动画类型, 其中的三种和咱们实现ZJDrawerController
的三种动画方式彻底相同, 第四种模仿简书的动画的代码须要一点点的数学能力去理解, 这里即不在说起了, 请读者直接参考源码, 实现相应的四种动画效果.- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer == self.panGesture) {
CGPoint transion = [self.panGesture translationInView:self];
return transion.y == 0; // 是不是上下滑动
}
}复制代码
if (gestureRecognizer == self.tapGesture) { // 全部的cell公用这一个tapGesture
if (self.overlayerContentView) {
return YES;
}
else {
return NO;
}
}复制代码
- (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;
}复制代码
ZJSwipeTableViewCell
来替代系统本来的侧滑效果了, 固然和咱们以前实现的抽屉菜单同样, 你还能够本身实现各类须要的炫酷的动画效果. 我相信充满想象力的你必定实现的比笔者这里的要更炫酷和强大.注意:
这是书籍内容中的一个章节, 做为试读文章, 应该已经算书中涉及到的demo中有难度的实现效果了. 从这一节试读章节能够看出, 书中的全部demo实现的难度都不大.同时你也能够参考全部demo的源码来判断每一节的实现难度, 从而总体评估这种难度的书籍是否须要去阅读, 同时判断个人写做风格是否适合你阅读. 关于书籍的更多说明在这里, 请仔细评估.git