0

I am trying to achieve a collection view where the cells are aligned at the bottom with a paging effect where the "selected" cell is bigger than the rest. Like this:

enter image description here

As of now, I am able to get the effect to work but the cells are aligned in the middle instead of at the bottom:

enter image description here

I have tried setting the anchorPoint property of the cell to pin the cells at (0, 1) in apply(_ layoutAttributes: UICollectionViewLayoutAttributes), but this causes the cells to move and as as a result they appear cut off. This ends up looking like this:

enter image description here

How do I pin these collection view cells at the bottom left corner, also respecting the CGAffine scale effect that occurs during paging?

Here is my code:

Custom UICollectionViewFlowLayout:

import Foundation
import UIKit

/// The layout used in the cover flow.
class CoverFlowLayout: UICollectionViewFlowLayout {
  let activeDistance: CGFloat = 25
  let zoomFactor: CGFloat = (CoverFlowCell.selectedSize / CoverFlowCell.unselectedSize) - 1
  
  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
    let itemSpace = itemSize.width + minimumInteritemSpacing
    var currentItemIdx = round(collectionView.contentOffset.x / itemSpace)
    
    let vX = velocity.x
    if vX > 0 {
      currentItemIdx += 1
    } else if vX < 0 {
      currentItemIdx -= 1
    }

    let nearestPageOffset = currentItemIdx * itemSpace
    return CGPoint(x: nearestPageOffset, y: 0)
  }
  
  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
    guard let collectionView = collectionView else { return nil }
    let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
    let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)

    // Make the cells be zoomed when they reach the center of the screen
    for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
      let distance = (visibleRect.minX + 20) - attributes.frame.minX
      let normalizedDistance = distance / activeDistance
      
      if distance.magnitude < activeDistance {
        let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
        attributes.transform = CGAffineTransform(scaleX: zoom, y: zoom)
      }
    }

    return rectAttributes
  }
  
  override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
    // Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
    return true
  }

  override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
    let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
    context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
    return context
  }
}

Custom UICollectionView Cell

import Foundation
import UIKit

class CoverFlowCell: UICollectionViewCell {
  
  static let unselectedSize: CGFloat = 185; // The size of the cell when it is not selected in the carousel
  static let selectedSize: CGFloat = 200;
  
  private var albumArt: UIImageView = {
    let art = UIImageView()
    art.backgroundColor = UIColor(hexString: "#ECF0F1")
    art.translatesAutoresizingMaskIntoConstraints = false
    art.layer.cornerRadius = 2
    return art
  }()
  
  /// Initializer
  override init(frame: CGRect) {
    super.init(frame: frame)
    setupUI()
    setupUIConstraints()
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func awakeFromNib() {
    super.awakeFromNib()
  }
  
  private func setupUI() {
    contentView.addSubview(albumArt)
  }
  
  override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
    super.apply(layoutAttributes)

    //we must change the anchor point for propper cells positioning and scaling
    self.layer.anchorPoint.x = 0
    self.layer.anchorPoint.y = 1
  }

  private func setupUIConstraints() {
    NSLayoutConstraint.activate([
      self.albumArt.topAnchor.constraint(equalTo: topAnchor),
      self.albumArt.bottomAnchor.constraint(equalTo: bottomAnchor),
      self.albumArt.leftAnchor.constraint(equalTo: leftAnchor),
      self.albumArt.rightAnchor.constraint(equalTo: rightAnchor)
    ])
  }
}

I have tried referring to this thread: Changing my CALayer's anchorPoint moves the view

But the solution provided did not help align the cells at the bottom.

Thanks

aritroper
  • 1,671
  • 5
  • 16
  • 26

1 Answers1

0

Add another transform to translate the y position, to slide it up after you scale it up:

let y = //set a negative number here, to slide up by that many points
transform = CGAffineTransform(translationX: 0, y: y)

I would also just apply the transform in the "did select item" method, rather than fuss with it in the layout attributes methods. Then when the cell is "deselected", you can just set the transform to .identity to reset it back to the normal layout.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  //set transforms
}

func collectionView(_ UICollectionView, didDeselectItemAt: IndexPath) {
  //set transform to .identity
}
SuperTully
  • 319
  • 1
  • 6