iOS 卡片式控件:QiCardView

前言:因項目中需求骡和,需要做一個卡片式控件。故QiCardView誕生了相寇。

首先慰于,先來看一下QiCardView的效果圖:

從命名來看,QiCardView唤衫,顧名思義婆赠,是一個可定制的卡片式UI控件。
從設(shè)計來看佳励,QiCardView仿照UITableView的設(shè)計休里,支持cell復(fù)用,節(jié)省了資源赃承。

話不多說妙黍,先來看下整體架構(gòu)~

一、QiCardView整體架構(gòu)設(shè)計

架構(gòu)層面仿照了UITableView的設(shè)計瞧剖,采用了cell復(fù)用策略拭嫁。
在此基礎(chǔ)上,融入了一些手勢操作抓于,更加富有交互性做粤。

上架構(gòu)圖:

兩個主類分別為QiCardViewQiCardViewCell。(仿照UITableView+UITableViewCell的設(shè)計)

  • QiCardView下有兩個代理:QiCardViewDataSource捉撮、QiCardViewDelegate怕品。(與UITableView的代理方法類似)
  • QiCardViewCell下有一個代理:QiCardViewCellDelegate。(這個代理可以不關(guān)心巾遭,主要目的是輔助QiCardView里的一些處理邏輯)

二肉康、如何自定義使用QiCardView?

Cell自定義很簡單灼舍,只要新建一個類(例如:QiCardViewItemCell)繼承自QiCardViewCell即可迎罗。

在Controller中,基本使用上幾乎與UITableView類似片仿。

  • 初始化CardView方法:

在上Demo之前,先介紹幾個可以自定義的配置屬性:

屬性 類型 介紹
visibleCount NSInteger 卡片Cell可見數(shù)量(默認3)尤辱。因為有復(fù)用策略砂豌,所以即實際創(chuàng)建的Cell數(shù)量厢岂。
lineSpacing CGFloat 行間距(默認10.0,可自行計算scale比例來做間距)
interitemSpacing CGFloat 列間距(默認10.0阳距,可自行計算scale比例來做間距)
maxAngle CGFloat 側(cè)滑最大角度(默認15°)塔粒。值約小越容易劃出,越大約不好劃出筐摘。
maxRemoveDistance CGFloat 最大移除距離(默認屏幕的1/4)卒茬,滑動距離不夠時歸位。
isAlpha CGFloat cell是否需要漸變透明度咖熟。(默認YES)
- (void)initViews {
    
    _cardView = [[QiCardView alloc] initWithFrame:CGRectMake(25.0, 150.0, self.view.frame.size.width - 50.0, 420.0)];
    _cardView.backgroundColor = [UIColor lightGrayColor];//!< 為了指出carddView的區(qū)域圃酵,指明背景色
    _cardView.dataSource = self;
    _cardView.delegate = self;
    _cardView.visibleCount = 4;
    _cardView.lineSpacing = 15.0;
    _cardView.interitemSpacing = 10.0;
    _cardView.maxAngle = 10.0;
    _cardView.isAlpha = YES;
    _cardView.maxRemoveDistance = 100.0;
    _cardView.layer.cornerRadius = 10.0;
    [_cardView registerClass:[QiCardItemCell class] forCellReuseIdentifier:qiCardCellId];
    [self.view addSubview:_cardView];
}
  • 數(shù)據(jù)源:QiCardViewDataSource
    首先controller要遵守協(xié)議:<QiCardViewDataSource>
#pragma mark - QiCardViewDataSource

- (QiCardItemCell *)cardView:(QiCardView *)cardView cellForRowAtIndex:(NSInteger)index {
    
    QiCardItemCell *cell = [cardView dequeueReusableCellWithIdentifier:qiCardCellId];
    cell.cellData = _cellItems[index];
    //...

    return cell;
}

- (NSInteger)numberOfCountInCardView:(UITableView *)cardView {
    return _cellItems.count;
}
  • 代理:QiCardViewDelegate
    還是首先controller需要遵守協(xié)議:<QiCardViewDelegate>
#pragma mark - QiCardViewDelegate

- (void)cardView:(QiCardView *)cardView didRemoveLastCell:(QiCardViewCell *)cell forRowAtIndex:(NSInteger)index {
    [cardView reloadDataAnimated:YES];
}

- (void)cardView:(QiCardView *)cardView didRemoveCell:(QiCardViewCell *)cell forRowAtIndex:(NSInteger)index {
    NSLog(@"didRemoveCell forRowAtIndex = %ld", index);
}

- (void)cardView:(QiCardView *)cardView didDisplayCell:(QiCardViewCell *)cell forRowAtIndex:(NSInteger)index {
    
    NSLog(@"didDisplayCell forRowAtIndex = %ld", index);
}

- (void)cardView:(QiCardView *)cardView didMoveCell:(QiCardViewCell *)cell forMovePoint:(CGPoint)point {
    NSLog(@"move point = %@", NSStringFromCGPoint(point));
}

三馍管、QiCardView的技術(shù)點

3.1 QiCardViewCell復(fù)用策略實現(xiàn)

  1. 注冊Cell:
    兩種方式:registerNib郭赐、registerClass。 很簡單确沸。
/** 注冊cell方法一:Nib */
- (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier {
    self.nib = nib;
    self.identifier = identifier;
}

/** 注冊cell方法二:Class */
- (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier {
    self.cellClass = cellClass;
    self.identifier = identifier;
}
  1. 獲取緩存Cell策略:
    先看緩存池中是否有相同ID(identifier)的Cell捌锭,有的話,直接返回Cell罗捎。
    若緩存池中沒有观谦,那么就new一個新的Cell啦~
/** 獲取緩存cell */
- (__kindof QiCardViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier {
    for (QiCardViewCell *cell in self.reusableCells) {
        if ([cell.reuseIdentifier isEqualToString:identifier]) {
            [self.reusableCells removeObject:cell];
            
            return cell;
        }
    }
    if (self.nib) {
        QiCardViewCell *cell = [[self.nib instantiateWithOwner:nil options:nil] lastObject];
        cell.reuseIdentifier = identifier;
        
        return cell;
    } else if (self.cellClass) { // 注冊class
        QiCardViewCell *cell = [[self.cellClass alloc] initWithReuseIdentifier:identifier];
        cell.reuseIdentifier = identifier;
        
        return cell;
    }
    return nil;
}
  1. 當cell走DidRemoveFromSuperView方法時,把cell加入緩存池桨菜。
- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {

    //...    

    [self.reusableCells addObject:cell];

    //...
}

3.2 cell重疊透明度漸變的實現(xiàn)

  1. 首先聲明了一個靜態(tài)變量:moveCount來記錄翻卡次數(shù)豁状。(以便將cell的index與卡片的index邏輯關(guān)聯(lián))
static int moveCount = 0;//!< 記錄翻頁次數(shù)
  1. 邏輯:每個CardCell 在 “remove from super view” 的時候 moveCount+1。
#pragma mark - QiCardViewCellDelagate

- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {
    
    moveCount++;
    
    //....
}
  1. 邏輯:在reload方法中雷激,需要將moveCount置0替蔬。(很好理解,reload時屎暇,moveCount需要重新開始計算)
- (void)reloadDataAnimated:(BOOL)animated {
    
    moveCount = 0;//!< 漸變需要
    
   //...
}
  1. 關(guān)鍵邏輯:在每次更新布局時承桥,設(shè)置每個Cell的漸變值(即alpha
/** 更新布局(動畫) */
- (void)updateLayoutVisibleCellsWithAnimated:(BOOL)animated {
    
    //...

    if (_isAlpha) {
        BOOL isTopCell = (i == _currentIndex - moveCount);
        if (isTopCell) {//!< 如果是最上面的Cell就透明度為1
            cell.alpha = 1.0;
         } else {
            cell.alpha = (i + 1.9) * 1.0/self.visibleCells.count;
        }
    }

    //...

}

3.3 手勢操作實現(xiàn)

這部分主要是手勢+動畫。
細節(jié)比較多根悼,小而雜凶异。
詳細邏輯,請見源碼挤巡。

#define Qi_SNAPSHOTVIEW_TAG 999
#define Qi_DEGREES_TO_RADIANS(angle) (angle / 180.0 * M_PI)

- (void)panGestureRecognizer:(UIPanGestureRecognizer*)pan {
    
    switch (pan.state) {
        case UIGestureRecognizerStateBegan:
            self.currentPoint = CGPointZero;
            break;
        case UIGestureRecognizerStateChanged: {
            CGPoint movePoint = [pan translationInView:pan.view];
            self.currentPoint = CGPointMake(self.currentPoint.x + movePoint.x , self.currentPoint.y + movePoint.y);
            
            CGFloat moveScale = self.currentPoint.x / self.maxRemoveDistance;
            if (ABS(moveScale) > 1.0) {
                moveScale = (moveScale > 0) ? 1.0 : -1.0;
            }
            CGFloat angle = Qi_DEGREES_TO_RADIANS(self.maxAngle) * moveScale;
            CGAffineTransform transRotation = CGAffineTransformMakeRotation(angle);
            self.transform = CGAffineTransformTranslate(transRotation, self.currentPoint.x, self.currentPoint.y);
            
            if (self.cell_delegate && [self.cell_delegate respondsToSelector:@selector(cardViewCellDidMoveFromSuperView:forMovePoint:)]) {
                [self.cell_delegate cardViewCellDidMoveFromSuperView:self forMovePoint:self.currentPoint];
            }
            [pan setTranslation:CGPointZero inView:pan.view];
        }
            break;
        case UIGestureRecognizerStateEnded:
            [self didPanStateEnded];
            break;
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateFailed:
            [self restoreCellLocation];
            break;
        default:
            break;
    }
}

// 手勢結(jié)束操作(不考慮上下位移)
- (void)didPanStateEnded {
    // 右滑移除
    if (self.currentPoint.x > self.maxRemoveDistance) {
        __block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO];
        snapshotView.transform = self.transform;
        [self.superview.superview addSubview:snapshotView];
        [self didCellRemoveFromSuperview];
        
        CGFloat endCenterX = [UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width * 1.5;
        [UIView animateWithDuration:Qi_DefaultDuration animations:^{
            CGPoint center = self.center;
            center.x = endCenterX;
            snapshotView.center = center;
        } completion:^(BOOL finished) {
            [snapshotView removeFromSuperview];
        }];
    }
    // 左滑移除
    else if (self.currentPoint.x < -self.maxRemoveDistance) {
        __block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO];
        snapshotView.transform = self.transform;
        [self.superview.superview addSubview:snapshotView];
        [self didCellRemoveFromSuperview];
        
        CGFloat endCenterX = -([UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width);
        [UIView animateWithDuration:Qi_DefaultDuration animations:^{
            CGPoint center = self.center;
            center.x = endCenterX;
            snapshotView.center = center;
        } completion:^(BOOL finished) {
            [snapshotView removeFromSuperview];
        }];
    }
    // 滑動距離不夠歸位
    else {
        [self restoreCellLocation];
    }
}

// 還原卡片位置
- (void)restoreCellLocation {
    
    [UIView animateWithDuration:Qi_SpringDuration delay:0
         usingSpringWithDamping:Qi_SpringWithDamping
          initialSpringVelocity:Qi_SpringVelocity
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
                         self.transform = CGAffineTransformIdentity;
                     } completion:nil];
}

// 卡片移除處理
- (void)didCellRemoveFromSuperview {
    
    self.transform = CGAffineTransformIdentity;
    [self removeFromSuperview];
    if ([self.cell_delegate respondsToSelector:@selector(cardViewCellDidRemoveFromSuperView:)]) {
        [self.cell_delegate cardViewCellDidRemoveFromSuperView:self];
    }
}

- (void)removeFromSuperviewSwipe:(QiCardCellSwipeDirection)direction {
    
    switch (direction) {
        case QiCardCellSwipeDirectionLeft: {
            [self removeFromSuperviewLeft];
        }
            break;
        case QiCardCellSwipeDirectionRight: {
            [self removeFromSuperviewRight];
        }
            break;
        default:
            break;
    }
}

// 向左邊移除動畫
- (void)removeFromSuperviewLeft {
    __block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO];
    [self.superview.superview addSubview:snapshotView];
    [self didCellRemoveFromSuperview];
    
    CGAffineTransform transRotation = CGAffineTransformMakeRotation(-Qi_DEGREES_TO_RADIANS(self.maxAngle));
    CGAffineTransform transform = CGAffineTransformTranslate(transRotation, 0, self.frame.size.height/4.0);
    CGFloat endCenterX = -([UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width);
    [UIView animateWithDuration:Qi_DefaultDuration animations:^{
        CGPoint center = self.center;
        center.x = endCenterX;
        snapshotView.center = center;
        snapshotView.transform = transform;
    } completion:^(BOOL finished) {
        [snapshotView removeFromSuperview];
    }];
}

// 向右邊移除動畫
- (void)removeFromSuperviewRight {
    __block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO];
    snapshotView.frame = self.frame;
    [self.superview.superview addSubview:snapshotView];
    [self didCellRemoveFromSuperview];
    
    CGAffineTransform transRotation = CGAffineTransformMakeRotation(Qi_DEGREES_TO_RADIANS(self.maxAngle));
    CGAffineTransform transform = CGAffineTransformTranslate(transRotation, 0, self.frame.size.height/4.0);
    CGFloat endCenterX = [UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width * 1.5;
    [UIView animateWithDuration:Qi_DefaultDuration animations:^{
        CGPoint center = self.center;
        center.x = endCenterX;
        snapshotView.center = center;
        snapshotView.transform = transform;
    } completion:^(BOOL finished) {
        [snapshotView removeFromSuperview];
    }];
}

四剩彬、未來可能優(yōu)化的點

  • 設(shè)計層面:如果將手勢操作融入QiCardView中,將QiCardViewCell變成純粹的Cell矿卑,會不會更好喉恋。(思考中)
  • 應(yīng)用層面:目前只支持一個ID的Cell重用,未來渴望拓展成多個ID的Cell都可重用。(PS:因為只存了一個ID轻黑,后續(xù)考慮存數(shù)組糊肤,以及對應(yīng)的Cell緩存池數(shù)組。以此猜測UITableView的內(nèi)部實現(xiàn)氓鄙。)

源碼:QiCardView源碼馆揉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抖拦,隨后出現(xiàn)的幾起案子升酣,更是在濱河造成了極大的恐慌,老刑警劉巖态罪,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件噩茄,死亡現(xiàn)場離奇詭異,居然都是意外死亡向臀,警方通過查閱死者的電腦和手機巢墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來券膀,“玉大人君纫,你說我怎么就攤上這事∏郾颍” “怎么了蓄髓?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舒帮。 經(jīng)常有香客問我会喝,道長,這世上最難降的妖魔是什么玩郊? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任肢执,我火速辦了婚禮,結(jié)果婚禮上译红,老公的妹妹穿的比我還像新娘预茄。我一直安慰自己,他們只是感情好侦厚,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布耻陕。 她就那樣靜靜地躺著,像睡著了一般刨沦。 火紅的嫁衣襯著肌膚如雪诗宣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天想诅,我揣著相機與錄音召庞,去河邊找鬼岛心。 笑死,一個胖子當著我的面吹牛篮灼,可吹牛的內(nèi)容都是我干的鹉梨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼穿稳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了晌坤?” 一聲冷哼從身側(cè)響起逢艘,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎骤菠,沒想到半個月后它改,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡商乎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年央拖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹉戚。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡鲜戒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抹凳,到底是詐尸還是另有隱情遏餐,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布赢底,位于F島的核電站失都,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏幸冻。R本人自食惡果不足惜粹庞,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洽损。 院中可真熱鬧庞溜,春花似錦、人聲如沸趁啸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽不傅。三九已至旅掂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間访娶,已是汗流浹背商虐。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人秘车。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓典勇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叮趴。 傳聞我的和親對象是個殘疾皇子割笙,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,089評論 1 32
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件眯亦、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,024評論 4 62
  • 2018.12.20感悟 昨晚寫作業(yè)的時候伤溉。寫累了和我聊會兒天。提起了她和他同學的事妻率。(每次說同學的事情我都認真聽...
    串珠小噠人閱讀 88評論 0 0
  • “知識付費”近幾年火起來之后,一批批想急切提升自己的朋友始終處于“買買買”的狀態(tài)孤里,看似購買的是知識伏伯,其實購買的是一...
    ym_baob閱讀 379評論 2 0
  • 記軍旅藝術(shù)家劉大為 《藝海軍魂》
    張咚咚閱讀 118評論 0 0