上記画像のような両端(prev/next)の要素(view)が見えてる状態で、かつAndroidのsnapHelperが効いている(スクロールする際に1番表示領域の大きい要素を中央へスクロールされる)ようなHorizontalScrollの実装方法です。
本来両端の要素を表示しない場合は padding enabled
を使用すれば実現できますが、両端のprev/nextのviewを表示しながらの場合はpadding enabled
では実現できません。
基本的にCollectionViewを用いて実装します。
※collectionviewの基本的な設定やcollectionviewcellは省略します。
※そして雑に書きます。
※Androidはこちらです。
FlowLayout.swift
import UIKit final class FlowLayout: UICollectionViewFlowLayout { private var layoutAttributesForPaging: [UICollectionViewLayoutAttributes]? override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = collectionView else { return proposedContentOffset } guard let targetAttributes = layoutAttributesForPaging else { return proposedContentOffset } let nextAttributes: UICollectionViewLayoutAttributes? if velocity.x == 0 { nextAttributes = layoutAttributesForNearbyCenterX(in: targetAttributes, collectionView: collectionView) } else if velocity.x > 0 { nextAttributes = targetAttributes.last } else { nextAttributes = targetAttributes.first } guard let attributes = nextAttributes else { return proposedContentOffset } if attributes.representedElementKind == UICollectionView.elementKindSectionHeader { return CGPoint(x: 0, y: collectionView.contentOffset.y) } else { let cellLeftMargin = (collectionView.bounds.width - attributes.bounds.width) * 0.5 return CGPoint(x: attributes.frame.minX - cellLeftMargin, y: collectionView.contentOffset.y) } } private func layoutAttributesForNearbyCenterX(in attributes: [UICollectionViewLayoutAttributes], collectionView: UICollectionView) -> UICollectionViewLayoutAttributes? { let screenCenterX = collectionView.contentOffset.x + collectionView.bounds.width * 0.5 let result = attributes.reduce((attributes: nil as UICollectionViewLayoutAttributes?, distance: CGFloat.infinity)) { result, attributes in let distance = attributes.frame.midX - screenCenterX return abs(distance) < abs(result.distance) ? (attributes, distance) : result } return result.attributes } func prepareForPaging() { guard let collectionView = collectionView else { return } let expansionMargin = sectionInset.left + sectionInset.right let expandedVisibleRect = CGRect(x: collectionView.contentOffset.x - expansionMargin, y: 0, width: collectionView.bounds.width + (expansionMargin * 2), height: collectionView.bounds.height) layoutAttributesForPaging = layoutAttributesForElements(in: expandedVisibleRect)?.sorted { $0.frame.minX < $1.frame.minX } } }
ViewController.swift
import UIKit final class ViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! { didSet { collectionView.delegate = self collectionView.dataSource = self collectionView .register( UINib(nibName: "CollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "CollectionViewCell" ) flowLayout.scrollDirection = .horizontal flowLayout.minimumLineSpacing = 0 flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) collectionView.collectionViewLayout = flowLayout collectionView.decelerationRate = .fast } } private lazy var flowLayout = FlowLayout() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } } // MARK: UICollectionViewDelegateFlowLayout extension ViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize( width: self.collectionView.bounds.width * 0.7, height: self.collectionView.bounds.height * 0.7 ) } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.flowLayout.prepareForPaging() } } // MARK: UICollectionViewDataSource extension ViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell: CollectionViewCell = self.collectionView.dequeueReusableCell( withReuseIdentifier: "CollectionViewCell", for: indexPath ) as? CollectionViewCell else { return UICollectionViewCell() } return cell } }
実行すると以下のようになります。
scaleを変更する
次に中央に表示されている要素を拡大表示させる場合です。
private func transformScale(cell: UICollectionViewCell) { let cellCenter: CGPoint = self.collectionView.convert(cell.center, to: nil) let screenCenterX: CGFloat = UIScreen.main.bounds.width / 2 let reductionRatio: CGFloat = -0.0005 let maxScale: CGFloat = 1 let cellCenterDisX: CGFloat = abs(screenCenterX - cellCenter.x) let newScale = reductionRatio * cellCenterDisX + maxScale cell.transform = CGAffineTransform(scaleX: newScale, y: newScale) }
これをviewWillLayoutSubviews()
とUICollectionViewDelegateFlowLayout
のscrollViewDidScroll()
で、visibleCellsのcellを渡してあげる感じです。
self.collectionView.visibleCells.forEach { cell in self.transformScale(cell: cell) }
今回はCollectionViewでやりましたが、ScrollView+StackViewでもできるかと思います。
sampleはこちら↓