+2

Snapping với UICollectionView

UICollectionView được kế thừa từ UIScrollView, liệu nó cũng có việc phân trang như ScrollView?

Việc phân trang này phụ thuộc vào kích thước của các cell, nếu nó bằng kích thước màn hình thì sẽ có. Tuy nhiên nhiều trường hợp kích thước Cell không như vậy nó thường nhỏ hơn.

Chính vì thế bài viết này sẽ hướng dẫn các bạn xử lý khi cell nhỏ hơn kích thước của CollectionView. Chúng ta có thể xem ví dụ tương tự như AppStore :

Giờ hãy bắt đầu xây dựng project demo

1. Khởi tạo

Tạo 1 Single Project

Tiếp theo khởi tạo giao diện cùng với 1 CollectionView

2. Tạo file Snapping

Tiếp theo chúng ta sẽ tạo một file Custom UICollectionViewFlowLayout với tên SnappingCollectionViewLayout. Sau đó thay đổi layout cho CollectionView đã khởi tạo ở trên

Nhiệm vụ của file này là sẽ tùy chỉnh việc hiển thị của CollectionView bằng cách override lại phương thức

func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, 
                                                          withScrollingVelocity velocity: CGPoint) -> CGPoint

Trong phương thức này có 2 tham số là proposedContentOffset chỉ vị trí của collectionView đang hiển thị cùng với velocity là vận tốc trượt. Kết quả trả về sẽ là vị trí hiển thị mong muốn.

3. Xử lý với hàm targetContentOffset

Dựa vào velocity chúng ta có thể xác định ra được việc nguời dùng đã trượt collectionview như thế nào. Chúng ta sẽ đặt ra một vận tốc giới hạn nhỏ nhất là snapToMostVisibleColumnVelocityThreshold để giới hạn khi trượt. Sẽ có các TH sau:

  • |velocity.x| <= snapToMostVisibleColumnVelocityThreshold : Tìm vị trí Cell gần nhất để làm mốc hiển thị

  • velocity.x > 0: Tìm vị trí của cell gần nhất bên phải để chọn làm mốc hiển thị

  • velocity.x <0 : Tìm vị trí của cell gần nhất bên trái để chọn làm mốc hiển thị

Dựa vào các TH đó chúng ta tính toán và trả về vị trí tương ứng. Code xử lý:

import UIKit

final class SnappingCollectionViewLayout: UICollectionViewFlowLayout {
    
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
                                      withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset,
                                             withScrollingVelocity: velocity)
        }
        if abs(velocity.x) <= snapToMostVisibleColumnVelocityThreshold {
            let targetRect = CGRect(x: proposedContentOffset.x,
                                    y: 0,
                                    width: collectionView.bounds.size.width,
                                    height: collectionView.bounds.size.height)
            let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) ?? []
            if collectionView.bounds.origin.x + collectionView.bounds.size.width + Constants.collectionLeftPadding
                > collectionView.contentSize.width {
                let offsetAdjustment = layoutAttributesArray
                    .max(by: { $0.center.x < $1.center.x })?
                    .frame.origin.x ?? collectionView.contentSize.width
                return CGPoint(x: offsetAdjustment - Constants.collectionLeftPadding, y: proposedContentOffset.y)
            } else {
                var offsetAdjustment = CGFloat.greatestFiniteMagnitude
                layoutAttributesArray.forEach {
                    if abs($0.frame.origin.x - proposedContentOffset.x) < abs(offsetAdjustment) {
                        offsetAdjustment = $0.frame.origin.x - proposedContentOffset.x
                    }
                }
                return CGPoint(x: proposedContentOffset.x + offsetAdjustment - Constants.collectionLeftPadding,
                               y: proposedContentOffset.y)
            }
        } else if velocity.x > 0 {
            let targetRect = CGRect(x: proposedContentOffset.x,
                                    y: 0,
                                    width: collectionView.bounds.size.width,
                                    height: collectionView.bounds.size.height)
            let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) ?? []
            let offsetAdjustment = layoutAttributesArray
                .filter { $0.frame.origin.x > proposedContentOffset.x }
                .min(by: { $0.center.x < $1.center.x })?
                .frame.origin.x ?? 0.0
            return CGPoint(x: offsetAdjustment - Constants.collectionLeftPadding,
                           y: proposedContentOffset.y)
        } else {
            let targetRect = CGRect(x: proposedContentOffset.x - collectionView.bounds.size.width,
                                    y: 0,
                                    width: collectionView.bounds.size.width,
                                    height: collectionView.bounds.size.height)
            let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) ?? []
            let offsetAdjustment = layoutAttributesArray
                .max(by: { $0.center.x < $1.center.x })?
                .frame.origin.x ?? 0.0
            return CGPoint(x: offsetAdjustment - Constants.collectionLeftPadding, y: proposedContentOffset.y)
        }
    }
    
    private var snapToMostVisibleColumnVelocityThreshold: CGFloat { return 0.3 }
}

struct Constants {
    static let collectionLeftPadding: CGFloat = 16.0
}

4. Kết quả

Bài viết mong giúp bạn giải quyết được vấn đề nêu ở đầu. Tuy nó không mượt như AppStore nhưng cũng là 1 cách để bạn xử lý Snapping với UICollectionVierw


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí