美文网首页@IT·互联网程序员iOS Developer
手把手教你自定义流水布局写特效

手把手教你自定义流水布局写特效

作者: mkb2 | 来源:发表于2016-09-03 17:36 被阅读647次
    最终效果

    步骤思路
    1.写出基本的colletionView,让他进行水平滑动,一个高度只能显示一个cell
    2.了解基本的UICollectionViewLayoutAttributes属性,让他可以改变一些cell的属性,例如aphelia,和缩小和放大
    3.了解一个方法,当我们rect改变的时候,会判断是否刷新cell的布局
    4.判断哪个cell离colleview的中点最近,了解一个target函数
    4.1使用刚才rect方法,调用super方法,获取的是计算好饿cell的中心点,所以知道了他的中心点,如果使用的是self,那么会在调用一次,比较麻烦
    4.2 计算collviontView的中心点值(注意,不能像过去的那个计算 过去:偏移量(手松开的哪一个可)+宽度的一般),此时拿到的偏移量事不准的,因为我们刺客还有速度,应该拿到最后的偏移量,也就是propertyTargetPoint,将要去哪里!!!
    4.3,要知道手松开的那一刻,的offSet是不准的,应该获取最终的,将要去的位置,所以传递rect是不能下传递,而是将来结束的时候,rect,用x最难判断,就是propertyPointOffset.x
    4.4 遍历布局属性,获取那个距离最短,所有的都偏移这段多
    4.5 明确,其实最后的所有的偏移量,= 最小的间距 + 目标偏移量!但是“最小的间距”可能是正负数!!
    5.给cell的初始化和结束设置一个sectionInset
    6.了解prepare函数的使用


    1.写出基本的colletionView,让他进行水平滑动,一个高度只能显示一个cell

    直接去写一个colletionView放在vc上就好,保证colletionView上只有一行,设置数据源和代理方法,布局对象直接使用系统的流水布局

    控制器成为了代理方法和数据源方法,用extension来写代理和数据源方法,突然红色,一脸懵逼,后来才发现,原来是在这里,没有写数据源必要的方法,写完方法就好了 通过系统的写出这个样式
    2.了解基本的UICollectionViewLayoutAttributes属性

    每个cell都有尺寸,位置和aplha值等等,其实每个cell都有一个UICollectionViewLayoutAttributes属性,这里有cell所有的信息,包括刚刚说的三个,还有很多~~

        /**
         
              解释类:UICollectionViewLayoutAttributes 
         
             *  每一个cell的尺寸,位置等各种属性都对应这个一个UICollectionViewLayoutAttributes,这个类中含有很多自己的属性,改变这个属性,那么cell的大小或者位置也会发生变化.还有`transform `可以更改他的形变等~
             
             @property (nonatomic) CGRect frame;
             @property (nonatomic) CGPoint center;
             @property (nonatomic) CGSize size;
             @property (nonatomic) CATransform3D transform3D;
             @property (nonatomic) CGRect bounds NS_AVAILABLE_IOS(7_0);
             @property (nonatomic) CGAffineTransform transform NS_AVAILABLE_IOS(7_0);
             @property (nonatomic) CGFloat alpha;
             @property (nonatomic) NSInteger zIndex; // default is 0
             @property (nonatomic, getter=isHidden) BOOL hidden; // As an optimization, UICollectionView might not create a view for items whose hidden attribute is YES
             @property (nonatomic, strong) NSIndexPath *indexPath;
             在这里还能拿到collectionView这个控件
             */
    

    想写出本文的目标效果,我们必须要重写布局,自定义一个布局,但是继承那个比较好,有两个选择,

    • 一个是抽象类UICollectionViewLayout,继承这个,那么我们滑动都不行,要重写的东西特别多,特别费事,不建议
    • 还有就是UICollectionViewFlowLayout这个苹果已经给我计算好了很多的东西,可以拿来就用,改改基本的某个属性就行
    2.1 自定义一个layout布局SFLayout

    直接在项目中将系统的替换,效果应该是一样的~

    2.2 func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?这个函数是干啥的?

    刚才都说了,每个cell都有一个UICollectionViewLayoutAttributes属性,这个函数的意思是,在rect范围之内的cell的属性放到一个数组中,传递出来,一定要调用super,然后用一个数组承接,为什么用super?因为super是流水布局,返回来的数组是计算好的,可以微调使用

        /**
         打印出当前rect之内的所有cell的“布局属性”- UICollectionViewLayoutAttributes(返回的是一个数组)
         */
        override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            //获得super已经计算好的尺寸
            let array = super.layoutAttributesForElementsInRect(rect)
            
            //对于每个属性进行尺寸,位置的微调
            for index in 0 ..< array!.count
            {
                let  abc:UICollectionViewLayoutAttributes  = array![index]
    // 1.改变allah值
          //      abc.alpha = 0.2
    
    //2.改变了大小
    //            let scale:CGFloat = CGFloat(arc4random_uniform(345))/345.0
    //            abc.transform = CGAffineTransformMakeScale(scale, scale)   
            }
            return array
    
    abc.alpha = 0.2 改变了大小-随机变化
    3.了解一个方法,当我们rect改变的时候,会判断是否刷新cell的布局
    3.1 我们看到的这些item都没有变化~

    我们滑动的时候,item一直没有发生变化,但是我想调用
    layoutAttributesForElementsInRect(默认之调用一次)刷新一下内部,如何处理?

    重写一个方法,只有滑动colletionView,rect发生了变化,就会调用布局属性方法

        /**
         当collectionView显示的rect发生了额变化,询问一下,是否要去刷新所在cell的layoutAttribute,返回true,调用layoutAttributesForElementsInRect刷新
         */
        override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
            return true
        }
    
    3.2 如何让放大的倍数会根据距离来变化?

    这个相对来说有点不好理解,不过多看两遍就好了~

    结构图

    1.要注意蓝色的是frame,也就是rect
    2.绿色的是contentSize我们现在的size肯定是大于frame的
    3.黑色的永远在frame的中点,挺好了,是frame的中点,随着content offset的改变(绿色的view一直前进或者后退),黑线还在frame 的中点,但是他在绿色的view的x值一直发生变化
    4.红色的是cell
    5.黄色的是cell的中心


    思路解析
    1.黑色线的位置如何计算?
    黑线.x = contentOffset.x + frame.size.width*0.5
    2.黄线如何计算?
    attir.center.x
    3.如何就算黄线和黑线的间距?
    用间距绝对值就行 abs(黑线.x - atria.center.x)
    4.如何根据绝对值改变cell的大小?
    比例 = 间距绝对值/frame.width ,除以宽度,是随便取得数据,可以是任意的,但是一定要大于间距的绝对值,我们要比例一定是(0,1)之间的,但是随着间距的变大,那么比例越来越大啊,咋整,只要用 真实比例 =(1-比例)就行,也可以用1.2- 比例这个随意,看具体的效果就好了,根据你们的要求,看看到底调到几

        /**
         打印出当前rect之内的所有cell的“布局属性”- UICollectionViewLayoutAttributes(返回的是一个数组)
         */
        override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            //获得super已经计算好的尺寸
            let array = super.layoutAttributesForElementsInRect(rect)
            
            //对于每个属性进行尺寸,位置的微调
            for index in 0 ..< array!.count
            {
                let  abc:UICollectionViewLayoutAttributes  = array![index]
                //1.获取collctionView最中间的线的X值
                let collectionViewX:CGFloat = (collectionView?.contentOffset.x)! + CGFloat((collectionView?.frame.width)!)*0.5
               //2.获取cell的中心点位置
                let cellX = abc.center.x
               let scale =  abs(cellX - collectionViewX)/(collectionView?.frame.width)!
                let trueScale = 1-scale
                abc.transform = CGAffineTransformMakeScale(trueScale, trueScale)
                
            }
            return array
        }
    
    随着远近改变大小
    4.判断哪个cell离colleview的中点最近

    刚才写完了如何将cell按照比例放大和缩小,还有就是如何放置到中间


    思路
    1.重写过去cell停止应该在什么位置的函数
    2.找出rect中那个一个cell里黑线最近
    3.获取那个cell里黑线最近的距离,可能是正数也可能是负数
    4.让所有的cell都微调 合适的间距,最终让那个特定的cell在屏幕中间

    /**
         当结束后,cell应该停止的位置
         
         :param: proposedContentOffset 打算停止的位置(通过速度计算出来的)
         :param: velocity              速度
         
         :returns: 最后停止的位置,你可能重新给数据了,就按照你给的位置停止,如果没有重写这个方法,那么就会返回proposedContentOffset这个位置(相对于当前的位置)
         */
        override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
            return CGPointZero
        }
    
    

    4.1.当结束后,cell应该停止的位置,正常cell停止的位置是
    proposedContentOffset,如果你重写了,那么cell停止后的位置就是你返回的位置

    返回的是CGPointZero

    4.2 找出rect中那个一个cell里黑线最近
    求间距,还用刚才的那个公式用间距绝对值就行 abs(黑线.x - attri.center.x)行不行,为什么?
    答案:不行,公式中获取的cell的中心点是可控的,我们知道,那一时刻,必须的,但是为什么在本函数中,我们认为他不行,不是准确的值?,因为我们快速滑动,然后松手,他还有速度,根据惯性,他还要滑动一会,所以不能取此时此刻的黄线(cell的中点),所以我们的公式是 本函数返回值 = proposedContentOffset .x + 黄线和黑线最近的间距(可能正也可能负)


    首先在本方法中,应该获取到rect中的cell的那些属性(layoutAttributesForElementsInRect),如何获取那个数组?
    调用super.layoutAttributesForElementsInRect方法,可以轻松获取的是计算好的cell的中心点。
    如果使用的是self.layoutAttributesForElementsInRect,获取的数据是经过我们计算的,我们此时要拿到未经过计算的attir数组

           //获取将要显示rect里面的cell属性
            let attrs = super.layoutAttributesForElementsInRect(rect)
    

    获取完了数组,我们拿数组中的对象和中线比较一下,看看那个是最近的,做一个记录,然后让所有的返回都是本函数返回值 = proposedContentOffset .x + 黄线和黑线最近的间距(可能正也可能负)

        /**
         当结束后,cell应该停止的位置
         
         :param: proposedContentOffset 打算停止的位置(通过速度计算出来的)
         :param: velocity              速度
         
         :returns: 最后停止的位置,你可能重新给数据了,就按照你给的位置停止,如果没有重写这个方法,那么就会返回proposedContentOffset这个位置(相对于当前的位置)
         */
        override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    //        print(proposedContentOffset)
            
            //1.获取所有的布局属性,调用super的方法获取
            //获取将要显示的rect值
            let rX:CGFloat = proposedContentOffset.x
            let rect = CGRectMake(rX, 0, (collectionView?.frame.width)!, (collectionView?.frame.height)!)
           //获取将要显示rect里面的cell属性
            let attrs = super.layoutAttributesForElementsInRect(rect)
            
            //计算colletionView中间哪个线的x
            let centX:CGFloat = proposedContentOffset.x + (collectionView?.frame.width)!*0.5
            
            //保存最小的间距
            var margin = MAXFLOAT
            //2.遍历属性,获取最小的间距
            for  index  in 0 ..< (attrs?.count)! {
                let att = attrs![index]
                let op = Float(att.center.x - centX)
                if abs(margin) > abs(op)
                {
                    margin = Float(att.center.x - centX)
                }
            }
            
            
            //所有的cell都要偏移量 = proposedContentOffset.x + marign(margin可能是正负)
            
            var currentOffset = proposedContentOffset
            currentOffset.x += CGFloat(margin)
    
            return currentOffset
        }
    

    4.3 计算collviontView的中心点值(注意,不能像过去的那个计算 过去:偏移量(手松开的哪一个可)+宽度的一般),此时拿到的偏移量事不准的,因为我们刺客还有速度,应该拿到最后的偏移量,也就是propertyTargetPoint,将要去哪里!!!
    4.3,要知道手松开的那一刻,的offSet是不准的,应该获取最终的,将要去的位置,所以传递rect是不能下传递,而是将来结束的时候,rect,用x最难判断,就是propertyPointOffset.x
    4.4 遍历布局属性,获取那个距离最短,所有的都偏移这段多
    4.5 明确,其实最后的所有的偏移量,= 最小的间距 + 目标偏移量!但是“最小的间距”可能是正负数!!


    5.给cell的初始化和结束设置一个sectionInset ,了解prepare函数的使用

    基本都讲完了,但是刚刚启动程序的时候,0号cell里左边太近了,不好看,我们想第一个cell和最后一个cell都在屏幕中间,怎么办?
    给他sectionInset设置数据就好了,组间距

    最终效果
        /**
         用来做布局的初始化,不建议在init中调用,因为那时候的colletionView = nil
         */
        override func prepareLayout() {
            super.prepareLayout()
            //设置sectionInset
            let magin:CGFloat =  ((collectionView?.frame.width)! - itemSize.width) * 0.5
            sectionInset = UIEdgeInsetsMake(0, magin, 0, magin)
        }
    
    
    6.自顶一个有imageView的cell就好了~

    同xib加载的,所以注册的时候有点不同,是这样的

    
            //注册cell
           collectionView.registerNib(UINib.init(nibName: "SFImageCell", bundle: nil),
                                      forCellWithReuseIdentifier: sfCellIdent)
    
    最终效果

    demo地址

    相关文章

      网友评论

        本文标题:手把手教你自定义流水布局写特效

        本文链接:https://www.haomeiwen.com/subject/tmakettx.html