Last active
October 24, 2023 06:04
-
-
Save amfathi/a6167201f5375e91f3bce8a17b8a596c to your computer and use it in GitHub Desktop.
Card paging custom layout (Inspired by Framer: Magic Motion- Card Paging https://framer.com/projects/Card-Paging-u07vJxAeT6WJIVlQlTF6) [Result: https://drive.google.com/open?id=1fdrIAeXa-6AzDUXh-IaY6oxv1qKOCRX8]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CardPagingLayout.swift | |
// | |
// Created by Ahmed Fathi on 5/29/20. | |
// Copyright © 2020 Ahmed Fathi. All rights reserved. | |
// | |
import UIKit | |
class CardPagingLayout: UICollectionViewLayout { | |
// MARK: Book keeping | |
private var itemCount = 0 | |
private var itemSize = CGSize.zero | |
private var contentInsets = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) | |
private var maxRotationAngle: CGFloat = .pi * 0.1 | |
private let interitemSpace: CGFloat = 16 | |
private var itemWidth: CGFloat { | |
itemSize.width | |
} | |
private var itemAndSpaceWidth: CGFloat { | |
itemWidth + interitemSpace | |
} | |
private var contentWidth: CGFloat { | |
(CGFloat(itemCount) * itemWidth) + | |
(CGFloat(itemCount - 1) * interitemSpace) + | |
(contentInsets.left + contentInsets.right) | |
} | |
// Cached layout attributes | |
private var layoutAttributes: [UICollectionViewLayoutAttributes] = [] | |
// ContentSize | |
override var collectionViewContentSize: CGSize { | |
guard let cv = collectionView else { return .zero } | |
return CGSize(width: contentWidth, height: cv.bounds.height) | |
} | |
// MARK: - Prepare | |
override func prepare() { | |
super.prepare() | |
guard let cv = collectionView else { return } | |
cv.decelerationRate = .fast | |
cv.contentInset = contentInsets | |
itemCount = cv.numberOfItems(inSection: 0) | |
itemSize = CGSize(width: cv.bounds.width - (interitemSpace * 2.0), | |
height: cv.bounds.height - (contentInsets.top + contentInsets.bottom)) | |
// Pre-variables | |
layoutAttributes = [] | |
var currentX: CGFloat = 0 | |
// Calculating the attributes for all the items. | |
// For large collection views more a thouthand item | |
// Consider splitting these calculations into chunks | |
for item in 0..<itemCount { | |
// Create attributes for each item | |
let indexPath = IndexPath(item: item, section: 0) | |
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) | |
// Set attributes size | |
attributes.size = itemSize | |
// Set attributes center | |
let xCenter = currentX + (itemSize.width / 2.0) | |
let yCenter = cv.bounds.midY | |
attributes.center = CGPoint(x: xCenter, y: yCenter) | |
// Append to cache | |
layoutAttributes.append(attributes) | |
// Shift current x with item width and interitem spacing | |
currentX += itemAndSpaceWidth | |
} | |
} | |
// MARK: - Layout Attributes | |
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { | |
guard let cv = collectionView else { return nil } | |
let visibleRect = CGRect(origin: cv.contentOffset, size: cv.bounds.size) | |
for attributes in layoutAttributes where visibleRect.intersects(attributes.frame) { | |
attributes.transform3D = getTransform3D(for: attributes) | |
} | |
return layoutAttributes.filter { rect.intersects($0.frame) } | |
} | |
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { | |
layoutAttributes.filter { $0.indexPath == indexPath }.first | |
} | |
// MARK: - Adjust contentOffset | |
override func targetContentOffset( | |
forProposedContentOffset proposedContentOffset: CGPoint, | |
withScrollingVelocity velocity: CGPoint) -> CGPoint | |
{ | |
guard let cv = collectionView else { return proposedContentOffset } | |
// Get target item | |
let targetX = proposedContentOffset.x | |
let targetMidX = targetX + (cv.bounds.width / 2.0) | |
let targetItem = floor(targetMidX / itemAndSpaceWidth) | |
// Calculate adjusted offset | |
let adjustedX = (targetItem * itemAndSpaceWidth) - contentInsets.left | |
return CGPoint(x: adjustedX, y: proposedContentOffset.y) | |
} | |
// MARK: - Layout invalidations | |
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { | |
true | |
} | |
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { | |
if context.invalidateEverything || context.invalidateDataSourceCounts { | |
layoutAttributes = [] | |
} | |
super.invalidateLayout(with: context) | |
} | |
// MARK: - Transform Calculator | |
private func getTransform3D(for attributes: UICollectionViewLayoutAttributes) -> CATransform3D { | |
var prespective = CATransform3DIdentity | |
prespective.m34 = -1.0 / 400 | |
let angle = getAngle(for: attributes) | |
var transform = CATransform3DRotate(prespective, angle, 0, 1, 0) | |
transform = CATransform3DTranslate(transform, angle * 125, 0, 0) | |
return transform | |
} | |
private func getAngle(for attributes: UICollectionViewLayoutAttributes) -> CGFloat { | |
guard let cv = collectionView else { return .zero } | |
let visibleRect = CGRect(origin: cv.contentOffset, size: cv.bounds.size) | |
let center = CGPoint(x: visibleRect.midX, y: visibleRect.midY) | |
let itemDistanceFromCenter = attributes.center.x - center.x | |
let totalSpaceFromCenterToEdge = (visibleRect.maxX - visibleRect.minX) / 2.0 | |
// Capping the factor between -1, 1 | |
let distanceFactor = max(-1, min(1, itemDistanceFromCenter / totalSpaceFromCenterToEdge)) | |
let angle = -1 * distanceFactor * maxRotationAngle | |
return angle | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice thanks 😊