系統的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