iOS UICollectionView: Cells with circular view in

2019-02-07 14:51发布

I'm trying to implement the UICollectionView for custom cells in the shape of circle. Now by default the circles are aligned in the same way as the normal square cell: top circle and bottom circle are on the same vertical line. How can I change the alignment to this: the top circle and two circles below it form an equilateral triangle (positions of top circle and bottom circle are shifted by radius' length)? As below:

from OOO
     OOO
     OOO

to   O O O
    O O O (no spacing among the circles)
     O O O

1条回答
Luminary・发光体
2楼-- · 2019-02-07 15:04

The basic idea is to create a custom UICollectionViewLayout that implements:

  • collectionViewContentSize, i.e., what's the size of the full, scrollable contentSize of the collection view;

  • layoutAttributesForItem(at indexPath:), i.e., what are the key attributes (namely center and size) of a particular cell; and

  • layoutAttributesForElements(in rect:), i.e., what are the key attributes for cells that fall within this particular rect ... this will be used to identify which cells are visible at any given point in time as well as the attributes for those cells; this is basically an array of the attributes for the cells from the previous method, filtered down to just the ones within that rect.

Thus, in Swift 3 you could do something like:

class AlternatingGridLayout: UICollectionViewLayout {

    private var itemSize: CGSize!
    private var numberOfItems: Int!
    private var itemsPerRow: Int!
    private var rows: Int!
    private var circleViewCenterOffset: CGPoint!
    private var radiusOfCircleViews: CGFloat!
    private var insets: UIEdgeInsets!

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }

        radiusOfCircleViews = CGFloat(40.0)
        itemSize = CGSize(width: radiusOfCircleViews * 2, height: radiusOfCircleViews * 2)
        circleViewCenterOffset = CGPoint(x: 2 * radiusOfCircleViews * cos(.pi / 3),
                                         y: 2 * radiusOfCircleViews * sin(.pi / 3))
        numberOfItems = collectionView.numberOfItems(inSection: 0)
        itemsPerRow = Int(floor((collectionView.bounds.width - radiusOfCircleViews) / CGFloat(2 * radiusOfCircleViews)) + 0.5)
        rows = (numberOfItems - 1) / itemsPerRow + 1
        let excess = collectionView.bounds.width - (CGFloat(itemsPerRow) * radiusOfCircleViews * 2 + circleViewCenterOffset.x)
        insets = UIEdgeInsets(top: 10, left: excess / 2, bottom: 10, right: excess / 2)
    }

    override var collectionViewContentSize: CGSize {
        return CGSize(width: collectionView!.bounds.width,
                      height: 2 * radiusOfCircleViews + CGFloat(rows - 1) * circleViewCenterOffset.y + insets.top + insets.bottom)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)

        attributes.center = centerForItem(at: indexPath)
        attributes.size = itemSize

        return attributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return (0 ..< collectionView!.numberOfItems(inSection: 0)).map { IndexPath(item: $0, section: 0) }
            .filter { rect.intersects(rectForItem(at: $0)) }
            .compactMap { self.layoutAttributesForItem(at: $0) }  // `flatMap` in Xcode versions before 9.3
    }

    private func centerForItem(at indexPath: IndexPath) -> CGPoint {
        let row = indexPath.item / itemsPerRow
        let col = indexPath.item - row * itemsPerRow

        var x: CGFloat = radiusOfCircleViews + CGFloat(col) * (radiusOfCircleViews * 2)
        let y: CGFloat = radiusOfCircleViews + CGFloat(row) * (circleViewCenterOffset.y)

        if row % 2 == 0 {
            x += circleViewCenterOffset.x
        }

        return CGPoint(x: x + insets.left, y: y + insets.top)
    }

    private func rectForItem(at indexPath: IndexPath) -> CGRect {
        let center = centerForItem(at: indexPath)

        return CGRect(x: center.x - radiusOfCircleViews, y: center.y - radiusOfCircleViews, width: radiusOfCircleViews * 2, height: radiusOfCircleViews * 2)
    }        
}

That yields:

enter image description here

Clearly, customize this as you see fit, but it illustrates the basic idea.


In my original answer, below, I assumed you wanted to see these cells in a circle, as shown in WWDC 2012 video Advanced Collection Views and Building Custom Layouts (about 40+ minutes into the video). See that below.


For example, in Swift 3:

class CircleLayout: UICollectionViewLayout {

    private var center: CGPoint!
    private var itemSize: CGSize!
    private var radius: CGFloat!
    private var numberOfItems: Int!

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }

        center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
        let shortestAxisLength = min(collectionView.bounds.width, collectionView.bounds.height)
        itemSize = CGSize(width: shortestAxisLength * 0.1, height: shortestAxisLength * 0.1)
        radius = shortestAxisLength * 0.4
        numberOfItems = collectionView.numberOfItems(inSection: 0)
    }

    override var collectionViewContentSize: CGSize {
        return collectionView!.bounds.size
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)

        let angle = 2 * .pi * CGFloat(indexPath.item) / CGFloat(numberOfItems)

        attributes.center = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
        attributes.size = itemSize

        return attributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return (0 ..< collectionView!.numberOfItems(inSection: 0))
            .compactMap { item -> UICollectionViewLayoutAttributes? in    // `flatMap` in Xcode versions prior to 9.3
                self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))
        }
    }
}

Then you can simply set the collectionViewLayout and then implement the standard UICollectionViewDataSource methods.

class ViewController: UICollectionViewController {

    var numberOfCells = 10

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = CircleLayout()

        // just for giggles and grins, let's show the insertion of a cell

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.collectionView?.performBatchUpdates({
                self.numberOfCells += 1
                self.collectionView?.insertItems(at: [IndexPath(item: 0, section: 0)])
            }, completion: nil)
        }
    }

}

// MARK: UICollectionViewDataSource

extension ViewController {
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return numberOfCells
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CircleCell", for: indexPath)
        return cell
    }
}

That yields:

circle view


See https://github.com/robertmryan/CircularCollectionView for sample.


Note, you mentioned that you want "no spacing among the circles", so just adjust the radius and/or itemSize accordingly to get the layout you want.

查看更多
登录 后发表回答