web-dev-qa-db-ja.com

UICollectionView flowLayoutでセル間の間隔をどのように決定しますか

フローレイアウトのUICollectionViewがあり、各セルは正方形です。各行の各セル間の間隔を決定するにはどうすればよいですか?これに適した設定が見つからないようです。コレクションビューのnibファイルには最小間隔属性がありますが、これを0に設定すると、セルは固定されません。

enter image description here

他のアイデアは?

49
adit

更新:Swiftこの回答のバージョン: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift


@ matt's lead 私は彼のコードを修正して、アイテムが常に左揃えになるようにしました。アイテムが単独で行に配置された場合、フローレイアウトによって中央に配置されることがわかりました。この問題に対処するために、次の変更を加えました。

この状況は、幅が異なるセルがある場合にのみ発生し、次のようなレイアウトになる可能性があります。 UICollectionViewFlowLayoutの動作により、最後の行は常に左揃えになります。問題は、最後の行以外の任意の行にあるアイテムにあります。

@mattのコードで私は見ていました。

enter image description here

この例では、セルが単独で線上にある場合、セルが中央に配置されることがわかります。以下のコードは、コレクションビューがこのようになることを保証します。

enter image description here

#import "CWDLeftAlignedCollectionViewFlowLayout.h"

const NSInteger kMaxCellSpacing = 9;

@implementation CWDLeftAlignedCollectionViewFlowLayout

- (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 + kMaxCellSpacing;

    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;
}

@end
70
Chris Wagner

アイテム間の最大間隔を取得するには、UICollectionViewFlowLayoutをサブクラス化し、layoutAttributesForElementsInRect:およびlayoutAttributesForItemAtIndexPath:をオーバーライドします。

たとえば、一般的な問題は次のとおりです。コレクションビューの行は、左揃えの最後の行を除き、左右に揃えられます。 all行を左揃えにして、それらの間のスペースが10ポイントになるようにしたいとしましょう。これは簡単な方法です(UICollectionViewFlowLayoutサブクラスで):

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* arr = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* atts in arr) {
        if (nil == atts.representedElementKind) {
            NSIndexPath* ip = atts.indexPath;
            atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
        }
    }
    return arr;
}

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

    if (indexPath.item == 0) // degenerate case 1, first item of section
        return atts;

    NSIndexPath* ipPrev =
    [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];

    CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
    CGFloat rightPrev = fPrev.Origin.x + fPrev.size.width + 10;
    if (atts.frame.Origin.x <= rightPrev) // degenerate case 2, first item of line
        return atts;

    CGRect f = atts.frame;
    f.Origin.x = rightPrev;
    atts.frame = f;
    return atts;
}

これが非常に簡単な理由は、レイアウトの面倒な作業を実際に行っていないからです。 UICollectionViewFlowLayoutがすでに行ったレイアウト作業を活用しています。各行にいくつのアイテムを入れるかはすでに決めています。私が言っていることがわかるなら、それらの行を読んでアイテムを一緒に押しているだけです。

32
matt

考慮すべきことがいくつかあります。

  1. IBの最小間隔を変更してみますが、カーソルはそのフィールドに残します。 Xcodeはドキュメントを変更済みとしてすぐにマークしないことに注意してください。別のフィールドをクリックすると、Xcodeはドキュメントが変更されたことに気づき、ファイルナビゲータでそうマークします。そのため、変更を行った後は、タブで移動するか、別のフィールドをクリックしてください。

  2. 変更を加えた後、ストーリーボード/ xibファイルを保存し、必ずアプリを再構築してください。そのステップを見逃すことは難しくありません。そして、あなたは、あなたの変更が何の効果も持たないように思われたのか疑問に思って頭をかき残しています。

  3. UICollectionViewFlowLayoutにはminimumInteritemSpacingプロパティがあり、これはIBで設定しているものです。ただし、コレクションのデリゲートには、 アイテム間の間隔を決定するメソッド を含めることもできます。このメソッドはレイアウトのプロパティよりも優先されるため、デリゲートに実装すると、レイアウトのプロパティは使用されません。

  4. 間隔にはminimum間隔があることに注意してください。レイアウトは、その番号(プロパティまたはデリゲートメソッドのどちらからでも)を最小許容スペースとして使用しますが、行にスペースが残っている場合は、より大きなスペースを使用できます。したがって、たとえば、最小間隔を0に設定しても、アイテム間に数ピクセルが表示される場合があります。アイテムの間隔を正確に制御したい場合は、おそらく別のレイアウトを使用する必要があります(おそらく独自のレイアウトの1つ)。

16
Caleb

少しの数学がトリックをより簡単にします。 Chris Wagnerが書いたコードは、以前の各アイテムのレイアウト属性を呼び出すため、恐ろしいものです。スクロールするほど遅くなります...

次のようにモジュロを使用します(私はminimumInteritemSpacing値を最大値としても使用しています):

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
    NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);

    if (indexPath.item % numberOfItemsPerLine != 0)
    {
        NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);

        CGRect itemFrame = [currentItemAttributes frame];
        itemFrame.Origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
        currentItemAttributes.frame = itemFrame;
    }

    return currentItemAttributes;
}
4
ldesroziers

左揃えの簡単な方法は、UICollectionViewFlowLayoutのサブクラスでlayoutAttributesForElementsInRect:を変更することです。

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
    CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
    for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
    {
        //fix blur
        CGRect theFrame = CGRectIntegral(layoutAttributes.frame);

        //left justify
        if(prevFrame.Origin.x > -FLT_MAX &&
           prevFrame.Origin.y >= theFrame.Origin.y &&
           prevFrame.Origin.y <= theFrame.Origin.y) //workaround for float == warning
        {
            theFrame.Origin.x = prevFrame.Origin.x +
                                prevFrame.size.width + 
                                EXACT_SPACE_BETWEEN_ITEMS;
        }
        prevFrame = theFrame;

        layoutAttributes.frame = theFrame;
    }
    return allLayoutAttributes;
}
3
xytor

Clean Swiftソリューション、進化の歴史から:

  1. マット回答 がありました
  2. クリス・ワーグナーの唯一のアイテムの修正
  3. mokagio sectionInsetおよびminimumInteritemSpacingの改善 がありました
  4. fanpyi Swiftバージョン
  5. 今ここに私のシンプルでクリーンなバージョンがあります:
_open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
    open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
    }

    open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
            collectionView != nil else {
            // should never happen
            return nil
        }

        // if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
        if indexPath.item != 0,
            let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
            currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.Origin.y, width: .infinity, height: previousFrame.size.height)) {
            // the next item on a line
            currentItemAttributes.frame.Origin.x = previousFrame.Origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
        } else {
            // the first item on a line
            currentItemAttributes.frame.Origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
        }
        return currentItemAttributes
    }

    func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
        return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
    }

    func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
        return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
    }
}
_

使用法:アイテム間の間隔は、デリゲートのcollectionView (_:layout:minimumInteritemSpacingForSectionAt:)によって決定されます。

Github、 https://github.com/Coeur/UICollectionViewLeftAlignedLayout に配置し、実際に両方向(水平および垂直)をサポートする機能を追加しました。

2
Cœur

Swift Chrisソリューションのバージョン。

class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
    var maxCellSpacing = 14.0
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
            for attributes in attributesToReturn {
                if attributes.representedElementKind == nil {
                    let indexPath = attributes.indexPath
                    attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
                }
            }
            return attributesToReturn;
        }
        return super.layoutAttributesForElementsInRect(rect)
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)

        if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
            let sectionInset = collectionViewFlowLayout.sectionInset
            if (indexPath.item == 0) { // first item of section
                var frame = currentItemAttributes.frame;
                frame.Origin.x = sectionInset.left; // first item of the section should always be left aligned
                currentItemAttributes.frame = frame;

                return currentItemAttributes;
            }

            let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
            let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
            let previousFrameRightPoint = Double(previousFrame.Origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing

            let currentFrame = currentItemAttributes.frame
            var width : CGFloat = 0.0
            if let collectionViewWidth = self.collectionView?.frame.size.width {
                width = collectionViewWidth
            }
            let strecthedCurrentFrame = CGRectMake(0,
                currentFrame.Origin.y,
                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
                var frame = currentItemAttributes.frame;
                frame.Origin.x = sectionInset.left; // first item on the line should always be left aligned
                currentItemAttributes.frame = frame;
                return currentItemAttributes;
            }

            var frame = currentItemAttributes.frame;
            frame.Origin.x = CGFloat(previousFrameRightPoint)
            currentItemAttributes.frame = frame;
        }
        return currentItemAttributes;
    }
}

それを使用するには、次の手順を実行します。

    override func viewDidLoad() {
    super.viewDidLoad()
    self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
    var layout = PazLeftAlignedCollectionViewFlowLayout()
    layout.itemSize = CGSizeMake(220.0, 230.0)
    layout.minimumLineSpacing = 12.0
    return layout
}
2
zirinisp

UICollectionViewFlowLayoutの「問題」は、セルに両端揃えを適用することです。行の最初のセルは左揃えで、行の最後のセルは右揃えで、その間の他のすべてのセルはminimumInteritemSpacingよりも大きい等間隔で均等に分布します。

この投稿には、UICollectionViewFlowLayoutをサブクラス化することでこの問題を解決する素晴らしい答えがすでにたくさんあります。その結果、セルを整列するレイアウトが得られますleft。一定の間隔でセルを分散する別の有効なソリューションは、セルを整列することですright

AlignedCollectionViewFlowLayout

matt および Chris Wagnerによって示唆されたのと同様のアイデアに従うUICollectionViewFlowLayoutサブクラスも作成しました= セル​​を整列させることができます

⬅︎ left

Left-aligned layout

または ➡︎ 

Right-aligned layout

ここからダウンロードして、プロジェクトにレイアウトファイルを追加し、AlignedCollectionViewFlowLayoutをコレクションビューのレイアウトクラスとして設定できます。
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

仕組み(左揃えのセルの場合):

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

ここでのコンセプトは、インデックスiの現在のセルとインデックスi-1の前のセルが同じ行を占めるかどうかをチェックすることです。

  • インデックスがないセルiは、行の左端のセルです。
    →セルをコレクションビューの左端に移​​動します(垂直位置は変更しません)。
  • 含まれている場合、インデックスiのセルは行の左端のセルではありません。
    →前のセルのフレーム(インデックスi-1)を取得し、現在のセルをその隣に移動します。

右揃えのセルの場合...

...同じ逆も同様です。つまり、代わりにnextセルをインデックスi + 1でチェックします。

2
Mischa

2つの方法でそれを行うことができます。

まず、layoutAttributesForItemでいくつかの変更を行います。

前のlayoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frameを介して現在の属性のレイアウトを取得します。

layoutAttributesForItem(at :):このメソッドは、オンデマンドのレイアウト情報をコレクションビューに提供します。これをオーバーライドして、要求されたindexPathにあるアイテムのレイアウト属性を返す必要があります。

第二に、UICollectionViewLayoutAttributes(forCellWith: indexPath)を介していくつかの属性を追加し、好きな場所に配置します。

また、計算が少し大きくなります。これは、レイアウトの重いリフティングを実行するためです。

layoutAttributesForElements(in :):このメソッドでは、指定された長方形内のすべてのアイテムのレイアウト属性を返す必要があります。属性をUICollectionViewLayoutAttributesの配列としてコレクションビューに返します。


確認できます My repo

  • アイテムを左揃えにすることができます

  • アイテムを右揃えにすることができます

  • アイテムを右揃えにしたり、アイテムを逆にしたりできます

2
dengApro

クリーナーSwift Chris Wagnerの回答に基づいた、興味のある人向けのバージョン:

class AlignLeftFlowLayout: UICollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(9.0)

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]

        for attributes in attributesToReturn ?? [] {
            if attributes.representedElementKind == nil {
                attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
            }
        }

        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
        let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset

        if indexPath.item == 0 {
            let f = curAttributes.frame
            curAttributes.frame = CGRectMake(sectionInset.left, f.Origin.y, f.size.width, f.size.height)
            return curAttributes
        }

        let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
        let prevFrameRightPoint = prevFrame.Origin.x + prevFrame.size.width + maximumCellSpacing

        let curFrame = curAttributes.frame
        let stretchedCurFrame = CGRectMake(0, curFrame.Origin.y, self.collectionView!.frame.size.width, curFrame.size.height)

        if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
            curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        } else {
            curAttributes.frame = CGRectMake(sectionInset.left, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        }

        return curAttributes
    }
}
1
Kevin R

NSCollectionViewFlowLayout用です

class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(2.0)

    override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {

        let attributesToReturn = super.layoutAttributesForElementsInRect(rect)

        for attributes in attributesToReturn ?? [] {
            if attributes.representedElementKind == nil {
                attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
            }
        }
        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {

        let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
        let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset

        if indexPath.item == 0 {
            let f = curAttributes!.frame
            curAttributes!.frame = CGRectMake(sectionInset.left, f.Origin.y, f.size.width, f.size.height)
            return curAttributes
        }

        let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
        let prevFrameRightPoint = prevFrame.Origin.x + prevFrame.size.width + maximumCellSpacing

        let curFrame = curAttributes!.frame
        let stretchedCurFrame = CGRectMake(0, curFrame.Origin.y, self.collectionView!.frame.size.width, curFrame.size.height)

        if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
            curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        } else {
            curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        }

        return curAttributes
    }
}
0
Chris