CollectionView ๊ฐ๋ก ์คํฌ๋กค์ ํ ๋ ์ฌ์ด์ฆ๊ฐ ๋์ ์ผ๋ก ์์ง์ด๊ฒ ๊ตฌํ
(๊ฐ ์น์ ์ด ๊ฐ๋ก ์คํฌ๋กค์ ํ๋ ๊ฒ์ด์ง, ์ ์ฒด ์ปฌ๋ ์ ๋ทฐ์ ์คํฌ๋กค๋ทฐ๊ฐ ๊ฐ๋ก ์คํฌ๋กค์ ํ๋ ๊ฒ์ด ์๋๊ธฐ ๋๋ฌธ)
๋งค์ฐ.. ๋ณต์กํ ์ฝ๋โฆ
final class CarouselFlowLayout: UICollectionViewFlowLayout {
private var sideItemScale: CGFloat = 0.6
private var spacing: CGFloat = 40
// collectionView์ ํฌ๊ธฐ
private var size: CGSize = .zero
// ์ฒ์ ์ปฌ๋ ์
๋ทฐ๊ฐ ๋ํ๋ ๋ ํธ์ถ๋๊ฑฐ๋ ๋ ์ด์์์ ๋ช
์์ ํน์ ์๋ฌต์ ์ผ๋ก ๋ฌดํจํํ์ ๋ ํธ์ถ
override func prepare() {
super.prepare()
let currentSize = collectionView?.bounds.size ?? .zero
if currentSize != size {
self.setupCollectionView()
self.updateLayout()
self.size = currentSize
}
}
private func setupCollectionView() {
guard let collectionView = collectionView else { return }
// ์ปฌ๋ ์
๋ทฐ์ ์คํฌ๋กค ๊ฐ์ ์๋๋ฅผ ์ค์
if collectionView.decelerationRate != .fast {
collectionView.decelerationRate = .fast
}
}
private func updateLayout() {
guard let collectionView = collectionView else { return }
let collectionSize = collectionView.bounds.size
let horizontalInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsets.init(top: 0, left: horizontalInset,
bottom: 0, right: horizontalInset)
let scaledItemOffset = (self.itemSize.width - self.itemSize.width * self.sideItemScale) / 2
self.minimumLineSpacing = self.spacing - scaledItemOffset
}
// ๋งค๋ฒ ๋ ์ด์์์ ์
๋ฐ์ดํธ ํ๋๋ก ์ค์ (๊ธฐ๋ณธ๊ฐ์ false)
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
// ๊ฐ ์์ดํ
์ ๋ ์ด์์ ์์ฑ
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.compactMap { self.transform($0) }
}
// ๊ฐ ์์ดํ
์ ๋ ์ด์์ ์์ฑ ๋ณํ
private func transform(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let contentOffsetX = collectionView.contentOffset.x
let normalizedCenter = attributes.center.x - contentOffsetX
let maxDistance = self.itemSize.width + self.minimumLineSpacing
// ์์ดํ
์ ์ค์๊ณผ ์ปฌ๋ ์
๋ทฐ์ ์ค์ ์ฌ์ด์ ๊ฑฐ๋ฆฌ๋ฅผ ๊ณ์ฐ
let distance = min(abs(collectionView.center.x - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance) / maxDistance // ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ scale ๋น์จ
// ๊ฑฐ๋ฆฌ์ ๋ฐ๋ผ ์์ดํ
์ ์ค์ผ์ผ(๋น์จ๋ก) ๊ณ์ฐ
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
attributes.alpha = 1
// ์์ดํ
์ ํฌ๊ธฐ์ ์์น ๋ณํ
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(scale * 10)
return attributes
}
// ์คํฌ๋กค์ด ๋๋๋ ค๋ ์์ ์ ํธ์ถ, ์คํฌ๋กค์ด ๋ฉ์ถ ์์น๋ฅผ ์ ์ด
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView,
collectionView.isPagingEnabled == false,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds) else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
let midSide = collectionView.bounds.size.width / 2
// ์ปฌ๋ ์
๋ทฐ์ ์ค์์ง์ ์ ์ ์๋ ์คํ์
์ ์ค์ ์ง์ ์ ๋ํด ์๋ก์ด ์ค์ ์ง์ ์ ๊ณ์ฐ
let proposedContentOffsetCenter = proposedContentOffset.x + midSide
let closest = layoutAttributes.min {
abs($0.center.x - proposedContentOffsetCenter) < abs($1.center.x - proposedContentOffsetCenter)
} ?? UICollectionViewLayoutAttributes()
let targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
return targetContentOffset
}
}
Compositional Layout
์ ๊ณต์ ๋ฌธ์์์ visibleItemsInvalidationHandler๋ฅผ ๋ฐ๊ฒฌํจ. /// Carousel ์ ์ ์ฉํ๊ธฐ ์ํด ์
์์ดํ
์ ์ค์ฌ๋ถ ๋ถํฐ์ ๊ฑฐ๋ฆฌ๋ฅผ ๊ณ์ฐ ํด transform ์ ์ ์ฉ
private func setupCollectionViewCarousel(to section: NSCollectionLayoutSection) {
section.visibleItemsInvalidationHandler = { visibleItems, offset, environment in
// ํค๋๊ฐ ์๋ ์
์์ดํ
๋ค
let cellItems = visibleItems.filter {
$0.representedElementKind != UICollectionView.elementKindSectionHeader
}
let containerWidth = environment.container.contentSize.width
cellItems.forEach { item in
let itemCenterRelativeToOffset = item.frame.midX - offset.x
// ์
์ด ์ปฌ๋ ์
๋ทฐ์ ์ค์์์ ์ผ๋ง๋ ๋จ์ด์ ธ ์๋์ง
let distanceFromCenter = abs(itemCenterRelativeToOffset - containerWidth / 2.0)
// ์
์ด ์ปค์ง๊ณ ์์์ง ๋์ ์ต๋ ์ค์ผ์ผ, ์ต์ ์ค์ผ์ผ
let minScale: CGFloat = 0.7
let maxScale: CGFloat = 1.0
let scale = max(maxScale - (distanceFromCenter / containerWidth), minScale)
item.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
}