Căn trái các ô trong UICollectionView


96

Tôi đang sử dụng UICollectionView trong dự án của mình, nơi có nhiều ô có độ rộng khác nhau trên một dòng. Theo: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

nó trải rộng các ô ra trên dòng với phần đệm bằng nhau. Điều này xảy ra như mong đợi, ngoại trừ tôi muốn căn chỉnh chúng và mã cứng một chiều rộng đệm.

Tôi nghĩ rằng tôi cần phân lớp UICollectionViewFlowLayout, tuy nhiên sau khi đọc một số hướng dẫn, v.v. trực tuyến, tôi dường như không hiểu cách này hoạt động.


Câu trả lời:


169

Các giải pháp khác trong chuỗi này không hoạt động bình thường, khi dòng chỉ gồm 1 mục hoặc quá phức tạp.

Dựa trên ví dụ do Ryan đưa ra, tôi đã thay đổi mã để phát hiện một dòng mới bằng cách kiểm tra vị trí Y của phần tử mới. Rất đơn giản và nhanh chóng trong hiệu suất.

Nhanh:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.y >= maxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}

Nếu bạn muốn có các khung nhìn bổ sung giữ nguyên kích thước của chúng, hãy thêm phần sau vào đầu phần đóng trong lệnh forEachgọi:

guard layoutAttribute.representedElementCategory == .cell else {
    return
}

Mục tiêu-C:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    CGFloat maxY = -1.0f;

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attribute in attributes) {
        if (attribute.frame.origin.y >= maxY) {
            leftMargin = self.sectionInset.left;
        }

        attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);

        leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
        maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
    }

    return attributes;
}

4
Cảm ơn! Tôi đã nhận được một cảnh báo, điều này đã được khắc phục bằng cách tạo một bản sao của các thuộc tính trong mảng let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou

2
Đối với bất cứ ai có thể đã stumbled khi này tìm kiếm một phiên bản căn giữa như tôi đã làm, tôi đã đăng một phiên bản sửa đổi của câu trả lời của thiên thần ở đây: stackoverflow.com/a/38254368/2845237
Alex Koshy

Tôi đã thử giải pháp này nhưng một số ô ở bên phải của chế độ xem bộ sưu tập vượt quá giới hạn của chế độ xem bộ sưu tập. Có ai khác gặp phải điều này?
Happiehappie

@Happiehappie vâng tôi cũng có hành vi đó. bất kỳ sửa chữa?
John Baum

Đã thử giải pháp này nhưng không thể kết hợp bố cục với collectionView trong bảng điều khiển Trình tạo giao diện. Kết thúc việc làm điều đó bằng mã. Bất kỳ manh mối tại sao?
acrespo

63

Có rất nhiều ý tưởng tuyệt vời được đưa vào câu trả lời cho câu hỏi này. Tuy nhiên, hầu hết chúng đều có một số nhược điểm:

  • Các giải pháp không kiểm tra giá trị y của ô chỉ hoạt động đối với bố cục một dòng . Chúng không thành công đối với bố cục dạng xem bộ sưu tập có nhiều dòng.
  • Giải pháp mà làm kiểm tra y giá trị như câu trả lời Thiên thần García Olloqui của công việc chỉ khi tất cả các tế bào có cùng một chiều cao . Chúng không thành công đối với các ô có chiều cao thay đổi.
  • Hầu hết các giải pháp chỉ ghi đè layoutAttributesForElements(in rect: CGRect)chức năng. Họ không ghi đè layoutAttributesForItem(at indexPath: IndexPath).Đây là một vấn đề vì chế độ xem bộ sưu tập định kỳ gọi hàm sau để truy xuất các thuộc tính bố cục cho một đường dẫn chỉ mục cụ thể. Nếu bạn không trả về các thuộc tính thích hợp từ hàm đó, bạn có thể gặp phải tất cả các loại lỗi trực quan, ví dụ: trong quá trình chèn và xóa hoạt ảnh của ô hoặc khi sử dụng ô tự định cỡ bằng cách thiết lập bố cục chế độ xem bộ sưu tập estimatedItemSize. Tài liệu của Apple cho biết:

    Mọi đối tượng bố cục tùy chỉnh được mong đợi để triển khai layoutAttributesForItemAtIndexPath:phương thức.

  • Nhiều giải pháp cũng đưa ra giả định về recttham số được chuyển đếnlayoutAttributesForElements(in rect: CGRect) hàm. Ví dụ, nhiều người dựa trên giả định rằng rectluôn bắt đầu ở đầu một dòng mới, điều này không nhất thiết phải như vậy.

Nói cách khác:

Hầu hết các giải pháp được đề xuất trên trang này hoạt động cho một số ứng dụng cụ thể, nhưng chúng không hoạt động như mong đợi trong mọi tình huống.


AlignedCollectionViewFlowLayout

Để giải quyết những vấn đề này, tôi đã tạo một UICollectionViewFlowLayoutlớp con theo một ý tưởng tương tự như được đề xuất bởi mattChris Wagner trong câu trả lời của họ cho một câu hỏi tương tự. Nó có thể căn chỉnh các ô

⬅︎ trái :

Bố cục căn trái

hoặc ➡︎ đúng :

Bố cục căn phải

và cung cấp thêm tùy chọn để theo chiều dọc căn chỉnh các ô trong các hàng tương ứng của chúng (trong trường hợp chúng khác nhau về chiều cao).

Bạn chỉ cần tải xuống tại đây:

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

Cách sử dụng được trình bày rõ ràng và được giải thích trong tệp README. Về cơ bản, bạn tạo một phiên bản AlignedCollectionViewFlowLayout, chỉ định căn chỉnh mong muốn và gán nó vào thuộc tính của chế độ xem bộ sưu tập của bạn collectionViewLayout:

 let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, 
                                                         verticalAlignment: .top)

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Nó cũng có sẵn trên Cocoapods .)


Cách hoạt động (đối với các ô căn trái):

Khái niệm ở đây là chỉ dựa vào layoutAttributesForItem(at indexPath: IndexPath)chức năng . Trong layoutAttributesForElements(in rect: CGRect)chúng ta chỉ cần lấy các đường dẫn chỉ mục của tất cả các ô bên trong rectvà sau đó gọi hàm đầu tiên cho mọi đường dẫn chỉ mục để truy xuất các khung chính xác:

override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    // We may not change the original layout attributes 
    // or UICollectionViewFlowLayout might complain.
    let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))

    layoutAttributesObjects?.forEach({ (layoutAttributes) in
        if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
            let indexPath = layoutAttributes.indexPath
            // Retrieve the correct frame from layoutAttributesForItem(at: indexPath):
            if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
                layoutAttributes.frame = newFrame
            }
        }
    })

    return layoutAttributesObjects
}

( copy()Hàm chỉ đơn giản là tạo một bản sao sâu của tất cả các thuộc tính bố cục trong mảng. Bạn có thể xem mã nguồn để triển khai nó.)

Vì vậy, bây giờ điều duy nhất chúng ta phải làm là thực hiện layoutAttributesForItem(at indexPath: IndexPath)đúng chức năng. Siêu lớp UICollectionViewFlowLayoutđã đặt đúng số ô trong mỗi dòng nên chúng ta chỉ phải dịch chuyển chúng sang trái trong hàng tương ứng của chúng. Khó khăn nằm ở việc tính toán lượng không gian chúng ta cần để chuyển mỗi ô sang trái.

Vì chúng ta muốn có một khoảng cách cố định giữa các ô, ý tưởng cốt lõi là chỉ cần giả sử rằng ô trước đó (ô bên trái của ô hiện đang được bố trí) đã được đặt đúng vị trí. Sau đó, chúng ta chỉ phải thêm khoảng cách ô vào maxXgiá trị của khung của ô trước đó và đó là origin.xgiá trị cho khung của ô hiện tại.

Bây giờ chúng ta chỉ cần biết khi nào chúng ta đã đến đầu dòng, để chúng ta không căn chỉnh một ô bên cạnh một ô trong dòng trước đó. (Điều này không chỉ dẫn đến bố cục không chính xác mà còn có độ trễ cực cao.) Vì vậy, chúng ta cần có một mỏ neo đệ quy. Cách tiếp cận tôi sử dụng để tìm mỏ neo đệ quy đó là như sau:

Để tìm hiểu xem ô ở chỉ mục i có ở cùng dòng với ô có chỉ số i-1 hay không ...

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
 |  inset  |     |intersection|        |                     |   line rect  |  inset  |
 |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
 | (left)  |     |            |             current item                    | (right) |
 |         |     +------------+                                             |         |
 |         |     previous item                                              |         |
 +---------+----------------------------------------------------------------+---------+

... Tôi "vẽ" một hình chữ nhật xung quanh ô hiện tại và kéo dài nó qua chiều rộng của toàn bộ chế độ xem bộ sưu tập. Vì căn giữa UICollectionViewFlowLayouttất cả các ô theo chiều dọc, mọi ô trong cùng một dòng phải giao với hình chữ nhật này.

Do đó, tôi chỉ cần kiểm tra xem ô có chỉ số i-1 có giao nhau với hình chữ nhật dòng này được tạo từ ô có chỉ số i hay không .

  • Nếu nó giao nhau, ô có chỉ số i không phải là ô bên trái nhất trong dòng.
    → Lấy khung của ô trước đó (với chỉ số i-1 ) và di chuyển ô hiện tại sang bên cạnh nó.

  • Nếu nó không giao nhau, ô có chỉ số i là ô bên trái nhất trong dòng.
    → Di chuyển ô sang cạnh trái của chế độ xem bộ sưu tập (mà không thay đổi vị trí dọc của ô).

Tôi sẽ không đăng việc triển khai thực sự của layoutAttributesForItem(at indexPath: IndexPath)chức năng ở đây vì tôi nghĩ phần quan trọng nhất là hiểu ý tưởng và bạn luôn có thể kiểm tra việc triển khai của tôi trong mã nguồn . (Nó phức tạp hơn một chút so với giải thích ở đây vì tôi cũng cho phép .rightcăn chỉnh và các tùy chọn căn chỉnh dọc khác nhau. Tuy nhiên, nó tuân theo cùng một ý tưởng.)


Chà, tôi đoán đây là câu trả lời dài nhất mà tôi từng viết trên Stackoverflow. Tôi hi vọng cái này giúp được. 😉


Tôi áp dụng với kịch bản này stackoverflow.com/questions/56349052/… nhưng nó không hoạt động. bạn có thể cho tôi biết làm thế nào tôi có thể đạt được điều này
Nazmul Hasan

Ngay cả với điều này (đó là một dự án thực sự tuyệt vời), tôi vẫn thấy nhấp nháy trong chế độ xem bộ sưu tập cuộn dọc ứng dụng của mình. Tôi có 20 ô, và trên cuộn đầu tiên, ô đầu tiên trống và tất cả các ô được chuyển sang phải một vị trí. Sau một lần cuộn lên và xuống đầy đủ, mọi thứ sẽ căn chỉnh đúng.
Parth Tamane

Dự án tuyệt vời! Thật tuyệt khi thấy nó được kích hoạt cho Carthage.
pjuzeliunas

30

Với Swift 4.1 và iOS 11, tùy theo nhu cầu của bạn, bạn có thể chọn một trong 2 cách triển khai hoàn chỉnh sau để khắc phục sự cố của mình.


# 1. Căn trái tự động lưu trữ UICollectionViewCells

Việc triển khai bên dưới cho thấy cách sử dụng UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' estimatedItemSizeUILabel' preferredMaxLayoutWidthđể căn trái các ô tự động lưu trữ trong một UICollectionView:

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]

    let columnLayout = FlowLayout(
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return array.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        cell.label.text = array[indexPath.row]
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .orange
        label.preferredMaxLayoutWidth = 120
        label.numberOfLines = 0

        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
        contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
        contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
        contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Kết quả mong đợi:

nhập mô tả hình ảnh ở đây


# 2. Căn trái UICollectionViewCells với kích thước cố định

Việc triển khai bên dưới cho thấy cách sử dụng UICollectionViewLayout's layoutAttributesForElements(in:)UICollectionViewFlowLayout' itemSizeđể căn trái các ô với kích thước xác định trước trongUICollectionView :

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let columnLayout = FlowLayout(
        itemSize: CGSize(width: 140, height: 140),
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 7
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        self.itemSize = itemSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .cyan
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Kết quả mong đợi:

nhập mô tả hình ảnh ở đây


Khi nhóm các thuộc tính ô theo hàng, việc sử dụng 10một số tùy ý có được không?
amariduran

25

Giải pháp đơn giản trong năm 2019

Đây là một trong những câu hỏi đáng buồn khi mọi thứ đã thay đổi rất nhiều trong những năm qua. Nó bây giờ là dễ dàng.

Về cơ bản, bạn chỉ làm điều này:

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // and, obviously start fresh again each row

Tất cả những gì bạn cần bây giờ là mã boilerplate:

override func layoutAttributesForElements(
                  in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
    
    guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
    var x: CGFloat = sectionInset.left
    var y: CGFloat = -1.0
    
    for a in att {
        if a.representedElementCategory != .cell { continue }
        
        if a.frame.origin.y >= y { x = sectionInset.left }
        
        a.frame.origin.x = x
        x += a.frame.width + minimumInteritemSpacing
        
        y = a.frame.maxY
    }
    return att
}

Chỉ cần sao chép và dán nó vào UICollectionViewFlowLayout - bạn đã hoàn tất.

Giải pháp làm việc đầy đủ để sao chép và dán:

Đây là toàn bộ điều:

class TagsLayout: UICollectionViewFlowLayout {
    
    required override init() {super.init(); common()}
    required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
    
    private func common() {
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        minimumLineSpacing = 10
        minimumInteritemSpacing = 10
    }
    
    override func layoutAttributesForElements(
                    in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        guard let att = super.layoutAttributesForElements(in:rect) else {return []}
        var x: CGFloat = sectionInset.left
        var y: CGFloat = -1.0
        
        for a in att {
            if a.representedElementCategory != .cell { continue }
            
            if a.frame.origin.y >= y { x = sectionInset.left }
            a.frame.origin.x = x
            x += a.frame.width + minimumInteritemSpacing
            y = a.frame.maxY
        }
        return att
    }
}

nhập mô tả hình ảnh ở đây

Và cuối cùng...

Cảm ơn @AlexShubin ở trên, người đầu tiên đã làm rõ điều này!


Chào. Tôi có đôi chút hoang mang. Thêm khoảng trắng cho mọi chuỗi ở đầu và cuối vẫn hiển thị từ của tôi trong ô mục với khoảng trống tôi đã thêm. Bất kỳ ý tưởng về cách làm điều đó?
Mohamed Lee

Nếu bạn có tiêu đề phần trong chế độ xem bộ sưu tập của mình, điều này dường như giết chết chúng.
TylerJames

22

Câu hỏi đã được đưa ra một thời gian nhưng không có câu trả lời và đó là một câu hỏi hay. Câu trả lời là ghi đè một phương thức trong lớp con UICollectionViewFlowLayout:

@implementation MYFlowLayoutSubclass

//Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this. 
#define ITEM_SPACING 10.0f

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left; //will add outside loop
        } else {
            CGRect newLeftAlignedFrame = attributes.frame;
            newLeftAlignedFrame.origin.x = leftMargin;
            attributes.frame = newLeftAlignedFrame;
        }

        leftMargin += attributes.frame.size.width + ITEM_SPACING;
        [newAttributesForElementsInRect addObject:attributes];
    }   

    return newAttributesForElementsInRect;
}

@end

Theo khuyến nghị của Apple, bạn lấy các thuộc tính bố cục từ super và lặp lại chúng. Nếu nó là chữ đầu tiên trong hàng (được xác định bởi origin. X nằm ở lề bên trái), bạn để nguyên và đặt lại x thành 0. Sau đó, đối với ô đầu tiên và mọi ô, bạn thêm chiều rộng của ô đó cộng với một số lề. Điều này được chuyển đến mục tiếp theo trong vòng lặp. Nếu nó không phải là mục đầu tiên, bạn đặt nó origin.x thành lề được tính toán đang chạy và thêm các phần tử mới vào mảng.


Tôi nghĩ cách này tốt hơn giải pháp của Chris Wagner ( stackoverflow.com/a/15554667/482557 ) cho câu hỏi được liên kết — việc triển khai của bạn không có lệnh gọi UICollectionViewLayout lặp lại nào.
Evan R

1
Có vẻ như điều này sẽ không hoạt động nếu hàng có một mục duy nhất vì UICollectionViewFlowLayout căn giữa nó. Bạn có thể kiểm tra trung tâm để giải quyết vấn đề. Một cái gì đó giống nhưattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos

Tôi đã triển khai điều này, nhưng vì một số lý do, nó bị hỏng đối với phần đầu tiên: Ô đầu tiên xuất hiện ở bên trái của hàng đầu tiên và ô thứ hai (không khớp trên cùng một hàng) chuyển sang hàng tiếp theo, nhưng không phải bên trái thẳng hàng. Thay vào đó, vị trí x của nó bắt đầu từ nơi ô đầu tiên kết thúc.
Nicolas Miari

@NicolasMiari Vòng lặp quyết định đó là một hàng mới và đặt lại leftMarginbằng if (attributes.frame.origin.x == self.sectionInset.left) {dấu kiểm. Điều này giả định rằng bố cục mặc định đã căn chỉnh ô của hàng mới để bằng với sectionInset.left. Nếu không đúng như vậy, hành vi bạn mô tả sẽ dẫn đến kết quả. Tôi sẽ chèn một điểm ngắt bên trong vòng lặp và kiểm tra cả hai bên cho ô của hàng thứ hai ngay trước khi so sánh. May mắn nhất.
Mike Sand

3
Cảm ơn ... Tôi đã kết thúc việc sử dụng UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Nó ghi đè một vài phương thức khác.
Nicolas Miari

9

Tôi cũng gặp sự cố tương tự, Hãy dùng thử Cocoapod UICollectionViewLeftAlignedLayout . Chỉ cần đưa nó vào dự án của bạn và khởi tạo nó như thế này:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];

Thiếu dấu ngoặc vuông mở trong lần khởi tạo bố cục của bạn ở đó
RyanOfCourse

Tôi đã kiểm tra hai câu trả lời github.com/eroth/ERJustifiedFlowLayout và UICollectionViewLeftAlignedLayout. Chỉ cái thứ hai mới tạo ra kết quả phù hợp khi sử dụng ước lượngItemSize (tự định cỡ).
EckhardN

5

Dựa trên câu trả lời của Michael Sand , tôi đã tạo một phân lớpUICollectionViewFlowLayout thư viện để thực hiện điều chỉnh theo chiều ngang trái, phải hoặc đầy đủ (về cơ bản là mặc định) — nó cũng cho phép bạn đặt khoảng cách tuyệt đối giữa mỗi ô. Tôi cũng có kế hoạch thêm biện minh trung tâm ngang và biện minh dọc cho nó.

https://github.com/eroth/ERJustifiedFlowLayout


5

Đây là câu trả lời ban đầu trong Swift. Hầu hết nó vẫn hoạt động tốt.

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElementsInRect(rect)

        var leftMargin = sectionInset.left

        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
        }

        return attributes
    }
}

Ngoại lệ: Tự động định kích thước ô

Đáng buồn là có một ngoại lệ lớn. Nếu bạn đang sử dụng UICollectionViewFlowLayout's estimatedItemSize. Nội bộ UICollectionViewFlowLayoutđang thay đổi mọi thứ một chút. Tôi đã không theo dõi nó hoàn toàn nhưng rõ ràng nó gọi các phương thức khác sau layoutAttributesForElementsInRectkhi tự định cỡ ô. Từ thử nghiệm và lỗi của tôi, tôi thấy nó dường như gọi layoutAttributesForItemAtIndexPathcho từng ô riêng lẻ trong quá trình tự động định cỡ thường xuyên hơn. Cập nhật này LeftAlignedFlowLayouthoạt động tuyệt vời với estimatedItemSize. Nó cũng hoạt động với các ô có kích thước tĩnh, tuy nhiên, các lệnh gọi bố cục bổ sung khiến tôi sử dụng câu trả lời ban đầu bất cứ lúc nào tôi không cần các ô tự động thay đổi kích thước.

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes

        // First in a row.
        if layoutAttribute?.frame.origin.x == sectionInset.left {
            return layoutAttribute
        }

        // We need to align it to the previous item.
        let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
        guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else {
            return layoutAttribute
        }

        layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing

        return layoutAttribute
    }
}

Điều này hoạt động cho phần đầu tiên nhưng nhãn không thay đổi kích thước trên phần thứ hai cho đến khi bảng cuộn. Chế độ xem bộ sưu tập của tôi nằm trong ô bảng.
EI Captain v2.0

5

Nhanh chóng. Theo câu trả lời của Michaels

override func layoutAttributesForElementsInRect(rect: CGRect) ->     [UICollectionViewLayoutAttributes]? {
    guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else {
        return super.layoutAttributesForElementsInRect(rect)
    }
    let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED
    var newAttributes = [UICollectionViewLayoutAttributes]()
    var leftMargin = self.sectionInset.left
    for attributes in oldAttributes {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left
        } else {
            var newLeftAlignedFrame = attributes.frame
            newLeftAlignedFrame.origin.x = leftMargin
            attributes.frame = newLeftAlignedFrame
        }

        leftMargin += attributes.frame.width + spacing
        newAttributes.append(attributes)
    }
    return newAttributes
}

4

Dựa trên tất cả các câu trả lời, tôi thay đổi một chút và nó phù hợp với tôi

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0


    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY
                   || layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }

        if layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }
        else {
            layoutAttribute.frame.origin.x = leftMargin
        }

        leftMargin += layoutAttribute.frame.width
        maxY = max(layoutAttribute.frame.maxY, maxY)
    }

    return attributes
}

4

Nếu bất kỳ ai trong số các bạn gặp vấn đề - một số ô ở bên phải của chế độ xem bộ sưu tập vượt quá giới hạn của chế độ xem bộ sưu tập . Sau đó, hãy sử dụng cái này -

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin : CGFloat = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }
        return attributes
    }
}

Sử dụng INT thay cho việc so sánh các giá trị CGFloat .


3

Dựa trên câu trả lời ở đây, nhưng đã khắc phục sự cố và sự cố căn chỉnh khi chế độ xem bộ sưu tập của bạn cũng có đầu trang hoặc chân trang. Căn chỉnh chỉ các ô bên trái:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var prevMaxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in

            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if layoutAttribute.frame.origin.y >= prevMaxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            prevMaxY = layoutAttribute.frame.maxY
        }

        return attributes
    }
}

2

Cảm ơn câu trả lời của Michael Sand . Tôi đã sửa đổi nó thành giải pháp gồm nhiều hàng (cùng căn chỉnh Trên cùng của mỗi hàng) là Căn trái, khoảng cách đều cho mỗi mục.

static CGFloat const ITEM_SPACING = 10.0f;

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    CGRect contentRect = {CGPointZero, self.collectionViewContentSize};

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init];

    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        UICollectionViewLayoutAttributes *attr = attributes.copy;

        CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue];
        if (lastLeftMargin == 0) lastLeftMargin = leftMargin;

        CGRect newLeftAlignedFrame = attr.frame;
        newLeftAlignedFrame.origin.x = lastLeftMargin;
        attr.frame = newLeftAlignedFrame;

        lastLeftMargin += attr.frame.size.width + ITEM_SPACING;
        [leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]];
        [newAttributesForElementsInRect addObject:attr];
    }

    return newAttributesForElementsInRect;
}

1

Đây là hành trình của tôi để tìm mã tốt nhất hoạt động với Swift 5. Tôi đã kết hợp một số câu trả lời từ chuỗi này và một số chuỗi khác để giải quyết các cảnh báo và vấn đề mà tôi gặp phải. Tôi đã có cảnh báo và một số hành vi bất thường khi cuộn qua chế độ xem bộ sưu tập của mình. Giao diện điều khiển sẽ in như sau:

Điều này có thể xảy ra vì bố cục luồng "xyz" đang sửa đổi các thuộc tính do UICollectionViewFlowLayout trả về mà không sao chép chúng.

Một vấn đề khác mà tôi phải đối mặt là một số ô dài đang bị cắt ở phía bên phải của màn hình. Ngoài ra, tôi đã thiết lập các nội dung phần và tối thiểuInteritemSpacing trong các hàm đại biểu dẫn đến các giá trị không được phản ánh trong lớp tùy chỉnh. Cách khắc phục điều đó là đặt các thuộc tính đó thành một phiên bản của bố cục trước khi áp dụng nó vào chế độ xem bộ sưu tập của tôi.

Đây là cách tôi sử dụng bố cục cho chế độ xem bộ sưu tập của mình:

let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)

Đây là lớp bố cục luồng

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) || layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }

            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}

Bạn đã cứu một ngày của tôi
David 'mArm' Ansermot

0

Vấn đề với UICollectionView là nó cố gắng tự động điều chỉnh các ô trong vùng có sẵn. Tôi đã thực hiện việc này bằng cách xác định số hàng và cột trước tiên, sau đó xác định kích thước ô cho hàng và cột đó

1) Để xác định các Phần (Hàng) của UICollectionView của tôi:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Để xác định số lượng mục trong một phần. Bạn có thể xác định số lượng mục khác nhau cho mỗi phần. bạn có thể lấy số phần bằng tham số 'section'.

(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

3) Để xác định kích thước ô cho từng phần và hàng riêng biệt. Bạn có thể lấy số phần và số hàng bằng cách sử dụng tham số 'indexPath' tức là [indexPath section]cho số phần và [indexPath row]cho số hàng.

(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

4) Sau đó, bạn có thể hiển thị các ô của mình theo hàng và phần bằng cách sử dụng:

(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

LƯU Ý: Trong UICollectionView

Section == Row
IndexPath.Row == Column

0

Câu trả lời của Mike Sand là tốt nhưng tôi đã gặp một số vấn đề với mã này (Giống như ô dài bị cắt bớt). Và mã mới:

#define ITEM_SPACE 7.0f

@implementation LeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
        if (nil == attributes.representedElementKind) {
            NSIndexPath* indexPath = attributes.indexPath;
            attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
        }
    }
    return attributesToReturn;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* currentItemAttributes =
    [super layoutAttributesForItemAtIndexPath:indexPath];

    UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];

    if (indexPath.item == 0) { // first item of section
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
        currentItemAttributes.frame = frame;

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + ITEM_SPACE;

    CGRect currentFrame = currentItemAttributes.frame;
    CGRect strecthedCurrentFrame = CGRectMake(0,
                                              currentFrame.origin.y,
                                              self.collectionView.frame.size.width,
                                              currentFrame.size.height);

    if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
        // the approach here is to take the current frame, left align it to the edge of the view
        // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
        // is on the same line, otherwise it is on it's own new line
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = frame;
        return currentItemAttributes;
    }

    CGRect frame = currentItemAttributes.frame;
    frame.origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = frame;
    return currentItemAttributes;
}

0

Đã chỉnh sửa câu trả lời của Angel García Olloqui để tôn trọng sự tôn trọng minimumInteritemSpacingcủa đại biểu collectionView(_:layout:minimumInteritemSpacingForSectionAt:), nếu nó thực hiện nó.

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0
    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY {
            leftMargin = sectionInset.left
        }

        layoutAttribute.frame.origin.x = leftMargin

        let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout
        let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing

        leftMargin += layoutAttribute.frame.width + spacing
        maxY = max(layoutAttribute.frame.maxY , maxY)
    }

    return attributes
}

0

Đoạn mã trên phù hợp với tôi. Tôi muốn chia sẻ mã Swift 3.0 tương ứng.

class SFFlowLayout: UICollectionViewFlowLayout {

    let itemSpacing: CGFloat = 3.0

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attriuteElementsInRect = super.layoutAttributesForElements(in: rect)
        var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = []
        var leftMargin = self.sectionInset.left
        for tempAttribute in attriuteElementsInRect! {
            let attribute = tempAttribute 
            if attribute.frame.origin.x == self.sectionInset.left {
                leftMargin = self.sectionInset.left
            }
            else {
                var newLeftAlignedFrame = attribute.frame
                newLeftAlignedFrame.origin.x = leftMargin
                attribute.frame = newLeftAlignedFrame
            }
            leftMargin += attribute.frame.size.width + itemSpacing
            newAttributeForElement.append(attribute)
        }
        return newAttributeForElement
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.