iOS新浪新闻首页卡片滚动特效实现浅谈。iOS 用UICollectionView实现各种神奇效果。

店家项目:找到了,采用了之特效,已发表以App
Store,欢迎下载查看。

前言

iOS里之UI控件其实远非几单,界面基本就是是绕那么几只控件灵活展开,最难以的应属UICollectionView了,因为其的可定制化程度最高,最灵,值得我们去研究一番

Leader非常受全然这个特效,说如果当我们app中运用,问我力所能及免可知促成,我同样看这个特效,卧槽,涉及了众多技术点,实现难度很大。如果是上下一心做特效还吓,模仿别人的特效,做的粗疏没什么,如果一旦举行得好,就得考虑森细节。身啊丈夫,怎么能够说十分,自己为好配置时,偷偷做,经过了几乎天之紧奋战,终于得了,开森。
会不辱使命这特效,感谢其,感谢他,感谢还是支持我之人头。

目录

  • ** UICollectionView的骨干用**
  • ** 自定义布局整体思路**
  • 兑现瀑布流
  • ** 每页多单Cell的档次滚动布局**
  • 实现CoverFlow效果
  • 轮转卡片
  • 模仿今日头长实现Cell重排
  • iOS9就此系统性能实现Cell重排
  • iOS10晚UICollectionView的优化和预加载

进去正题

事先看下新浪的特效(版本 v6.6.0)

新浪新闻首页卡片滚动特效

先期分析一下以此滚动特效,有3种植方式控制滚动:

1、1单是指上下滑动整个表格,中间的滚动视图会跟着一起步。
2、1单凡是左右滑中间的轮转视图,滚动视图会快速滑动。
3、1独凡是左右晃动手机,根据角度不同,滚动视图跟着一起动。

需要缓解许多题目:

1、中间的滚动视图是均等块一样片移动的,停止时距离中间最近底卡片会自动滑动到中,居中对同。
2、中间的轮转视图在滑的时节发现卡是折叠在并的,中间的以上层,其他组成部分以下层,根据距离中间位置的远近来区别上下层。
3、中间的滚动视图在滑的下发现卡片大小不一致,中间的卓绝酷,越靠近边框越聊。
4、中间的滚视图在滑行的时刻发现滑动的离和卡移动的去并无是变成正比,而是按持续变更之加速度移动的。
5、中间的滚视图滑到横边缘时视图透明度改变。
6、循环滚动方案的实现
7、上下滑动表格时,中间的滚动视图要随着一块儿滑动,上滑时于左移动,下滑时于右侧走。
8、左右晃动手机时,中间的轮转视图要随着一起滑动,向左晃动时卡片向左移动,向右晃动时卡片向右侧走。
9、需要确保刚才提到的3种控制方式互不干扰。

老题材大概就是是这几个,当然还有不少不怎么题目,就不一一列下了。

1.UICollectionView的主导用

  • 创建UICollectionFlowLayout对象
  • 根据flowlayout创建UICollectionView的对象
  • 报cell或头尾部视图
  • 恪守协议

这就是说,说干就干

好这特效之前,首先是设选取实现工具,实现工具的选好重点,相当给方向,方向选对了,才会要上很劲。
兑现工具发出3种植:

1、自定义滚动视图
2、UIScrollView
3、UICollectionView

第一种,使用于定义滚动视图,亲自去Github上面找了无数关于从定义滚动视图的demo,觉得难度不是一般的难以,而是二班的难呐,想在新浪的iOS开发水准应该为没有那大,肯定不是这种方案,就Pass了第一种植。
这就是说看下第二种植,使用UIScrollView,苹果好动手的UIScrollView,可以分页是好分页,但是只能整个视图分页,无法完成区域分页,看了许多之Demo,实现效益啊殊强人意,无奈Pass了亚栽。
这就是说第三栽也?使用UICollectionView,在网上找到了众方可供灵感的Demo,感觉上是有效的,然后伟哥给了仿佛之Demo告诉我这种方案的方向。可以的,那便因故这种方法抓起。

创建UICollectionFlowLayout对象
  • itemSize cell的大小

  • scrollDirection 滚动方向

  • minimumInteritemSpacing
    与滚动方向相反的个别单item之间极小离,默认为10,它会依据你设定的这个价值长item的轻重,来查看能一行最多克推广多少只item,再管确定的item铺满总行,左右不留给余,每个item之间的偏离可能会见于之值大

  • minimumLineSpacing
    滚动方向上item的区间,如果您的凡水平滚动,留心水平距离变化误设为minimumInteritemSpacing,笔者就达到过这样的铮铮,默认为10,在闹规律的item之间严格遵照设定的离开来,但是于混乱的item之间,就是每行item的极小去,如下图绿色箭头所示

    图片 1
    minimumLineSpacing

  • sectionInset
    每组的内切距,默认都为0,item会根据其来铺满总行,如下图所示

    图片 2
    sectionInset

  • headerReferenceSize footerReferenceSize
    每组的脑瓜儿视图和尾巴视图的尺寸

  • sectionHeadersPinToVisibleBounds
    sectionHeadersPinToVisibleBounds iOS 9.0
    以后新特性,滚动时,每组的头部视图或尾部视图是否稳定在头要尾部

  • 苟您的layout对象属性不是永恒的,你要贯彻UICollectionViewDelegateFlowLayout谋里相应属性的数据源方法

    let layout = UICollectionViewFlowLayout()
    let margin: CGFloat = 8
    let itemW = (view.bounds.width - margin * 4) / 3
    let itemH = itemW
    // 每个item的大小
    layout.itemSize = CGSize(width: itemW, height: itemH)
    // 最小行间距
    layout.minimumLineSpacing = margin
    // 最小item之间的距离
    layout.minimumInteritemSpacing = margin
    // 每组item的边缘切距
    layout.sectionInset = UIEdgeInsetsMake(0, margin, 0, margin) 
    // 滚动方向
    layout.scrollDirection = .vertical  
    // 创建collection
    let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
    // 遵守协议    
    collectionView.delegate = self
    collectionView.dataSource = sel

确定好了可行性之后便全力以赴了。

题目1:中间的滚视图是平块一样块移动的,停止时去中间最近之卡片会自动滑动到中间,居中对一头。
UICollectionView有只布局类叫做UICollectionViewFlowLayout,可以以里面实现卡片效果,通过计算每个可见Cell到中间位置,来计量出偏移量,得出CollectionView最终留的位置,实现活动滑动到中路,居中对共同之功力。实现以下方法:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity

题材2:中间的滚视图在滑的时节发现卡片是折叠在一道的,中间的于上层,其他一些于下层,根据距离中间位置的远近来分上下层。
看UICollectionViewFlowLayout布局文件,通过测算可见Cell到基本点位置,来规定Cell的职位,达到叠加的功能。上下层的概念则是经z轴的例外来贯彻,搞了玩之对象或会见重复清楚一些,三维坐标系中发生x轴,y轴,z轴,通过z轴确定视图的上下层次。实现以下措施:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds

题材3:中间的滚动视图在滑的时段发现卡大小不一致,中间的极其特别,越靠近边框越聊。
当UICollectionViewFlowLayout布局文件被化解,设置好垂直缩放系数,根据距离的远近进行放大处理,距离中间的放权最酷。

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

题材4:中间的滚视图在滑行的下发现滑动的偏离及卡移动的相距并无是成为正比,而是遵循持续转变之加以速度移动的。
以UICollectionViewFlowLayout布局文件被解决,根据可见Cell到核心的两样位置设置移动距离,不同的相距下不同之加速度。

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

问题5:中间的轮转视图滑到横边缘时视图透明度改变。
于UICollectionViewFlowLayout布局文件中化解,通过计算边缘距离,来动态改变透明度。

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

题目6:循环滚动方案的贯彻
关于循环滚动方案,网上的说法层出不穷。我原先为打过巡回滚动,中间呈现3只,使用5只视图进行巡回滚动。但这种方案计算比较复杂,功力不够的伴儿还是不要尝试的好。还有同种植方案是可以被她滚动到最终一格的时,跳反到第一格,这种方案来接触未是大通畅。最终我以了中等定位法实现了巡回滚动,需要用下的方法:

[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];

题目7:上下滑动表格时,中间的滚动视图要随着一起滑动,上滑动时为左移动,下滑时于右侧走。
需在报表类吃滚动代理方中装滚动时设置UICollectionView的ContentOffset偏移量,然后于scrollViewDidEndDragging和scrollViewDidEndDecelerating两单法子中装置滚动结束晚,UICollectionView的ContentOffset偏移量,需要计算到屏幕中间位置的偏移量进行偏移。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

题材8:左右晃动手机时,中间的滚视图要跟着一块儿滑动,向左晃动时卡片向左移动,向右晃动时卡片向右侧走。
忽悠手机,涉及到硬件支持。iPhone手机内安排了各种传感器,其中一个纵是重力影响。通过打听重力影响,知道了加速器,使用加速器来支配晃动偏移量,发现效果不好,抖动特别厉害,在网上也远非专门好的事例说明。那么改用陀螺仪,苹果之陀螺仪做的雅精致,效果也甚好,非常安静,但是还要发现了初题材,就是活动停止不了。最终以加速器和陀螺仪的相当之下,终于完满的解决了问题。

- (void)startGyroUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMGyroHandler)handler
- (void)startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMAccelerometerHandler)handler

题目9:需要保证刚才提到的3种植控制方法互不干扰。
关联到基准判断,不仅用在表的UIScrollView代理方法做决定,还要当UICollection代理方做决定,保证功效互不干扰。

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

完了以后的效应如下:

自我开的卡片滚动特效

苟是仍新浪新闻的还简要一点,但是问题是,我们的出品连续会抓点花样,比如修改透明度,修改圆角呀,等等,所以实现的难度就多了众多。总之,兵来将挡水来土掩,完成就是了。

尾记:有人说服务端比客户端高,并非如此。闻道有先后,术业有专攻,不同之丁,Level也殊。做接口考虑好数额,考虑数据库结构,查询优化等等,这些说白了只还是工具、数学、逻辑思考、数据结构、算法,跟客户端差不多。客户端也只要考虑代码框架设计、网络框架设计、数据库设计、特效空间数学计算、逻辑处理、数据结构、算法等等。客户端做的胜的总人口,转至服务端也是分分钟搞定的从。

——————————-我是分割线——————————-

注册cell和头尾部视图
  • 使用registerClass:forCellWithReuseIdentifier:
    或者registerNib:forCellWithReuseIdentifier: 注册cell
  • 使用
    registerClass:forSupplementaryViewOfKind:withReuseIdentifier:或者
    registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
    注册头尾部视图,kind类型有 UICollectionElementKindSectionHeader
    UICollectionElementKindSectionFooter
  • 于定义头尾部视图必须继续UICollectionReusableView,其实UICollectionViewCell为是继往开来自她

    // 注册cell
    collectionView.register(BaseCollectionViewCell.self, forCellWithReuseIdentifier: baseCellID)

    // 注册头尾部视图,它们必须继承自UICollectionReuseView
    collectionView.register(UINib(nibName: "BaseHeaderAndFooterCollectionReusableView", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: baseReuseHeaderID)
    collectionView.register(UINib(nibName: "BaseHeaderAndFooterCollectionReusableView", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: baseReuseFooterID)

闪闪金服 – 杨毅辉

比如数据源协议
  • numberOfSections(in:)方里 返回组数
  • collectionView(_:numberOfItemsInSection) 返回各国组个数
  • collectionView(_:cellForItemAt:)
    collectionView(_:viewForSupplementaryElementOfKind:at:)
    编辑而的cell和头尾部视图

   func numberOfSections(in collectionView: UICollectionView) -> Int {
       return 3
   }

   func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
       return 10 + section * 3
   }

   func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
       let cell = collectionView.dequeueReusableCell(withReuseIdentifier: baseCellID, for: indexPath) as! BaseCollectionViewCell
       cell.cellIndex = indexPath.item
       return cell
   }

   // 头尾部的数据源协议
   func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
       if kind == UICollectionElementKindSectionHeader {
           let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: baseReuseHeaderID, for: indexPath) as! BaseHeaderAndFooterCollectionReusableView
           header.backgroundColor = .purple
           header.textLabel.text = "第 \(indexPath.section) 组的头部"
           return header
       }
       let footer = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: baseReuseFooterID, for: indexPath) as! BaseHeaderAndFooterCollectionReusableView
       footer.textLabel.text = "第 \(indexPath.section) 组的尾部"
       footer.backgroundColor = .lightGray
       return footer
   }

末了效果:

图片 3

基本用.png

2017-12-20

2.自定义布局整体思路

  • 让类继承UICollectionViewLayout或者UICollectionViewFlowLayout

  • prepare()方式里准备而用布局的信,这个方法会在率先不好布局以及reloadData()以及invalidateLayout()时会调用,对于那些无会见随视图滚动而变更之布局的靶子,都该当此地计算好,进行缓存

  • collectionViewContentSize里返回collectionViewcontentSize

  • 比方布局就范围变更如果实时移,在shouldInvalidateLayout(forBoundsChange:)函数里返回true

  • layoutAttributesForElements(in:)里返回布局数组,如果你的布局对象还早已缓存好了,也应当就回去跟layAttributes.frameRect会友的之区间内之靶子往往组,如下图所示

    图片 4

    回去屏幕范围外之对象.png

对于那些随滚动而改变的`item`,应该在这里进行重新计算,
记住,千万不要在这方法里调用
`UICollectionView`的`visibleCells`方法,因为这个范围内所有的cell还没确定;
  • 一旦想调整滚动的职位,例如为离开核心近日之cell居中,在targetContentOffset(forProposedContentOffset:withScrollingVelocity:)计里展开调整

  • 咦状态下用从定义UICollectionAttributes对象,首先问UICollectionViewcell何以非跟UITableViewCell同样,直接就是把布局搞定,非要多有一个UICollectionViewLayout目标,因为其再也扑朔迷离灵活,自定义程度高,那UICollectionViewcell举凡怎抱布局对象
    的, 通过
    apply(_:)本条方式来收获布局UICollectionAttributes对象,再冲它来布局,但UICollectionAttributes的性不多,例如我们想要一个锚点、一种颜色当她都是没有底,如果你用为此到这些额外属性传递给cell布局,就待由定义布局对象;

  • 怎么打定义UICollectionAttributes布局对象,首先以接近中加加你打定义的特性,由于布局时见面拷贝对象,需要遵循NSCoping协议,实现copy(with:)方法,UICollectionReusableView(Cell也是它的子类)欲实现apply(_:)方法,在iOS7其后,它会咬定你的布局对象是不是改变,来决定是否调用apply(_:)办法,如果你的自定义UICollectionAttributes里只有从定义的性能改变,而任何性质没有改动,它见面视为你这目标没转,你需要重新写
    isEqual艺术,来判定目标是否改变

  • 对于layoutAttributesForItem(at:)方式,它是匪会见主动调用的,只是被我们当布局方法prepare() 和layoutAttributesForElements(in:)里再接再厉调用它来收获layoutAttributes靶,但咱一般经过UICollectionViewLayoutAttributes(forCellWith:)法好创办

3.贯彻瀑布流

瀑布流是每个item宽度相同,高度不等的同等种之布局,
自定义一个继承UICollectionViewFlowLayout的接近,效果图如下

图片 5

瀑布流.png

概念基本性

    var cols = 4 // 列数   
    /// 布局frame数组
    fileprivate lazy var layoutAttributeArray: [UICollectionViewLayoutAttributes] = []   
    /// 每列的高度的数组
    fileprivate lazy var yArray: [CGFloat] = Array(repeating: self.sectionInset.top, count: self.cols)
     /// 最大高度
    fileprivate var maxHeight: CGFloat = 0

在prepare()方法里补充加我们用的布局属性,并盘算出ContentSize的无比充分惊人,重点是怎管每列的惊人存起来,接着极小之同一排继续排

/// 重写Prepare方法,准备我们要缓存的布局对象
override func prepare() {
    super.prepare()
    let itemW = (collectionView!.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(cols - 1)) / CGFloat(cols)

    let itemCount = collectionView!.numberOfItems(inSection: 0)

    // 最小高度的那一个的索引
    var minHeightIndex = 0

    // 从 layoutAttributeArray.count 开始,避免重复加载
    for j in layoutAttributeArray.count ..< itemCount {
        let indexPath = IndexPath(item: j, section: 0)
        let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        // item高度,从代理中获取
        let itemH = delegate?.waterFlowLayout(self, itemHeightAt: indexPath)

        // 找出最小高度的那一列
        let value = yArray.min()
        minHeightIndex = yArray.index(of: value!)! 
        var itemY = yArray[minHeightIndex]

        // 大于第一行的高度才相加
        if j >= cols {
            itemY += minimumInteritemSpacing
        }

        let itemX = sectionInset.left + (itemW + minimumInteritemSpacing) * CGFloat(minHeightIndex)

        attr.frame = CGRect(x: itemX, y: itemY, width: itemW, height: CGFloat(itemH!))
        layoutAttributeArray.append(attr)
        // 重新设置最小列高度
        yArray[minHeightIndex] = attr.frame.maxY
    }
    maxHeight = yArray.max()! + sectionInset.bottom
 }
  • 回collectionViewContentSize的分寸,记住这里宽度不克设置为0,如果安为0,在layoutAttributesForElements(in:)不可知是的归大小

    override var collectionViewContentSize: CGSize {
        return CGSize(width: collectionView!.bounds.width, height: maxHeight)
    }
  • 在layoutAttributesForElements返回布局对象数组

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // 找出相交的那些,别全部返回
        return layoutAttributeArray.filter { $0.frame.intersects(rect)}
    }

4.各级页多只Cell的程度方向滚动布局

若是每页的item很多,而且是程度方向滚动,item是同样排一排的散,这给咱们非常不好的感到,因为我们习惯是单排一行看之,原效力图如下

图片 6

水平滚动原效力图.png

咱们得改变成为仍程度方向排列的效果:

图片 7

用贯彻之成效图.png

实现方式以及瀑布流基本相仿,首先定义基本特性

    var cols = 4 // 列数,默认为4
    var line = 4  // 行数,默认为4

    /// contentSize的最大宽度
    fileprivate var maxWidth: CGFloat = 0

    /// 布局frame数组
    fileprivate lazy var layoutAttributeArray: [UICollectionViewLayoutAttributes] = []

以prepare()方法里上加我们用的布局属性,并盘算出ContentSize的极端要命幅面,重点就是是怎将岗位为算出来

/// 重写Prepare方法,准备我们要缓存的布局对象
override func prepare() {
    super.prepare()
    // 每个item的宽度
    let itemW = (collectionView!.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(cols - 1)) / CGFloat(cols)
    // 每个item的高度
    let itemH = (collectionView!.bounds.height - sectionInset.top - sectionInset.bottom - minimumLineSpacing * CGFloat(line - 1)) / CGFloat(line)

    // 求出对应的组数
    let sections = collectionView?.numberOfSections
    // 每个item所在组的 前面总的页数
    var prePageCount: Int = 0
    for i in 0..<sections! {
        // 每组的item的总的个数
        let itemCount = collectionView!.numberOfItems(inSection: i)
        for j in 0..<itemCount {
            let indexPath = IndexPath(item: j, section: i)
            let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)

            // item 在 这一组内处于第几页
            let page = j / (cols * line)
            // item 在每一页内是处于第几个
            let index = j % (cols * line)

            // item的y值
            let itemY = sectionInset.top + (itemH + minimumLineSpacing) * CGFloat(index / cols)

            // item的x值 为 左切距 + 前面页数宽度 + 在本页中的X值
            let itemX = sectionInset.left + CGFloat(prePageCount + page) * collectionView!.bounds.width + (itemW + minimumInteritemSpacing) * CGFloat(index % cols)

            attr.frame = CGRect(x: itemX, y: itemY, width: itemW, height: itemH)

            layoutAttributeArray.append(attr)
        }
        // 重置 PrePageCount
        prePageCount += (itemCount - 1) / (cols * line) + 1
    }
    // 最大宽度
    maxWidth = CGFloat(prePageCount) * collectionView!.bounds.width
}
  • 返回collectionViewContentSize的大小

    override var collectionViewContentSize: CGSize {
        return CGSize(width: maxWidth, height: 0)
    }
  • 以layoutAttributesForElements返回布局对象数组

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // 找出相交的那些,别全部返回
        return layoutAttributeArray.filter { $0.frame.intersects(rect)}
    }

5.实现CoverFlow效果

CoverFlow是一致种异常要命的封皮浏览效果,item的轻重缓急就滑动而缩放,滑动结束时,距中最近之一个item局中形,效果图如下

图片 8

coverFlow.gif

  • 坐item大小论范围变更而实时变化,在prepare()方法里算缓存已经无用,需要以layoutAttributesForElements(in:)里亲自计算来回到布局对象数组

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // 获取这个范围的布局数组
        let attributes = super.layoutAttributesForElements(in: rect)
        // 找到中心点
        let centerX = collectionView!.contentOffset.x + collectionView!.bounds.width / 2

        // 每个点根据距离中心点距离进行缩放
        attributes!.forEach({ (attr) in
            let pad = abs(centerX - attr.center.x)
            let scale = 1.8 - pad / collectionView!.bounds.width
            attr.transform = CGAffineTransform(scaleX: scale, y: scale)
        })
        return attributes
    }
  • 为滚动停止时,距中心近日item居中显示

    /// 重写滚动时停下的位置
    ///
    /// - Parameters:
    ///   - proposedContentOffset: 将要停止的点
    ///   - velocity: 滚动速度
    /// - Returns: 滚动停止的点
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var targetPoint = proposedContentOffset

        // 中心点
        let centerX = proposedContentOffset.x + collectionView!.bounds.width

        // 获取这个范围的布局数组
        let attributes = self.layoutAttributesForElements(in: CGRect(x: proposedContentOffset.x, y: proposedContentOffset.y, width: collectionView!.bounds.width, height: collectionView!.bounds.height))

        // 需要移动的最小距离
        var moveDistance: CGFloat = CGFloat(MAXFLOAT)
        // 遍历数组找出最小距离
        attributes!.forEach { (attr) in
            if abs(attr.center.x - centerX) < abs(moveDistance) {
                moveDistance = attr.center.x - centerX
            }
        }
        // 只有在ContentSize范围内,才进行移动
        if targetPoint.x > 0 && targetPoint.x < collectionViewContentSize.width - collectionView!.bounds.width {
            targetPoint.x += moveDistance
        }

        return targetPoint
    }
  • Bounds变化时,应该再次布局

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
  • 返回collectionViewContentSize的大小

    override var collectionViewContentSize: CGSize {        
        return CGSize(width:sectionInset.left + sectionInset.right + (CGFloat(collectionView!.numberOfItems(inSection: 0)) * (itemSize.width + minimumLineSpacing)) - minimumLineSpacing, height: 0)
    }
  • 为给中的cell不为拦住,我们需要拿它放最前边,在控制器中实现这些点子

    /// 把中间的cell带到最前面
    fileprivate func bringMiddleCellToFront() {
        let pointX = (collectionView.contentOffset.x + collectionView.bounds.width / 2)
        let point = CGPoint(x: pointX, y: collectionView.bounds.height / 2)
         // 找到中心点的indexPath
        let indexPath = collectionView.indexPathForItem(at: point)
        if let letIndexPath = indexPath {
            let cell = collectionView.cellForItem(at: letIndexPath)
            guard let letCell = cell else {
                return
            }
            // 把cell放到最前面
            collectionView.bringSubview(toFront: letCell)
        }
    }

    /// 第一次显示需要主动调用把中间的cell的放在最前面
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        bringMiddleCellToFront() 
    }
    /// 滚动时,每次调用这方法
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        bringMiddleCellToFront()
    }

6.贯彻轮转卡片效果

每当轮转卡片中,我们得为此到从定义UICollectionViewLayoutAttributes的自定义类,来改cell的锚点和positon,
从而改变cell的旋角度

图片 9

轮转卡片.gif

  • 先是由定义UICollectionViewLayoutAttributes类似,添加锚点属性,一定要遵照NSCoping商讨,
    因为不单纯于定义属性锚点实时移,还有自带的transform属性改变,可以免更写isEqual方法

/// 主要为了存储 anchorPoint,好在cell的apply(_:)方法中使用来旋转cell,因为UICollectionViewLayoutAttributes没有这个属性
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {

    var anchorPoint = CGPoint(x: 0.5, y: 0.5)

    /// 需要实现这个方法,collection View  实时布局时,会copy参数,确保自身的参数被copy
    override func copy(with zone: NSZone? = nil) -> Any {
        let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copy(with: zone) as! CircularCollectionViewLayoutAttributes
        copiedAttributes.anchorPoint = anchorPoint
        return copiedAttributes
    }
}
  • 每当起定义UICollectionViewLayout对象吃,自定义属性

  • 有n个item, 我们尽管使ContentSize的幅度为 item * n

  • 每个item的对立于上一个item的舞狮角度我们打定义一个也 anglePerItem

  • 当偏移量为
    0时,第一独item处于刚刚中间,偏移角度也0,当collectionView的偏移量最可怜时,最后一个item处于刚刚中间,偏移角度为0

    /// 每个item的大小
    let itemSize = CGSize(width: 133, height: 173)

    /// 属性数组
    var attributesList: [CircularCollectionViewLayoutAttributes] = []

    /// 设置半径,需要重新设置布局
    var radius: CGFloat = 500 {
        didSet {
            invalidateLayout()
        }
    }

    /// 每两个item 之间的角度,任意值
    var anglePerItem: CGFloat {
        return atan(itemSize.width / radius)  // atan反正切
    }

    /// 当collectionView滑到极端时,第 0个item的角度 (第0个开始是 0 度,  当滑到极端时, 最后一个是 0 度)
    var angleAtextreme: CGFloat {
        return collectionView!.numberOfItems(inSection: 0) > 0 ? -CGFloat(collectionView!.numberOfItems(inSection: 0) - 1) * anglePerItem : 0
    }

    /// 滑动时,第0个item的角度
    var angle: CGFloat {
        return angleAtextreme * collectionView!.contentOffset.x / (collectionViewContentSize.width - collectionView!.bounds.width)
    }
  • 重写prepare()方法,求来彼此对应之性数组attributesList

override func prepare() {
    super.prepare()

    // 整体布局是将每个item设置在屏幕中心,然后旋转 anglePerItem * i 度
    let centerX = collectionView!.contentOffset.x + collectionView!.bounds.width / 2.0
    // 锚点的y值,多增加了raidus的值
    let anchorPointY = ((itemSize.height / 2.0) + radius) / itemSize.height

    /// 不要计算所有的item,只计算在屏幕中的item,theta最大倾斜
    let theta = atan2(collectionView!.bounds.width / 2, radius + (itemSize.height / 2.0) - collectionView!.bounds.height / 2)
    var startIndex = 0
    var endIndex = collectionView!.numberOfItems(inSection: 0) - 1
    // 开始位置
    if angle < -theta {
        startIndex = Int(floor((-theta - angle) / anglePerItem))
    }
    // 结束为止
    endIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))

    if endIndex < startIndex {
        endIndex = 0
        startIndex = 0
    }
    //  startIndex...endIndex
    attributesList = (startIndex...endIndex).map({ (i) -> CircularCollectionViewLayoutAttributes in
        let attributes = CircularCollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
        attributes.size = self.itemSize
        // 设置居中
        attributes.center = CGPoint(x: centerX, y: collectionView!.bounds.midY)
        // 设置偏移角度
        attributes.transform = CGAffineTransform(rotationAngle: self.angle + anglePerItem * CGFloat(i))
        // 锚点,我们自定义的属性
        attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
        return attributes
    })
}
  • layoutAttributesForElements(in:)返回布局数组

    // 返回布局数组
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return attributesList
    }
  • 被滚动停止时,距中心最近item居中显示

     /// 重写滚动时停下的位置
    ///
    /// - Parameters:
    ///   - proposedContentOffset: 将要停止的点
    ///   - velocity: 滚动速度
    /// - Returns: 滚动停止的点
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        var finalContentOffset = proposedContentOffset

        // 每单位偏移量对应的偏移角度
        let factor = -angleAtextreme / (collectionViewContentSize.width - collectionView!.bounds.width)
        let proposedAngle = proposedContentOffset.x * factor

        // 大约偏移了多少个
        let ratio = proposedAngle / anglePerItem

        var multiplier: CGFloat

        // 往左滑动,让multiplier成为整个
        if velocity.x > 0 {
            multiplier = ceil(ratio)
        } else if (velocity.x < 0) {  // 往右滑动
            multiplier = floor(ratio)
        } else {
            multiplier = round(ratio)
        }

        finalContentOffset.x = multiplier * anglePerItem / factor

        return finalContentOffset

    }
  • Bounds变化时,应该还布局

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
  • 返回collectionViewContentSize的大小,为item * n

    override var collectionViewContentSize: CGSize {
        return CGSize(width: CGFloat(collectionView!.numberOfItems(inSection: 0)) * itemSize.width, height: collectionView!.bounds.height)
    }
  • 以我们从自然的cell中重写apply(_:)艺术,拿到layoutAttribute中的锚点进行布局

     override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)
        let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
        layer.anchorPoint = circularlayoutAttributes.anchorPoint
        layer.position.y = layer.position.y + (circularlayoutAttributes.anchorPoint.y - 0.5) * bounds.height
     }

7. 效仿今日条长条实现Cell重排

挪动cell在照片排版和新闻类的APP中较大,主要采用的是moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath)此办法进行交换,主要思路

  1. 于collectionView添加长按与拖拽手势,拖拽手势主要是点击进入编辑状态后,可拖拽cell直接开展置换,因为拖拽手势不比较增长论手势需要遵循一段时间才开,反应迅捷,快速滑动时,有多问题,需要注意

  2. 据悉手势的老三栽状态对cell进行操作,手势开始时对cell进行截图,并隐蔽起来cell,
    手势移动时,让截图继走,当到别的cell上经常,进行交换,手势结束时,移除截图,并于藏的初步cell显示

  3. 点击cell,移动她进行添加删减

要意义图如下:

图片 10

今头条.gif

  • 给collectionView添加手势

 /// collectionView的pan手势
    func panGestureRecognized(_ sender: UIPanGestureRecognizer) {
        guard self.isEdit else { return }
        handleGesture(sender)
    }

    /// collectionView的长按手势
    func longPressGestureRecognized(_ sender: UILongPressGestureRecognizer) {
        handleGesture(sender)
    }

    func handleGesture(_ sender: UIGestureRecognizer) {
        let senderState = sender.state
        // 手指在collectionView中的位置
        fingerLocation = sender.location(in: collectionView)
        // 手指按住位置对应的indexPath,可能为nil
        relocatedIndexPath = collectionView?.indexPathForItem(at: fingerLocation)

        switch senderState {
        case .began:
              //  根据relocatedIndexPath,找出cell,隐藏它,并截图
        case .changed:
             //  根据fingerLocation,移动cell,如果到达其它cell上,交换两个cell            
        case .ended:
            // 移除截图,并让开始cell,显示
            didEndDraging()            
  }

    /// 拖动结束,显示cell,并移除截图
    func didEndDraging() {
        ...
        UIView.animate(withDuration: 0.2, animations: { 
            self.snapshot!.center = cell!.center
            // 隐藏截图
            self.snapshot!.alpha = 0
            self.snapshot!.transform = .identity
            cell?.alpha = 1
        }) { (_) in
            self.snapshot!.removeFromSuperview()
            self.snapshot = nil
            self.originalIndexPath = nil
            self.relocatedIndexPath = nil
        }       
    }
  • 盖收手势后,隐藏截图的动画片时间设置了0.2秒,在动画还没有了时,可能又开启了别一个拖拽手势,我们需要给耽搁拽手势设置代理,并操纵是否启用其

    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        let sender = gestureRecognizer as! UIPanGestureRecognizer
        let trnslationPoint = sender.translation(in: collectionView)
        // 结束动画有时间,扫的手势很容易出问题,需要保证 snapshot == nil,才让它开始,
        // pan手势结束和开始可能会特别快,需要格外留心,
        // 为了保证pan手势不影响collectionView的竖直滑动,竖直方向偏移不让它开始
        if abs(trnslationPoint.x) > 0.2  && snapshot == nil {
            return true
        }
        return false
    }

现实详情参见示范代码

8. iOS9运系统自带属性进行重排

以iOS9 之后,苹果为collectionView推出了几乎独方法

beginInteractiveMovementForItem(at:)
updateInteractiveMovementTargetPosition(_ 🙂
endInteractiveMovement()
cancelInteractiveMovement()

季单办法分别吗发端相互、更新交互位置、结束交互、取消交互,跟方的同一为collectionView添加手势,在手势的老三种状态中,分别调用上面相应的季种植艺术,实现系统的collectionView(_:moveItemAt:to:)方,更新数据源,实现力量如下

图片 11

iOS9系统又排.gif

func handleLongGesture(_ gesture: UILongPressGestureRecognizer) {   
    switch(gesture.state) {

    case .began:
        guard let selectedIndexPath = self.collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
            break
        }
        // 开始交互
        collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
    case .changed:
        // 更新位置
        collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
    case .ended:
        // 结束交互
        collectionView.endInteractiveMovement()
    default:
        // 默认取消交互
        collectionView.cancelInteractiveMovement()
    }
}

/// 更新我们自己的数据源 
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    let number = dataNumbers.remove(at: sourceIndexPath.item)
    dataNumbers.insert(number, at: destinationIndexPath.item)
    }
  • 她的使十分简短,如果控制器是UICollectionViewController,不需要我们调用交互方式,就可以兑现拖拽cell了,只待以collectionView(_:moveItemAt:to:)方式里,更新数据源,如果我们想关闭拖拽功能,设置installsStandardGestureForInteractiveMovementfalse就行了

  • 拖拽时,当cell触及到屏幕边缘时她会自动滚动,在iOS10丁,如果设置了性能collectionveiw的isPagingEnabled属性为true,拖拽到屏幕边缘时见面翻页滚动,苹果的设计是要拖到边缘稍微停留一会儿才翻页,而休翻页滚动的单待cell触碰到边缘就会见立刻移动

iOS10晚UICollectionView的优化和预加载

为要UICollectionView的滑更通畅,官方进行了一部分优化,首先得明白cell的加载顺序是什么样的,是预先调用collectionView(_:cellForItemAt:)数据源方法,再调用collectionView(_:willDisplay:forItemAt:)来得cell,cell消失调用collectionView(_:didEndDisplaying:forItemAt:)方法

  • 在iOS9在先,只交了屏幕边缘才调用cellForItemAt方,调用了之后这便见面调用willDisplay
    但在iOS10中,willDisplay立即方式还是和以前一样,只在cell马上上屏幕的上才调用,
    cellForItemAt倒是提前了,它见面在相距屏幕边缘还有一段距离的时节就是调用,这样保证了咱们来双重多之工夫错开加载我们的资源

  • cell的生命周期延长,滑发生屏幕之后,它会保留一段时间,如果cell快速滑动时,却出人意料往相反方向快速滑动,这时她不见面调用cellForItemAt方式,而是一直调用willDisplay方法,如果是在cellForItemAt方里动态改变cell属性,就需要专注了,可能会见出现问题,因为这个主意从未会见调用,如果要想念在及时措施里改变cell,
    跟iOS9一样,可以设置collectionView的isPrefetchingEnabled为false

  • 要是collectionView每行有差不多列cell,在iOS9会整行整行的加载,而到了iOS10它们会一个一个底加载,保证了流畅性

  • 提供了预加载方法,collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath])
    ,在此处提前加载图片资源,注意这些资源一定要是是异步加载,仔细观察了生,屏幕来差不多个cell,它便大多加载多少只cell,
    但是自身发觉cellForItemAt方法竟然以滑行时为提前加载了如此多个cell,唯一不同之凡兑现了这些代理方,我们会于率先次无滑动显示时,collectionView(_:prefetchItemsAt:)会晤提早加载屏幕这么多之cell,而cellForItemAt唯有加载屏幕及显得的cell,不见面多加载屏幕外的cell

  • 撤回预加载方法collectionView(_:cancelPrefetchingForItemsAt:),它独自于速滑动还从未平息下来经常,突然朝相反方向迅速滑动调用,当它们调用时,程序吗基本不见面倒cellForItemAt方法,直接走willDisplaycell术显示cell

实现预加载,效果图如下:

图片 12

prefeching.gif

  • 守协议collectionView?.prefetchDataSource = self
  • 兑现预加载数据源协议

// 预加载,每次都会领先基本一页的数据
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.flatMap {
        URL(string: dataArray[$0.item])
    }
    // 开始下载
    ImagePrefetcher(urls: urls).start()
}

// 取消预加载,会在快速滑动还没停下来时,突然往相反方向快速滑动调用,当它调用, 程序也基本不会走cellForItemAt 方法, 直接走 willDisplaycell方法
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.flatMap {
        URL(string: dataArray[$0.item])
    }
    // 取消下载
    ImagePrefetcher(urls: urls).stop()
}

结语

纯属续续花了森时光毕竟写了了,但当挺值得的,感觉太深的即是即时控件太灵敏了

Demo地址

相关文章

Post Author: admin

发表评论

电子邮件地址不会被公开。 必填项已用*标注