mirror of
https://bitbucket.org/vendoo/vendoo_v1.0.git
synced 2025-12-25 03:37:39 +00:00
285 lines
9.4 KiB
Swift
285 lines
9.4 KiB
Swift
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2015 Joakim Gyllström
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
import UIKit
|
|
|
|
/**
|
|
Provides a grid collection view layout
|
|
*/
|
|
@objc(BSGridCollectionViewLayout)
|
|
public final class GridCollectionViewLayout: UICollectionViewLayout {
|
|
/**
|
|
Spacing between items (horizontal and vertical)
|
|
*/
|
|
public var itemSpacing: CGFloat = 0 {
|
|
didSet {
|
|
_itemSize = estimatedItemSize()
|
|
}
|
|
}
|
|
|
|
/**
|
|
Number of items per row
|
|
*/
|
|
public var itemsPerRow = 3 {
|
|
didSet {
|
|
_itemSize = estimatedItemSize()
|
|
}
|
|
}
|
|
|
|
/**
|
|
Item height ratio relative to it's width
|
|
*/
|
|
public var itemHeightRatio: CGFloat = 1 {
|
|
didSet {
|
|
_itemSize = estimatedItemSize()
|
|
}
|
|
}
|
|
|
|
/**
|
|
Size for each item
|
|
*/
|
|
public var itemSize: CGSize {
|
|
get {
|
|
return _itemSize
|
|
}
|
|
}
|
|
|
|
var items = 0
|
|
var rows = 0
|
|
var _itemSize = CGSizeZero
|
|
|
|
public override func prepareLayout() {
|
|
// Set total number of items and rows
|
|
items = estimatedNumberOfItems()
|
|
rows = items / itemsPerRow + ((items % itemsPerRow > 0) ? 1 : 0)
|
|
|
|
// Set item size
|
|
_itemSize = estimatedItemSize()
|
|
}
|
|
|
|
/**
|
|
See UICollectionViewLayout documentation
|
|
*/
|
|
public override func collectionViewContentSize() -> CGSize {
|
|
guard let collectionView = collectionView where rows > 0 else {
|
|
return CGSizeZero
|
|
}
|
|
|
|
let height = estimatedRowHeight() * CGFloat(rows)
|
|
return CGSize(width: collectionView.bounds.width, height: height)
|
|
}
|
|
|
|
/**
|
|
See UICollectionViewLayout documentation
|
|
*/
|
|
public override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
|
return indexPathsInRect(rect).map { (indexPath) -> UICollectionViewLayoutAttributes in
|
|
return self.layoutAttributesForItemAtIndexPath(indexPath)! // TODO: Fix forcefull unwrap
|
|
}
|
|
}
|
|
|
|
/**
|
|
See UICollectionViewLayout documentation
|
|
*/
|
|
public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
|
|
let itemIndex = flatIndex(indexPath) // index among total number of items
|
|
let rowIndex = itemIndex % itemsPerRow // index within it's row
|
|
let row = itemIndex / itemsPerRow // which row for that item
|
|
|
|
let x = (CGFloat(rowIndex) * itemSpacing) + (CGFloat(rowIndex) * itemSize.width)
|
|
let y = (CGFloat(row) * itemSpacing) + (CGFloat(row) * itemSize.height)
|
|
let width = _itemSize.width
|
|
let height = _itemSize.height
|
|
|
|
let attribute = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
|
|
attribute.frame = CGRect(x: x, y: y, width: width, height: height)
|
|
|
|
return attribute
|
|
}
|
|
|
|
/**
|
|
See UICollectionViewLayout documentation
|
|
*/
|
|
public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
|
|
return true
|
|
}
|
|
|
|
// No decoration or supplementary views
|
|
/**
|
|
See UICollectionViewLayout documentation
|
|
*/
|
|
public override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return nil }
|
|
/**
|
|
See UICollectionViewLayout documentation
|
|
*/
|
|
public override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return nil }
|
|
}
|
|
|
|
extension GridCollectionViewLayout {
|
|
/**
|
|
Calculates which index paths are within a given rect
|
|
- parameter rect: The rect which we want index paths for
|
|
- returns: An array of indexPaths for that rect
|
|
*/
|
|
func indexPathsInRect(rect: CGRect) -> [NSIndexPath] {
|
|
// Make sure we have items/rows
|
|
guard items > 0 && rows > 0 else { return [] }
|
|
|
|
let rowHeight = estimatedRowHeight()
|
|
|
|
let startRow = GridCollectionViewLayout.firstRowInRect(rect, withRowHeight: rowHeight)
|
|
let endRow = GridCollectionViewLayout.lastRowInRect(rect, withRowHeight: rowHeight, max: rows)
|
|
|
|
let startIndex = GridCollectionViewLayout.firstIndexInRow(startRow, withItemsPerRow: itemsPerRow)
|
|
let endIndex = GridCollectionViewLayout.lastIndexInRow(endRow, withItemsPerRow: itemsPerRow, numberOfItems: items)
|
|
|
|
let indexPaths = (startIndex...endIndex).map { indexPathFromFlatIndex($0) }
|
|
|
|
return indexPaths
|
|
}
|
|
|
|
/**
|
|
Calculates which row index would be first for a given rect.
|
|
- parameter rect: The rect to check
|
|
- parameter rowHeight: Height for a row
|
|
- returns: First row index
|
|
*/
|
|
static func firstRowInRect(rect: CGRect, withRowHeight rowHeight: CGFloat) -> Int {
|
|
if rect.origin.y / rowHeight < 0 {
|
|
return 0
|
|
} else {
|
|
return Int(rect.origin.y / rowHeight)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Calculates which row index would be last for a given rect.
|
|
- parameter rect: The rect to check
|
|
- parameter rowHeight: Height for a row
|
|
- returns: Last row index
|
|
*/
|
|
static func lastRowInRect(rect: CGRect, withRowHeight rowHeight: CGFloat, max: Int) -> Int {
|
|
guard rect.size.height >= rowHeight else { return 0 }
|
|
|
|
if (rect.origin.y + rect.height) / rowHeight > CGFloat(max) {
|
|
return max - 1
|
|
} else {
|
|
return Int(ceil((rect.origin.y + rect.height) / rowHeight)) - 1
|
|
}
|
|
}
|
|
|
|
/**
|
|
Calculates which index would be the first for a given row.
|
|
- parameter row: Row index
|
|
- parameter itemsPerRow: How many items there can be in a row
|
|
- returns: First index
|
|
*/
|
|
static func firstIndexInRow(row: Int, withItemsPerRow itemsPerRow: Int) -> Int {
|
|
return row * itemsPerRow
|
|
}
|
|
|
|
/**
|
|
Calculates which index would be the last for a given row.
|
|
- parameter row: Row index
|
|
- parameter itemsPerRow: How many items there can be in a row
|
|
- parameter numberOfItems: The total number of items.
|
|
- returns: Last index
|
|
*/
|
|
static func lastIndexInRow(row: Int, withItemsPerRow itemsPerRow: Int, numberOfItems: Int) -> Int {
|
|
let maxIndex = (row + 1) * itemsPerRow - 1
|
|
let bounds = numberOfItems - 1
|
|
|
|
if maxIndex > bounds {
|
|
return bounds
|
|
} else {
|
|
return maxIndex
|
|
}
|
|
}
|
|
|
|
/**
|
|
Takes an index path (which are 2 dimensional) and turns it into a 1 dimensional index
|
|
- parameter indexPath: The index path we want to flatten
|
|
- returns: A flat index
|
|
*/
|
|
func flatIndex(indexPath: NSIndexPath) -> Int {
|
|
guard let collectionView = collectionView else {
|
|
return 0
|
|
}
|
|
|
|
return (0..<indexPath.section).reduce(indexPath.row) { $0 + collectionView.numberOfItemsInSection($1)}
|
|
}
|
|
|
|
/**
|
|
Converts a flat index into an index path
|
|
- parameter index: The flat index
|
|
- returns: An index path
|
|
*/
|
|
func indexPathFromFlatIndex(index: Int) -> NSIndexPath {
|
|
guard let collectionView = collectionView else {
|
|
return NSIndexPath(forItem: 0, inSection: 0)
|
|
}
|
|
|
|
var item = index
|
|
var section = 0
|
|
|
|
while(item >= collectionView.numberOfItemsInSection(section)) {
|
|
item -= collectionView.numberOfItemsInSection(section)
|
|
section += 1
|
|
}
|
|
|
|
return NSIndexPath(forItem: item, inSection: section)
|
|
}
|
|
|
|
/**
|
|
Estimated the size of the items
|
|
- returns: Estimated item size
|
|
*/
|
|
func estimatedItemSize() -> CGSize {
|
|
guard let collectionView = collectionView else {
|
|
return CGSizeZero
|
|
}
|
|
|
|
let itemWidth = (collectionView.bounds.width - CGFloat(itemsPerRow - 1) * itemSpacing) / CGFloat(itemsPerRow)
|
|
return CGSize(width: itemWidth, height: itemWidth * itemHeightRatio)
|
|
}
|
|
|
|
/**
|
|
Estimated total number of items
|
|
- returns: Total number of items
|
|
*/
|
|
func estimatedNumberOfItems() -> Int {
|
|
guard let collectionView = collectionView else {
|
|
return 0
|
|
}
|
|
|
|
return (0..<collectionView.numberOfSections()).reduce(0, combine: {$0 + collectionView.numberOfItemsInSection($1)})
|
|
}
|
|
|
|
/**
|
|
Height for each row
|
|
- returns: Row height
|
|
*/
|
|
func estimatedRowHeight() -> CGFloat {
|
|
return _itemSize.height+itemSpacing
|
|
}
|
|
}
|