级别: ★★☆☆☆
标签:「iOS」「卡片式控件」「QiCardView」
做者: MrLiuQ
审校: QiShare团队php
前言:因项目中需求,须要作一个卡片式控件。故QiCardView诞生了。git
首先,先来看一下QiCardView的效果图:github
从命名来看,QiCardView,顾名思义,是一个可定制的卡片式UI控件。 从设计来看,QiCardView仿照UITableView的设计,支持cell复用,节省了资源。数组
话很少说,先来看下总体架构~缓存
架构层面仿照了UITableView
的设计,采用了cell复用策略。 在此基础上,融入了一些手势操做,更加富有交互性。微信
上架构图:架构
两个主类分别为QiCardView
与QiCardViewCell
。(仿照UITableView
+UITableViewCell
的设计)ide
QiCardView
下有两个代理:QiCardViewDataSource
、QiCardViewDelegate
。(与UITableView的代理方法相似)QiCardViewCell
下有一个代理:QiCardViewCellDelegate
。(这个代理能够不关心,主要目的是辅助QiCardView里的一些处理逻辑)Cell自定义很简单,只要新建一个类(例如:QiCardViewItemCell
)继承自QiCardViewCell
便可。布局
在Controller中,基本使用上几乎与UITableView
相似。优化
CardView
方法:在上Demo以前,先介绍几个能够自定义的配置属性:
属性 | 类型 | 介绍 |
---|---|---|
visibleCount | NSInteger | 卡片Cell可见数量(默认3)。由于有复用策略,因此即实际建立的Cell数量。 |
lineSpacing | CGFloat | 行间距(默认10.0,可自行计算scale比例来作间距) |
interitemSpacing | CGFloat | 列间距(默认10.0,可自行计算scale比例来作间距) |
maxAngle | CGFloat | 侧滑最大角度(默认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的区域,指明背景色
_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];
}
复制代码
QiCardViewDataSource
:<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
:<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));
}
复制代码
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;
}
复制代码
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;
}
复制代码
DidRemoveFromSuperView
方法时,把cell加入缓存池。- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {
//...
[self.reusableCells addObject:cell];
//...
}
复制代码
moveCount
来记录翻卡次数。(以便将cell的index与卡片的index逻辑关联)static int moveCount = 0;//!< 记录翻页次数
复制代码
#pragma mark - QiCardViewCellDelagate
- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {
moveCount++;
//....
}
复制代码
0
。(很好理解,reload时,moveCount须要从新开始计算)- (void)reloadDataAnimated:(BOOL)animated {
moveCount = 0;//!< 渐变须要
//...
}
复制代码
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;
}
}
//...
}
复制代码
这部分主要是手势+动画。
细节比较多,小而杂。
详细逻辑,请见源码。
#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;
}
}
// 手势结束操做(不考虑上下位移)
- (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];
}];
}
复制代码
源码:QiCardView源码。
小编微信:可加并拉入《QiShare技术交流群》。
关注咱们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)