前言:因項目中需求骡和,需要做一個卡片式控件。故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)圖:
兩個主類分別為QiCardView
與QiCardViewCell
。(仿照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)
- 注冊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;
}
- 獲取緩存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;
}
- 當cell走
DidRemoveFromSuperView
方法時,把cell加入緩存池桨菜。
- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {
//...
[self.reusableCells addObject:cell];
//...
}
3.2 cell重疊透明度漸變的實現(xiàn)
- 首先聲明了一個靜態(tài)變量:
moveCount
來記錄翻卡次數(shù)豁状。(以便將cell的index與卡片的index邏輯關(guān)聯(lián))
static int moveCount = 0;//!< 記錄翻頁次數(shù)
- 邏輯:每個CardCell 在 “remove from super view” 的時候 moveCount+1。
#pragma mark - QiCardViewCellDelagate
- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {
moveCount++;
//....
}
- 邏輯:在reload方法中雷激,需要將moveCount置
0
替蔬。(很好理解,reload時屎暇,moveCount需要重新開始計算)
- (void)reloadDataAnimated:(BOOL)animated {
moveCount = 0;//!< 漸變需要
//...
}
- 關(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源碼馆揉。