UICollectionViewLayout

UICollectionView 自定義簡(jiǎn)述

寫(xiě)作目的

UICollectionView是一個(gè)十分強(qiáng)大的控件,其所有的實(shí)現(xiàn)效果都是依賴于UICollectionViewLayout和UICollectionViewFlowLayout。我這里做一些簡(jiǎn)單的總結(jié),希望能幫助大家,也是自己對(duì)知識(shí)的梳理。

UICollectionViewLayout、UICollectionViewFlowLayout介紹

由于系統(tǒng)自帶的實(shí)現(xiàn)效果可能在真正的開(kāi)發(fā)過(guò)程當(dāng)中無(wú)法滿足我們的需求,這時(shí)候我們就會(huì)想到自定義,而自定義UICollectionView其實(shí)就是自定義UICollectionViewLayout。

UICollectionViewLayoutAttributes

它的主要作用是負(fù)責(zé)存儲(chǔ)每一個(gè)Cell位置、大小屬性等。在一個(gè)CollectionView中可能有很多個(gè)這樣的屬性,我們也通過(guò)這個(gè)屬性獲取到每個(gè)cell存儲(chǔ)的信息來(lái)進(jìn)行布局

open class UICollectionViewLayoutAttributes : NSObject, NSCopying, UIDynamicItem {

    
    open var frame: CGRect //獲取到cell的frame

    open var center: CGPoint // 獲取噠cell的center

    open var size: CGSize // size

    open var transform3D: CATransform3D // 設(shè)置動(dòng)畫(huà)

    @available(iOS 7.0, *)
    open var bounds: CGRect

    @available(iOS 7.0, *)
    open var transform: CGAffineTransform // 動(dòng)畫(huà)

    open var alpha: CGFloat

    open var zIndex: Int // default is 0

    open var isHidden: Bool // As an optimization, UICollectionView might not create a view for items whose hidden attribute is YES

    open var indexPath: IndexPath

    
    open var representedElementCategory: UICollectionElementCategory { get }

    open var representedElementKind: String? { get } // nil when representedElementCategory is UICollectionElementCategoryCell

    
    public convenience init(forCellWith indexPath: IndexPath)

    public convenience init(forSupplementaryViewOfKind elementKind: String, with indexPath: IndexPath)

    public convenience init(forDecorationViewOfKind decorationViewKind: String, with indexPath: IndexPath)
}

  • UICollectionViewLayoutAttributes 的實(shí)例中包含了邊框、中心、大小、形狀、透明度、層次關(guān)系等信息
  • 一個(gè)cell對(duì)應(yīng)一個(gè)UICollectionViewLayoutAttributes對(duì)象

開(kāi)始工作

  • 首先創(chuàng)建一個(gè)工程,并且 pod 需要的一些第三方庫(kù)

  • 添加UI等

  • 自定義UICollectionViewCell


class photoCell: UICollectionViewCell {
    
    var photoImageView: UIImageView?
    var photoImageS: String?{
        didSet{
            self.photoImageView?.image = UIImage(named: self.photoImageS!)
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        photoImageView = UIImageView(frame: CGRect.zero)
        photoImageView?.layer.borderColor = UIColor.white.cgColor
        photoImageView?.layer.borderWidth = 10
        self.contentView.addSubview(self.photoImageView!)
        photoImageView?.snp.makeConstraints({ (make) in
            make.edges.equalTo(self.contentView)
        })
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    

}


class ViewController: UIViewController {
    
    var collectionView: UICollectionView!
    var dataSource: [String] = {
        var array: [String] = []
        for index in 0..<20 {
            array.append(String(index + 1))
        }
        return array
    }()
    let suqareLayout: SquareLayout = {
        let layout = SquareLayout()
        return layout
    }()
    
    let customFlow: ScrollFlowLayout = {
        let flowlayout = ScrollFlowLayout()
        flowlayout.itemSize = CGSize(width: 150, height: UIScreen.height / 2)
        return flowlayout
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        initSubView()
        
        view.backgroundColor = UIColor.white
        
        
        let button = UIButton()
        button.frame = CGRect(x: 200, y: 300, width: 80, height: 80)
        button.setBackgroundImage(UIImage.init(named: "sub_add"), for: .normal)
        button.setBackgroundImage(UIImage.init(named: "sub_add_h"), for: .highlighted)
        button.addTarget(self, action: #selector(changeLayout), for: .touchUpInside)
        view.addSubview(button)
    }
    func changeLayout() {
        if self.collectionView.collectionViewLayout == suqareLayout {
            self.collectionView.setCollectionViewLayout(customFlow, animated: true)
        } else {
            self.collectionView.setCollectionViewLayout(suqareLayout, animated: true)
            
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {


    }
}
extension ViewController{
    // MARK:-- UI
    func initSubView(){
        collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: suqareLayout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.backgroundColor = UIColor.clear
        collectionView.isScrollEnabled = true
        collectionView.showsVerticalScrollIndicator = false
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.register(photoCell.self, forCellWithReuseIdentifier: NSStringFromClass(photoCell.self))
        view.addSubview(collectionView)
        
        collectionView.snp.makeConstraints { (make) in
            make.edges.equalTo(view)
        }
        
        
    }
}

extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(photoCell.self), for: indexPath) as? photoCell
        cell?.photoImageS = self.dataSource[indexPath.row]
        return cell!
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.dataSource.remove(at: indexPath.row)
        self.collectionView.deleteItems(at: [indexPath])
    }
}
extension ViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataSource.count
    }
    
}

大家先不要看我自定義的UICollectionViewLayout,稍后我會(huì)給大家詳細(xì)介紹。實(shí)現(xiàn)了UICollectionView的代理設(shè)置等。接下來(lái)就自定義UICollectionViewLayout

自定義UICollectionViewLayout

  • 介紹layout的一些方法
prepare()
        override func prepare() {
        super.prepare()
        AttribuArray.removeAll()
        let count = self.collectionView?.numberOfItems(inSection: 0)
        guard count != 0 else {
            print("沒(méi)有數(shù)據(jù)")
            return
        }
        for index in 0..<Int(count!){
            let indexpath = NSIndexPath(item: index, section: 0)
            let attributes = self.layoutAttributesForItem(at: indexpath as IndexPath)
            self.AttribuArray.append(attributes!)
        }
     }

當(dāng)前方法的作用是初始化布局,獲得所有cell的Attributes屬性

contensize

<font color = #DC143C> 注意:這里的contentSize 是整個(gè)CollectionView的size,這個(gè)還要得益于UICollectionView繼承與UIScrollView</font>

// 設(shè)置ContentSize 這樣才可以滑動(dòng)
override var collectionViewContentSize: CGSize{
    get{
        
        let count = self.collectionView?.numberOfItems(inSection: 0)
        let rows = (count! + 2) / 3
        let rowH: CGFloat = (self.collectionView?.width)! / 2
        if Int(rowH) * rows > Int(UIScreen.height) {
            return CGSize(width: 0, height: rowH * CGFloat(rows))
        } else {
            return CGSize(width: 0, height: rowH * CGFloat(rows) - rowH / 2)
        }
        
    } set{
        self.collectionViewContentSize = newValue
    }
}
設(shè)置展示樣式 獲得 Attributes

<font color = #DC143C> 注意:
layoutAttribsForItem(at indexpath: Indexpath) 和
layoutAttributesInRect(In rect: CGRect)
這兩方法,前者返回的是indexpath的Attributes屬性,而后者返回的是當(dāng)前Rect中所有元素的布局,返回的是包含UICollectionViewLayoutAttris的Array
</font>

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let width: CGFloat = (self.collectionView?.width)! * 0.5
        let attributes = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
        let height: CGFloat = width
        let i = indexPath.item
        
        switch i {
        case 0:
            attributes.frame = CGRect(x: 0, y: 0, width: width, height: height)
            break
        case 1:
            attributes.frame = CGRect(x: width, y: 0, width: width, height: height / 2)
            break
        case 2:
            attributes.frame = CGRect(x: width, y: height / 2, width: width, height: height / 2)
            break
        case 3:
            attributes.frame = CGRect(x: 0, y: height, width: width, height: height / 2)
            break
        case 4:
            attributes.frame = CGRect(x: 0, y: height + height / 2, width: width, height: height / 2)
            break
        case 5:
            attributes.frame = CGRect(x: width, y: height, width: width, height: height)
            break
   
        default:
            let last = self.AttribuArray[i - 6]
            var frame = last.frame
            frame.origin.y += 2 * height
            attributes.frame = frame
        }
        return attributes
    }


  • 賦上代碼
class SquareLayout: UICollectionViewLayout {
    
    // 存放相關(guān)的屬性
    var AttribuArray : [UICollectionViewLayoutAttributes] = []
    override init() {
        super.init()
     
        
    }
    
    // 設(shè)置ContentSize 這樣才可以滑動(dòng)
    override var collectionViewContentSize: CGSize{
        get{
            
            let count = self.collectionView?.numberOfItems(inSection: 0)
            let rows = (count! + 2) / 3
            let rowH: CGFloat = (self.collectionView?.width)! / 2
            if Int(rowH) * rows > Int(UIScreen.height) {
                return CGSize(width: 0, height: rowH * CGFloat(rows))
            } else {
                return CGSize(width: 0, height: rowH * CGFloat(rows) - rowH / 2)
            }
            
        } set{
            self.collectionViewContentSize = newValue
        }
    }
    //初始化 布局
    override func prepare() {
        super.prepare()
        AttribuArray.removeAll()
        let count = self.collectionView?.numberOfItems(inSection: 0)
        guard count != 0 else {
            print("沒(méi)有數(shù)據(jù)")
            return
        }
        for index in 0..<Int(count!){
            let indexpath = NSIndexPath(item: index, section: 0)
            let attributes = self.layoutAttributesForItem(at: indexpath as IndexPath)
            self.AttribuArray.append(attributes!)
        }

    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let width: CGFloat = (self.collectionView?.width)! * 0.5
        let attributes = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
        let height: CGFloat = width
        let i = indexPath.item
        
        switch i {
        case 0:
            attributes.frame = CGRect(x: 0, y: 0, width: width, height: height)
            break
        case 1:
            attributes.frame = CGRect(x: width, y: 0, width: width, height: height / 2)
            break
        case 2:
            attributes.frame = CGRect(x: width, y: height / 2, width: width, height: height / 2)
            break
        case 3:
            attributes.frame = CGRect(x: 0, y: height, width: width, height: height / 2)
            break
        case 4:
            attributes.frame = CGRect(x: 0, y: height + height / 2, width: width, height: height / 2)
            break
        case 5:
            attributes.frame = CGRect(x: width, y: height, width: width, height: height)
            break
   
        default:
            let last = self.AttribuArray[i - 6]
            var frame = last.frame
            frame.origin.y += 2 * height
            attributes.frame = frame
        }
        return attributes
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        return AttribuArray
    }
    
    
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
import UIKit
import Foundation
class ScrollFlowLayout: UICollectionViewFlowLayout {
    
    
    /*
     注意: 在初始化一個(gè)UICollectionViewLayout實(shí)例吼,會(huì)有一系列準(zhǔn)備方法被自動(dòng)調(diào)用,保證layout實(shí)例的正確
     1.prepare
     2.CollectionViewContentSize
     3.layoutAttributesForElementInRect -> 初始化的layout的外觀將由該方法返回的UICollectionViewLayoutAttributes決定
     */
    
    override init() {
        super.init()
        
        self.minimumLineSpacing = 0.7 * self.itemSize.width
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    // MARK:-- 特別注意,布局的初始化操作,不要在init中做布局的初始化操作
    override func prepare() {
        super.prepare()
        
        self.scrollDirection = .horizontal
        let insert: CGFloat = ((self.collectionView?.width)! - self.itemSize.width) / 2
        self.sectionInset = UIEdgeInsetsMake(0, insert, 0, insert)
    }
    
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        let array = super.layoutAttributesForElements(in: rect)
        
        let centerX = (self.collectionView?.contentOffset.x)! + (self.collectionView?.width)! / 2
        
//        print("layoutAttributesForElementsInRect==\(rect)=======\(centerX)")
        for attribute in array! {
//            print("第 \(attribute.indexPath.item) cell --距離:\(attribute.center.x - centerX)")
            
            let delta = abs(attribute.center.x - centerX);
            let scale = 1 - delta/(self.collectionView?.width)!
            attribute.transform = CGAffineTransform(scaleX: scale, y: scale)
        }
        
        return array!
    }
    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
   
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        
        var rect = CGRect()
        rect.origin.x = proposedContentOffset.x
        rect.origin.y = proposedContentOffset.y
        rect.size = (self.collectionView?.frame.size)!
        
        let array = super.layoutAttributesForElements(in: rect)
        let centerX = (self.collectionView?.width)! / 2 + proposedContentOffset.x
        print("=====\(proposedContentOffset.x)")
        //存放的最小間距
        //TODO:   注意研究一下:
        var minDelta = MAXFLOAT
        for attribute in array! {
            if Float(abs(minDelta)) > Float(abs(attribute.center.x - centerX)) {
                minDelta = Float(attribute.center.x - centerX)
            }
        }
        // 修改原有的偏移量

        //如果返回的時(shí)zero 那個(gè)滑動(dòng)停止后 就會(huì)立刻回到原地
        return CGPoint.init(x: CGFloat(proposedContentOffset.x + CGFloat(minDelta)), y: proposedContentOffset.y)
        
    }
    
    
}

<font color = #DC143C> 方法調(diào)用的先后順序

1.prepare()

2.collectionViewContentSize

3.layoutAttributesFoeItem

4.layoutAttributesInRect

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容