iOS 自定义卡片式控件:QiCardView

栏目: IOS · 发布时间: 5年前

内容简介:级别: ★☆☆☆☆标签:「iOS」「卡片式控件」「QiCardView」作者:MrLiuQ

级别: ★☆☆☆☆

标签:「iOS」「卡片式控件」「QiCardView」

作者:MrLiuQ

审校:QiShare团队

前言:因项目中需求,需要做一个卡片式控件。故QiCardView诞生了。

首先,先来看一下QiCardView的效果图:

iOS 自定义卡片式控件:QiCardView

从命名来看,QiCardView,顾名思义,是一个可定制的卡片式UI控件。 从设计来看,QiCardView仿照UITableView的设计,支持cell复用,节省了资源。

话不多说,先来看下整体架构~

一、QiCardView整体架构设计

架构层面仿照了 UITableView 的设计,采用了cell复用策略。 在此基础上,融入了一些手势操作,更加富有交互性。

上架构图:

iOS 自定义卡片式控件:QiCardView

两个主类分别为 QiCardViewQiCardViewCell 。(仿照 UITableView + UITableViewCell 的设计)

  • QiCardView 下有两个代理: QiCardViewDataSourceQiCardViewDelegate 。(与UITableView的代理方法类似)
  • QiCardViewCell 下有一个代理: QiCardViewCellDelegate 。(这个代理可以不关心,主要目的是辅助QiCardView里的一些处理逻辑)

二、如何自定义使用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

    首先controller要遵守协议: <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需要遵守协议: <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的技术点

3.1 QiCardViewCell复用策略实现

  1. 注册Cell:

    两种方式: registerNibregisterClass 。 很简单。

/** 注册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重叠透明度渐变的实现

  1. 首先声明了一个静态变量: moveCount 来记录翻卡次数。(以便将cell的index与卡片的index逻辑关联)
static int moveCount = 0;//!< 记录翻页次数
复制代码
  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. 关键逻辑:在每次更新布局时,设置每个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 手势操作实现

这部分主要是手势+动画。

细节比较多,小而杂。

详细逻辑,请见 源码

#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中,将QiCardViewCell变成纯粹的Cell,会不会更好。(思考中)
  • 应用层面:目前只支持一个ID的Cell重用,未来渴望拓展成多个ID的Cell都可重用。(PS:因为只存了一个ID,后续考虑存数组,以及对应的Cell缓存池数组。以此猜测UITableView的内部实现。)

源码: QiCardView源码

小编微信:可加并拉入《QiShare技术交流群》。

iOS 自定义卡片式控件:QiCardView

关注我们的途径有:

QiShare(简书)

QiShare(掘金)

QiShare(知乎)

QiShare(GitHub)

QiShare(CocoaChina)

QiShare(StackOverflow)

QiShare(微信公众号)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员的自我修养

程序员的自我修养

俞甲子、石凡、潘爱民 / 电子工业出版社 / 2009-4 / 65.00

这本书主要介绍系统软件的运行机制和原理,涉及在Windows和Linux两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。每个技术专题都配备了大量图、表和代码实例,力求将复杂的机制以简洁的形式表......一起来看看 《程序员的自我修养》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具