Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make the center cell of the UICollectionView overlap the other two cells positioned at the side?

I am making a collection view to generate a Carousel effect.

I need the centre cell to overlap the other two cells on the left and the right. The centre cell needs to be always at the top.

But, when I try to overlap the cell on the side with the cell on the centre, it doesn't work. Instead the right cell ( blue cell) overlaps the centre cell ( black cell) as seen in the image below.

enter image description here

The code used is for this effect are as follows:

The below is the view controller for the collection view.

import UIKit

private let reuseIdentifier = "Cell"
let kRoomCellScaling: CGFloat = 0.6

class RoomsViewController: UICollectionViewController {

override func viewDidLoad() {
    super.viewDidLoad()

    // This method sets up the collection view
    let layout = UPCarouselFlowLayout()
    layout.itemSize = CGSizeMake(250, 250)
    layout.scrollDirection = .Horizontal

    layout.sideItemAlpha = 1
    layout.sideItemScale = 0.8
    layout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 60)

    collectionView?.setCollectionViewLayout(layout, animated: false)

}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}



// MARK: UICollectionViewDataSource

override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}


override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of items
    return 3
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)



    // Configure the cell
    switch indexPath.row%3 {

    case 0:
        cell.backgroundColor = UIColor.redColor()
    case 1:
        cell.backgroundColor = UIColor.blackColor()
    case 2:
        cell.backgroundColor = UIColor.blueColor()

    default:
        break

    }

    return cell
}


}

The below is the flow layout used for the collection view.

import UIKit


public enum UPCarouselFlowLayoutSpacingMode {
    case fixed(spacing: CGFloat)
    case overlap(visibleOffset: CGFloat)
}

public class UPCarouselFlowLayout: UICollectionViewFlowLayout {

    private struct LayoutState {
        var size: CGSize
        var direction: UICollectionViewScrollDirection
        func isEqual(otherState: LayoutState) -> Bool {
            return CGSizeEqualToSize(self.size, otherState.size) && self.direction == otherState.direction
        }
    }

    @IBInspectable public var sideItemScale: CGFloat = 0.6
    @IBInspectable public var sideItemAlpha: CGFloat = 0.6
    public var spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 40)

    private var state = LayoutState(size: CGSizeZero, direction: .Horizontal)


    override public func prepareLayout() {
        super.prepareLayout()

        let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)

        if !self.state.isEqual(currentState) {
            self.setupCollectionView()
            self.updateLayout()
            self.state = currentState
        }
    }

    private func setupCollectionView() {
        guard let collectionView = self.collectionView else { return }
        if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
            collectionView.decelerationRate = UIScrollViewDecelerationRateFast
        }
    }

    private func updateLayout() {
        guard let collectionView = self.collectionView else { return }

        let collectionSize = collectionView.bounds.size
        let isHorizontal = (self.scrollDirection == .Horizontal)

        let yInset = (collectionSize.height - self.itemSize.height) / 2
        let xInset = (collectionSize.width - self.itemSize.width) / 2
        self.sectionInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset)

        let side = isHorizontal ? self.itemSize.width : self.itemSize.height
        let scaledItemOffset =  (side - side*self.sideItemScale) / 2
        switch self.spacingMode {
        case .fixed(let spacing):
            self.minimumLineSpacing = spacing - scaledItemOffset
        case .overlap(let visibleOffset):
            let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
            let inset = isHorizontal ? xInset : yInset
            self.minimumLineSpacing = inset - fullSizeSideItemOverlap
        }
    }

    override public func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }

    override public func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superAttributes = super.layoutAttributesForElementsInRect(rect),
            let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
            else { return nil }
        return attributes.map({ self.transformLayoutAttributes($0) })
    }

    private func transformLayoutAttributes(attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        guard let collectionView = self.collectionView else { return attributes }
        let isHorizontal = (self.scrollDirection == .Horizontal)

        let collectionCenter = isHorizontal ? collectionView.frame.size.width/2 : collectionView.frame.size.height/2
        let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
        let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y) - offset

        let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
        let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
        let ratio = (maxDistance - distance)/maxDistance

        let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
        let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
        attributes.alpha = alpha
        attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)

        return attributes
    }

    override public func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView where !collectionView.pagingEnabled,
            let layoutAttributes = self.layoutAttributesForElementsInRect(collectionView.bounds)
            else { return super.targetContentOffsetForProposedContentOffset(proposedContentOffset) }

        let isHorizontal = (self.scrollDirection == .Horizontal)

        let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
        let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide

        var targetContentOffset: CGPoint
        if isHorizontal {
            let closest = layoutAttributes.sort { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
            targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
        }
        else {
            let closest = layoutAttributes.sort { abs($0.center.y - proposedContentOffsetCenterOrigin) < abs($1.center.y - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
            targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
        }

        return targetContentOffset
    }
}

So, how can i make the centre cell always overlap the other two side cells?

like image 395
Ariel Avatar asked Sep 08 '16 12:09

Ariel


1 Answers

You can translate your items to a small negative z value based on distance from center.

Replace this line:

attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)

with

let visibleRect = CGRect(origin: self.collectionView!.contentOffset, size: self.collectionView!.bounds.size)
let dist = CGRectGetMidX(attributes.frame) - CGRectGetMidX(visibleRect)
var transform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
transform = CATransform3DTranslate(transform, 0, 0, -abs(dist/1000))
attributes.transform3D = transform

enter image description here

Or you can rotate items around the y axis based on distance from center and give the transform.m34 a small negative value so it'll have perspective and a more realistic look.

Replace this line:

attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)

with

let visibleRect = CGRect(origin: self.collectionView!.contentOffset, size: self.collectionView!.bounds.size)
let dist = CGRectGetMidX(attributes.frame) - CGRectGetMidX(visibleRect)
let currentAngle = dist / (CGRectGetWidth(visibleRect)/2)
var transform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
transform.m34 = -1.0 / 1000
transform = CATransform3DRotate(transform, -currentAngle, 0, 1, 0)
attributes.transform3D = transform

enter image description here

like image 171
Sam_M Avatar answered Oct 19 '22 23:10

Sam_M