web-dev-qa-db-ja.com

水平ページングをApp Storeのような複数行のコレクションビューにスナップする方法は?

複数行のApp Storeのコレクションビューでページングを複製したいと思います。

enter image description here

これまでのところ、前のセルと次のセルのピークを表示するなど、見た目にできるだけ近いように設計しましたが、ページングを機能させて次の3つのグループをスナップする方法がわかりません。

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView.collectionViewLayout = MultiRowLayout(
        rowsCount: 3,
        inset: 16
    )
}

...

class MultiRowLayout: UICollectionViewFlowLayout {
    private var rowsCount: CGFloat = 0

    convenience init(rowsCount: CGFloat, spacing: CGFloat? = nil, inset: CGFloat? = nil) {
        self.init()

        self.scrollDirection = .horizontal
        self.minimumInteritemSpacing = 0
        self.rowsCount = rowsCount

        if let spacing = spacing {
            self.minimumLineSpacing = spacing
        }

        if let inset = inset {
            self.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
        }
    }

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }
        self.itemSize = calculateItemSize(from: collectionView.bounds.size)
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        guard let collectionView = collectionView,
            !newBounds.size.equalTo(collectionView.bounds.size) else {
                return false
        }

        itemSize = calculateItemSize(from: collectionView.bounds.size)
        return true
    }
}

private extension MultiRowLayout {

    func calculateItemSize(from bounds: CGSize) -> CGSize {
        return CGSize(
            width: bounds.width - minimumLineSpacing * 2 - sectionInset.left,
            height: bounds.height / rowsCount
        )
    }
}

残念ながら、isPagingEnabledのネイティブのUICollectionViewフラグは、セルがコレクションビューの100%の幅の場合にのみ機能するため、ユーザーは前のセルと前のセルと次のセルを確認できません。

私は動作している スナップページング機能 ですが、この3行のコレクションではなく、ページごとに1つのアイテムに対してのみです。誰かが、ページごとに1つのアイテムではなく、グループ化された行に対してスナップページングを機能させるのに役立ちますか?

12
TruMan1

この動作のためだけにUICollectionViewFlowLayoutをサブクラス化する理由はありません。

UICollectionViewUIScrollViewのサブクラスなので、そのデリゲートプロトコルUICollectionViewDelegateUIScrollViewDelegateのサブタイプです。つまり、コレクションビューのデリゲートにUIScrollViewDelegateのメソッドを実装できます。

コレクションビューのデリゲートでscrollViewWillEndDragging(_:withVelocity:targetContentOffset:)を実装して、ターゲットコンテンツのオフセットを最も近いセルの列の左上隅に丸めます。

次に実装例を示します。

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let layout = collectionViewLayout as! UICollectionViewFlowLayout
    let bounds = scrollView.bounds
    let xTarget = targetContentOffset.pointee.x

    // This is the max contentOffset.x to allow. With this as contentOffset.x, the right Edge of the last column of cells is at the right Edge of the collection view's frame.
    let xMax = scrollView.contentSize.width - scrollView.bounds.width

    if abs(velocity.x) <= snapToMostVisibleColumnVelocityThreshold {
        let xCenter = scrollView.bounds.midX
        let poses = layout.layoutAttributesForElements(in: bounds) ?? []
        // Find the column whose center is closest to the collection view's visible rect's center.
        let x = poses.min(by: { abs($0.center.x - xCenter) < abs($1.center.x - xCenter) })?.frame.Origin.x ?? 0
        targetContentOffset.pointee.x = x
    } else if velocity.x > 0 {
        let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
        // Find the leftmost column beyond the current position.
        let xCurrent = scrollView.contentOffset.x
        let x = poses.filter({ $0.frame.Origin.x > xCurrent}).min(by: { $0.center.x < $1.center.x })?.frame.Origin.x ?? xMax
        targetContentOffset.pointee.x = min(x, xMax)
    } else {
        let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget - bounds.size.width, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
        // Find the rightmost column.
        let x = poses.max(by: { $0.center.x < $1.center.x })?.frame.Origin.x ?? 0
        targetContentOffset.pointee.x = max(x, 0)
    }
}

// Velocity is measured in points per millisecond.
private var snapToMostVisibleColumnVelocityThreshold: CGFloat { return 0.3 }

結果:

demo

ここで私のテストプロジェクトの完全なソースコードを見つけることができます: https://github.com/mayoff/multiRowSnapper

17
rob mayoff

UICollectionView(。scrollDirection = .horizo​​ntal)は、個々のUICollectionViewCellに各リストを格納するための外部コンテナーとして使用できます。

各リストは、個別のUICollectionView(。scrollDirection = .vertical)を使用して構築できます。

collectionView.isPagingEnabled = trueを使用して、外側のUICollectionViewでページングを有効にします。

スクロールビューでページングを有効にするかどうかを決定するブール値。このプロパティの値がtrueの場合、ユーザーがスクロールすると、スクロールビューはスクロールビューの境界の倍数で停止します。デフォルト値はfalseです。

注:左右のコンテンツインセットをリセットして、各ページの側面の余分なスペースを削除します。例えばcollectionView?.contentInset = UIEdgeInsetsMake(0、0、0、0)

2
RSG