(iOS)能夠這樣來玩玩tableViewCell的滑動菜單

系統的tableView是隻須要配置幾個代理方法, 就能夠實現cell的左右側滑菜單的. 通常會被用來做爲編輯,刪除等使用. 可是雖然在使用上挺方便的. 不過系統提供的的樣式侷限性很大, 就像QQ的側滑樣式, 只能顯示字符而且動畫效果很單一. 不過, 咱們實際開發中會遇到的可能並不只僅是這麼簡單, 多是上面圖片顯示的這樣本節中就分享給朋友們吧, 也許不久的開發中你就會遇到相似的需求了, 那就再好不過了.javascript

本節中, 咱們將實現自定義的tableViewCell的側滑菜單, 而且實現四種常見的動畫效果, 同時簡書炫酷的側滑效果也一併實現了.java

這個看上去比較小的需求, 筆者最初嘗試實現的時候仍然是不知道如何下手去完成, 通過一段時間的考慮後纔有一些想法. 後來大概使用了兩種方式來實現. 由於在實現這個需求以前筆者本身實現過抽屜菜單的需求(咱們上一節中也已經實現了), 最初想到的就是在每個cell相似抽屜菜單同樣, 增長兩個左右的抽屜菜單, 而後打開和關閉就和咱們處理抽屜菜單同樣, 最終是順利的實現了這個需求. 用上去仍是比較方便. 後來再次回頭研究的時候, 想到了另一種比較方便的實現方法. 下面咱們就使用這種方法來實現了.

1. 首先咱們新建一個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];

}複製代碼
2. 由於咱們但願實現的滑動菜單中的按鈕能夠展現多種樣式的內容, 好比只展現圖片, 只展現文字, 能夠同時展現圖片和文字, 不過圖片在上方文字在下方. 因此咱們首先自定義一下咱們須要的按鈕. 新建一個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;
}複製代碼
3. 而後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);
    }
}複製代碼
4. 處理好了咱們的側滑菜單上的按鈕, 接下來須要處理咱們的側滑菜單了, 側滑菜單分爲左右菜單, 上面用來容納左右的按鈕, 因此咱們但願將這些按鈕的frame設置等工做所有交給側滑菜單來處理, 而不須要咱們在ZJSwipeTableViewCell裏面來完成. 因此新建一個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;
}複製代碼
5. 完成了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;複製代碼
6. 咱們以前完成了抽屜菜單ZJDrawerController, 那麼咱們很清楚, 相似的咱們還須要一些屬性來幫助咱們處理在手勢滑動的過程當中的滑動方向的判斷和滑動的距離的獲取.
// 滑動操做的類型
typedef NS_ENUM(NSUInteger, ZJSwipeOperation) {
    ZJSwipeOperationNone,
    ZJSwipeOperationOpenLeft,
    ZJSwipeOperationCloseLeft,
    ZJSwipeOperationOpenRight,
    ZJSwipeOperationCloseRight
};
// 記錄手勢開始的時候`overlayerContentView`的x
CGFloat _beginContentViewX;
// 記錄手勢開始的時候`snapView`的x
CGFloat _beginSnapViewX;
// 記錄手勢開始的時候手指的位置, 便於處理手指鬆開的時候判斷滑動了多遠,是否完成滑動
CGFloat _beginX;複製代碼
7. 咱們就能夠處理滑動手勢了, 在手勢處理的方法中, 咱們須要處理的是: 手勢開始的時候設置好左右側滑菜單和cell截圖而且記錄須要的初始數據, 在手指滑動狀態的時候, 咱們須要根據滑動操做的類型, 相應的改變滑動菜單的frame和切換動畫, 最後是在手指離開的時候, 咱們根據滑動的距離和離開時的滑動速度來判斷是否打開和關閉菜單. 手勢開始的狀態.
case UIGestureRecognizerStateBegan: {
  // 設置左右側滑菜單和截圖
  [self setupSwipeViewWithSwipeVelocityX:velocityX];
  // 記錄初始數據
  _beginX = locationX;
  _beginSnapViewX = self.snapView.zj_x;
  _beginContentViewX = self.overlayerContentView.zj_x;
  self.swipeOperation = ZJSwipeOperationNone;

}複製代碼
8. 設置左右側滑菜單和截圖, 咱們知道, 若是左右的swipeView沒有建立, 咱們首先須要建立他們, 這個時候咱們就須要獲取到swipeView上面須要顯示的按鈕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複製代碼
9. 能夠看到咱們上面定義的代理方法裏面須要的參數有tableView和indexPath, 那麼咱們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];複製代碼
10. 而後就能夠設置左右側滑菜單和截圖, 咱們將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];
    }   
}複製代碼
11. 接下來是處理手指滑動過程當中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];

        }

    }
}複製代碼
12. 最後是手指離開屏幕的時候, 咱們應該根據滾動的距離和手指離開時的速度來判斷這一次操做是否完成仍是返回操做前的狀態. 這裏就以關閉右邊菜單爲例. 其餘狀況相似的呢.
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];

      }
  }
}複製代碼
13. 關於咱們定義的ZJSwipeViewAnimatedStyle這個枚舉中, 定義了四種動畫類型, 其中的三種和咱們實現ZJDrawerController的三種動畫方式徹底相同, 第四種模仿簡書的動畫的代碼須要一點點的數學能力去理解, 這裏即不在說起了, 請讀者直接參考源碼, 實現相應的四種動畫效果.
14. 完成了上面的工做, 咱們就能夠寫測試代碼了, 在ViewController中添加tableView而後使用咱們的ZJSwipeTableViewCell, 實現對應的返回左右菜單按鈕的代理方法, 順利的話, 就能正常的運行了, 而後能夠左右側滑而且上面的按鈕顯示正常點擊也是正常的還有咱們實現的四種動畫效果. 看上去不錯. 不過問題就來了, 如今不能滾動tableView了, 由於咱們添加在cell上的手勢和系統的手勢發生了衝突, 因而咱們, 須要在咱們添加的panGesture的代理方法中判斷若是是準備上下滑動就不要開始手勢, 就不會和系統的手勢衝突了.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {

    if (gestureRecognizer == self.panGesture) {
        CGPoint transion = [self.panGesture translationInView:self];
        return transion.y == 0; // 是不是上下滑動
    }
 }複製代碼
15. 如今在運行項目tableView就能正常的滾動了, 可是如今咱們發如今開始滾動和點擊其餘地方的時候滑動菜單並不會自動關閉. 筆者這裏的處理方式是在ZJSwipeTableViewCell所在的tableView上面添加一個tap手勢, 當側滑菜單打開的時候, 點擊tableView就關閉滑動菜單,可是, 咱們要注意處理tap手勢和tableView點擊cell的手勢的衝突, 因此咱們在tap手勢的代理中判斷, 只有在滑動菜單打開的時候才能執行tap手勢.
if (gestureRecognizer == self.tapGesture) { // 全部的cell公用這一個tapGesture
        if (self.overlayerContentView) {
            return YES;
        }
        else {
            return NO;
        }
}複製代碼
16. 處理tableView開始滾動的時候關閉打開的滑動菜單, 筆者是經過kvo來監聽tableView手勢狀態的改變, 在手勢開始的時候就關閉滑動菜單. 同時由於tableView的重用機制, 咱們添加在cell上面的截圖和滑動菜單, 咱們應該在關閉完成的時候移除掉, 從而不影響咱們原來的cell的操做.
- (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;
}複製代碼

到這裏咱們實現的使用方便靈活的tableView側滑菜單就結束了, 那麼如今你就可使用咱們實現的這個ZJSwipeTableViewCell來替代系統本來的側滑效果了, 固然和咱們以前實現的抽屜菜單同樣, 你還能夠本身實現各類須要的炫酷的動畫效果. 我相信充滿想象力的你必定實現的比筆者這裏的要更炫酷和強大.

注意:
這是書籍內容中的一個章節, 做爲試讀文章, 應該已經算書中涉及到的demo中有難度的實現效果了. 從這一節試讀章節能夠看出, 書中的全部demo實現的難度都不大.同時你也能夠參考全部demo的源碼來判斷每一節的實現難度, 從而總體評估這種難度的書籍是否須要去閱讀, 同時判斷個人寫做風格是否適合你閱讀. 關於書籍的更多說明在這裏, 請仔細評估.git

相關文章
相關標籤/搜索