A cover flow implementation by subclassing UICollectionViewFlowLayout
Original found on here, thanks to solomidSF(He must be a dota player:p), and I copy it using Swift with a little modifications
/**
* Layout to add cover flow effect to collection view scrolling.
* Applicable only for horizontal flow direction.
*/
class TMCoverFlowLayout: UICollectionViewFlowLayout {
/**
* Max degree that can be applied to individual item.
* Default to 45 degrees.
*/
var maxCoverDegree: CGFloat = 0
/**
* Determines how elements covers each other.
* Should be in range 0..1.
* Default to 0.25.
* Examples:
* 0 means that items are placed within a continious line.
* 0.5 means that half of 3 and 1 cover will be behind 2.
*/
var coverDensity: CGFloat = 0
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
override init() {
super.init()
self.commonInit()
}
override func prepareLayout() {
super.prepareLayout()
//This is a cover flow layout so forcely set these properties
self.scrollDirection = .Horizontal
self.collectionView?.pagingEnabled = true
assert(self.collectionView?.numberOfSections() == 1, "TMCoverFlowLayout: Multiple sections aren't supported!")
assert(self.scrollDirection == .Horizontal, "TMCoverFlowLayout: Vertical scrolling isn't supported!")
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
var indexPaths = self.indexPathsContainedInRect(rect)
var resultingAttributes = [UICollectionViewLayoutAttributes]()
for path in indexPaths {
// We should create attributes by ourself.
var attributes = self.layoutAttributesForItemAtIndexPath(path)
resultingAttributes.append(attributes)
}
return resultingAttributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
var attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.size = self.itemSize
attributes.center = CGPoint(x: self.collectionViewWidth() * CGFloat(indexPath.row) + self.collectionViewWidth(), y: self.collectionViewHeight()/2)
self.interpolateAttributes(attributes, forOffset: self.collectionView!.contentOffset.x)
return attributes
}
override func collectionViewContentSize() -> CGSize {
return CGSize(width: self.collectionView!.bounds.size.width * CGFloat(self.collectionView!.numberOfItemsInSection(0)), height: self.collectionView!.bounds.size.height)
}
func collectionViewHeight() -> CGFloat {
return self.collectionView!.bounds.size.height
}
func collectionViewWidth() -> CGFloat {
return self.collectionView!.bounds.size.width
}
func commonInit() {
self.maxCoverDegree = 15
self.coverDensity = 0
}
func itemCenterForRow(row: Int) -> CGPoint {
var collectionViewSize = self.collectionView!.bounds.size
return CGPoint(x: CGFloat(row) * collectionViewSize.width + collectionViewSize.width/2, y: collectionViewSize.height/2)
}
func minXForRow(row: Int) -> CGFloat {
return self.itemCenterForRow(row - 1).x + (1.0/2 - self.coverDensity) * self.itemSize.width
}
func maxXForRow(row: Int) -> CGFloat {
return self.itemCenterForRow(row + 1).x - (1.0/2 - self.coverDensity) * self.itemSize.width
}
func minXCenterForRow(row: Int) -> CGFloat {
var center = self.itemCenterForRow(row - 1).x
return center + (self.itemSize.width/2) * (1 - 2 * self.coverDensity + cos(self.degreesToRad(self.maxCoverDegree)))
}
func maxXCenterForRow(row: Int) -> CGFloat {
var center = self.itemCenterForRow(row + 1).x
return center - (self.itemSize.width/2) * (1 - 2 * self.coverDensity + cos(self.degreesToRad(self.maxCoverDegree)))
}
func degreesToRad(degrees: CGFloat) -> CGFloat {
return degrees * CGFloat(M_PI)/180.0
}
func indexPathsContainedInRect(rect: CGRect) -> [NSIndexPath] {
if self.collectionView!.numberOfItemsInSection(0) == 0 {
// Nothing to do here when we don't have items.
return [NSIndexPath]()
}
// Find min and max rows that can be determined for sure.
var minRow = Int(max(rect.origin.x/self.collectionViewWidth(), 0))
var maxRow = Int(CGRectGetMaxX(rect)/self.collectionViewWidth())
// Additional check for rows that also can be included (our rows are moving depending on content size).
var candidateMinRow = Int(max(minRow - 1, 0))
if self.maxXForRow(candidateMinRow) >= rect.origin.x {
// We have a row that is less than given minimum.
minRow = candidateMinRow
}
var candidateMaxRow = Int(min(maxRow + 1, self.collectionView!.numberOfItemsInSection(0) - 1))
if self.minXForRow(candidateMaxRow) <= CGRectGetMaxX(rect) {
maxRow = candidateMaxRow
}
// Simply add index paths between min and max.
var resultingIndexPaths = [NSIndexPath]()
for var i = minRow; i <= maxRow; i++ {
resultingIndexPaths.append(NSIndexPath(forRow: i, inSection: 0))
}
return resultingIndexPaths
}
func interpolateAttributes(attributes: UICollectionViewLayoutAttributes, forOffset offset: CGFloat) {
var attributesPath = attributes.indexPath
// Interpolate offset for given attribute. For this task we need min max interval and min and max x allowed for item.
var minInterval = CGFloat(attributesPath.row - 1) * self.collectionViewWidth()
var maxInterval = CGFloat(attributesPath.row + 1) * self.collectionViewWidth()
var minX = self.minXCenterForRow(attributesPath.row)
var maxX = self.maxXCenterForRow(attributesPath.row)
// Interpolate by formula
var interpolatedX = min(max(minX + ((maxX - minX)/(maxInterval - minInterval)) * (offset - minInterval), minX), maxX)
attributes.center = CGPoint(x: interpolatedX, y: attributes.center.y)
// Interpolate position into angle by formula.
var angle = -self.maxCoverDegree + (interpolatedX - minX) * 2 * self.maxCoverDegree / (maxX - minX)
var transform = CATransform3DIdentity
// Add perspective.
transform.m34 = -1/500.0
// Then rotate.
// transform = CATransform3DRotate(transform, angle * CGFloat(M_PI)/180, 0, 1, 0)
// Or scale
let scale = max(1 - abs(angle)/45, 0.9)
transform = CATransform3DScale(transform, scale, scale, scale)
let alpha = max(1 - abs(angle)/45, 0.5)
attributes.alpha = alpha
attributes.transform3D = transform
attributes.zIndex = NSIntegerMax - attributesPath.row
}
}