UICollectionView 固定行距列表左排: 来一个自定制 Layout

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

内容简介:一般我们是使用 UICollectionViewFlowLayout , 熟悉的格子视图。也可以自定制 UICollectionViewLayout ,对于每一个列表元素,想放哪就放哪。这种情况,系统的就不好直接拿来使了,需要自己定制一个 UICollectionViewLayout.

一般我们是使用 UICollectionViewFlowLayout , 熟悉的格子视图。也可以自定制 UICollectionViewLayout ,对于每一个列表元素,想放哪就放哪。

譬如: 固定行距列表左排

UICollectionView 固定行距列表左排: 来一个自定制 Layout

这种情况,系统的就不好直接拿来使了,需要自己定制一个 UICollectionViewLayout.

一般 new 一个 UICollectionViewLeftAlignedLayout, 继承自 UICollectionViewFlowLayout

通常要重写 UICollectionViewFlowLayout 的这两个方法,

layoutAttributesForElements(in:) , 这个方法需要提供,给定的矩形里面所有的格子的布局属性。给定的矩形区域,就是 UICollectioonView 的内容视图区域 contentSize.

layoutAttributesForItem(at:): 这个方法需要提供,格子视图需要的具体的布局信息。我们要重写这个方法,返回要求的 indexPath 位置上格子的布局属性。

有时候也要重写这个属性:

collectionViewContentSize , 一般我们是把内容区域的尺寸,作为计算属性处理的。他提供格子视图的内容区域的宽度与高度。格子视图的内容区域,不是格子视图的可见区域。

因为格子视图 UICollectionView,继承自 UIScrollView。 格子视图使用该属性,配置他作为可滑动视图 UIScrollView 的内容视图尺寸。

主要代码见如下:

其中辅助函数没有列出来,具体见文尾的 github repo.

class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {

// 这个函数没有做什么事情,主要是调用做事情的函数 layoutAttributesForItem,获取信息,提供出去
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var attributesCopy: [UICollectionViewLayoutAttributes] = []
        if let attributes = super.layoutAttributesForElements(in: rect) {
            attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })
        }
        for attributes in attributesCopy {
            if attributes.representedElementKind == nil {
                let indexpath = attributes.indexPath
                // 做事情的地方
                if let attr = layoutAttributesForItem(at: indexpath) {
                    attributes.frame = attr.frame
                }
            }
        }
        return attributesCopy
    }
    
// 这个函数里面,具体处理了固定行距列表左排的布局
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        
        if let currentItemAttributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes, let collection = collectionView {
            let sectionInset = evaluatedSectionInsetForItem(at: indexPath.section)
            let isFirstItemInSection = indexPath.item == 0
            let layoutWidth = collection.frame.width - sectionInset.left - sectionInset.right
            // 让每一行的第一个元素排头,分两种情况处理。这是第一种,这个 section 的第一个元素,自然是排头。
            guard !isFirstItemInSection else{
                currentItemAttributes.leftAlignFrame(with: sectionInset)
                return currentItemAttributes
            }
            let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)
            let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame ?? CGRect.zero
            let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
            let currentFrame = currentItemAttributes.frame
            let strecthedCurrentFrame = CGRect(x: sectionInset.left,
                                                    y: currentFrame.origin.y,
                                                    width: layoutWidth,
                                                    height: currentFrame.size.height)
            let isFirstItemInRow = !previousFrame.intersects(strecthedCurrentFrame)
            // 让每一行的第一个元素排头,分两种情况处理。这是第二种,这个 section 的其他的排头,算出来,就是:上一个格子在上一行,不在当前行,
            guard !isFirstItemInRow else{
                currentItemAttributes.leftAlignFrame(with: sectionInset)
                return currentItemAttributes
            }
            //  剩下的,简单了。统一处理掉。 剩下的格子都不是排头,与上一个固定间距完了。
            var frame = currentItemAttributes.frame
            frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacing(at: indexPath.section)
            currentItemAttributes.frame = frame
            return currentItemAttributes
            
        }
        return nil
    }
// ...
}

说一下 func layoutAttributesForItem(at indexPath: IndexPath) 的设计思路。

因为如果使用 UICollectionViewFlowLayout ,什么都不干,与上图的区别就一点。

每一行的元素个数一致,具体也一致,就是那些格子是居中的。毕竟 minimumInteritemSpacing 是最小行内间距的意思,不是固定的行内间距。

然后移一移,就好了。让每一行的第一个元素排头,每一行的其他元素与上一个元素固定间距,这就完了。

例子二: 固定行距列表右排

UICollectionView 固定行距列表左排: 来一个自定制 Layout

分析:看起来,右排就是把左边排列好的元素,推往右边。具体就是左边排列好的元素,每一行的元素都添加一个当前行的 OffsetX ,

OffsetX = CollectionView 的 frame.width - 左边排列好元素的最后一个 frame.maxX

这是另外一种情况。因为不能改一改左排的 layoutAttributesForItem 方法,就好。左排用的是 previous,没什么问题。

右排用 next , 对于每一个元素,找 next , 知道其 X + width > collectionView 的 width , OffsetX 就出来了。

采用 layoutAttributesForItem 找 next, 会有一个比较烦的递归,当前找 next, next 找 next.

其实第一种情况也是这样,当前找 previous, previous 找 previous.

区别在于苹果做了优化,next 找 next,因为我在 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 返回的尺寸宽度随机。

然后就乱套了。当前找 next 返回的随机的 size, 到了 next, 会返回另外随机的 size.

previous 找 previous, 就没有这方面的问题。

我采用的是算两遍,先左排,再基于左排的结果,添加 OffsetX , 就变右排了。

// 我用了锁,测试时,每秒刷新一个布局。次数多了,会出现 Mac 上多核心 CPU 异步绘制的结果不太好

let lock = NSLock()
// 因为要算两次,第一次就不能放在  func layoutAttributesForElements(in rect: CGRect)  方法,
// 我放在  func prepare() 方法,采用创建的 UICollectionViewLayoutAttributes
    override func prepare() {
        lock.lock()
        contentHeight = 0
        cache.removeAll()
        storedCellSize.removeAll()
        guard let collectionView = collectionView else {
            return
        }
        var currentXOffset:CGFloat = 0
        var nextXOffset:CGFloat = 0
        var currentYOffset:CGFloat = 0
        var nextYOffset:CGFloat = 0
        
        
        for section in 0..<collectionView.numberOfSections{
            let sectionInset = evaluatedSectionInsetForItem(at: section)
            nextXOffset = sectionInset.left
            nextYOffset += sectionInset.top
            let count = collectionView.numberOfItems(inSection: section)
            for item in 0..<count{
                currentXOffset = nextXOffset
                currentYOffset = nextYOffset
                let indexPath = IndexPath(item: item, section: section)
                // 采用创建的 UICollectionViewLayoutAttributes,不是访问系统的 super.layoutAttributesForItem(at:)
                let currentItemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                let currentIndexPathSize = queryItemSize(indexPath)
                let currentItemInNext = (currentXOffset + evaluatedMinimumInteritemSpacing(at: section) + currentIndexPathSize.width) > (collectionView.frame.width - sectionInset.right + 0.1)
                if currentItemInNext{
                    currentXOffset = sectionInset.left
                    currentYOffset += (currentIndexPathSize.height + evaluatedMinimumLineSpacing(at: section))
                    
                    nextXOffset = currentXOffset + (currentIndexPathSize.width + evaluatedMinimumInteritemSpacing(at: section))
                   
                    nextYOffset = currentYOffset
                }else{
                    nextXOffset += (currentIndexPathSize.width + evaluatedMinimumInteritemSpacing(at: section))
                }
                
                
                let frame = CGRect(origin: CGPoint(x: currentXOffset, y: currentYOffset), size: currentIndexPathSize)
                currentItemAttributes.frame = frame
                cache[indexPath] = currentItemAttributes
                contentHeight = max(contentHeight, frame.maxY)
            }
            nextYOffset = contentHeight
        }
        lock.unlock()
    }
    
    // 这个函数没有做什么事情,主要是调用做事情的函数 layoutAttributesForItem,获取信息,提供出去
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        var attributesCopy: [UICollectionViewLayoutAttributes] = []
        if let attributes = super.layoutAttributesForElements(in: rect) {
            attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })
        }
        for attributes in attributesCopy {
            if attributes.representedElementKind == nil {
                let indexpath = attributes.indexPath
                if let attr = layoutAttributesForItem(at: indexpath) {
                    attributes.frame = attr.frame
                }
            }
        }
        return attributesCopy
    }
    
    
    // 这个函数是第二次计算。把第一计算的结果左排,通过算出 OffsetX 添加上去,变成右排
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let collectionView = collectionView,let attribute = cache[indexPath] else {
            return nil
        }
        
        var offsetX: CGFloat = attribute.frame.maxX
        let criteria = collectionView.frame.width - evaluatedSectionInsetForItem(at: indexPath.section).right - 0.1
        var gap = criteria - offsetX
        var ip = indexPath
        let sectionCount = collectionView.numberOfItems(inSection: indexPath.section)
        while ip.item < sectionCount{
           // 通过 Y 值来比较,结果比较稳定。同一行嘛,Y 坐标,自然是一致的。
            var conditionSecond = false
            if let nextAttri = cache[ip.next]{
                conditionSecond = nextAttri.frame.minY != attribute.frame.minY
            }
            if (ip.item + 1) >= sectionCount || conditionSecond {
                gap = criteria - offsetX
                break
            }
            else{
                ip = ip.next
                offsetX += (evaluatedMinimumInteritemSpacing(at: indexPath.section) + cache[ip]!.frame.width)
               
            }
        }
        attribute.trailingAlignFrame(with: gap)
        return attribute
        
    }

这里用到了 prepare() 方法,当有布局操作的时候,就调用这个方法。

我们用这个时机,计算出提供给 collectionView 的尺寸和这些格子的位置。

: 例子 3 , 右边反着排

UICollectionView 固定行距列表左排: 来一个自定制 Layout

同例子一,把每行第一个元素,铺到最右端,其余的格子保持固定间距就好了。

代码如下:

// 这个函数没有做什么事情,主要是调用做事情的函数 layoutAttributesForItem,获取信息,提供出去
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        var attributesCopy: [UICollectionViewLayoutAttributes] = []
        if let attributes = super.layoutAttributesForElements(in: rect) {
            attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })
        }
        
        for attributes in attributesCopy {
            if attributes.representedElementKind == nil {
                let indexpath = attributes.indexPath
                if let attr = layoutAttributesForItem(at: indexpath) {
                    attributes.frame = attr.frame
                }
            }
        }
        return attributesCopy
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        
        if let currentItemAttributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes , let collection = collectionView{
            let isFirstItemInSection = indexPath.item == 0
 
            // 右边派头情况一
            if isFirstItemInSection {
                currentItemAttributes.rightAlignFrame(with: collection.frame.size.width)
                return currentItemAttributes
            }
            
            let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)
            
            let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame ?? CGRect.zero
  
            let currentFrame = currentItemAttributes.frame
            let strecthedCurrentFrame = CGRect(x: 0,
                                                    y: currentFrame.origin.y,
                                                    width: collection.frame.size.width,
                                                    height: currentFrame.size.height)
            let isFirstItemInRow = !previousFrame.intersects(strecthedCurrentFrame)
             // 右边派头情况二
            if isFirstItemInRow {
                currentItemAttributes.rightAlignFrame(with: collection.frame.size.width)
                return currentItemAttributes
            }
             // 右边正常的情况
            let previousFrameLeftPoint = previousFrame.origin.x
            var frame = currentItemAttributes.frame
            let minimumInteritemSpacing = evaluatedMinimumInteritemSpacing(at: indexPath.item)
            frame.origin.x = previousFrameLeftPoint - minimumInteritemSpacing - frame.size.width
            currentItemAttributes.frame = frame
            return currentItemAttributes
            
        }
        return nil
    }

BoxDengJZ/UICollectionViewLeftAlignedLayout

StackOverFlow: How do you determine spacing between cells in UICollectionView flowLayout


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

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

Hadoop in Action

Hadoop in Action

Chuck Lam / Manning Publications / 2010-12-22 / USD 44.99

HIGHLIGHT Hadoop in Action is an example-rich tutorial that shows developers how to implement data-intensive distributed computing using Hadoop and the Map- Reduce framework. DESCRIPTION Hadoop i......一起来看看 《Hadoop in Action》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具