ios - uicollectionviewdelegateflowlayout - uicollectionviewflowlayoutautomaticsize




使用自動佈局在UICollectionView中指定單元的一個維度 (4)

在iOS 8中, UICollectionViewFlowLayout支持根據自己的內容大小自動調整單元格大小。 這根據其內容根據寬度和高度調整單元格大小。

是否可以為所有單元格的寬度(或高度)指定固定值並允許其他維度調整大小?

對於一個簡單的例子,考慮一個單元格中的多行標籤,其約束將其定位到單元格的兩側。 多行標籤可以調整不同的方式來適應文本。 單元格應該填充集合視圖的寬度並相應地調整它的高度。 相反,單元格的大小是隨意的,當單元格大小大於集合視圖的不可滾動維度時,它甚至會導致崩潰。

iOS 8引入了方法systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:對於集合視圖中的每個單元格,佈局在單元格上調用此方法,傳遞估計的大小。 對我來說有意義的是在單元格上覆蓋此方法,傳遞給定的大小並將水平約束設置為所需的水平約束,將垂直約束設置為低優先級。 這樣水平尺寸固定為佈局中設置的值,並且垂直尺寸可以是靈活的。

像這樣的東西:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

然而,這種方法給出的尺寸是完全奇怪的。 關於這個方法的文檔對我來說很不明確,並且提到使用常量UILayoutFittingCompressedSize UILayoutFittingExpandedSize ,它只是代表零大小和相當大的一個。

這個方法的size參數真的只是一種傳遞兩個常量的方法嗎? 有沒有辦法達到我期望得到給定尺寸的適當高度的行為?

備用解決方案

1)添加將為單元格指定特定寬度的約束可實現正確的佈局。 這是一個糟糕的解決方案,因為該約束應該設置為它沒有安全引用的單元集合視圖的大小。 這個約束的值可以在單元配置時傳入,但這看起來完全違反直覺。 這也很尷尬,因為直接向單元格或其內容視圖添加約束會導致很多問題。

2)使用表格視圖。 表格視圖以這種方式工作,因為單元格具有固定的寬度,但這不適用於其他情況,例如具有固定寬度單元格在多列中的iPad佈局。


一個簡單的方法在iOS 9中用幾行代碼完成 - 水平方式例如(將其高度固定為其Collection View高度):

使用estimatedItemSize初始化您的集合視圖流佈局以啟用自定義單元格:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

實現集合視圖佈局委託(在你的視圖控制器大部分時間), collectionView:layout:sizeForItemAtIndexPath: 這裡的目標是將固定高度(或寬度)設置為集合視圖維度。 10值可以是任何值,但是您應該將其設置為不會破壞約束的值:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

覆蓋你的自定義單元格preferredLayoutAttributesFittingAttributes:方法,這部分實際上是根據你的自動佈局約束和你剛剛設置的高度來計算你的動態單元格寬度:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}

嘗試在首選佈局屬性中修改寬度:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

當然,你也想確保你正確地設置你的佈局:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

下面是我放在Github上的東西,它使用了恆定寬度的單元格並支持動態類型,因此單元格的高度隨系統字體大小的變化而更新。


有一個比這裡的其他答案更乾淨的方法,它運作良好。 它應該是高性能的(集合視圖加載速度快,沒有不必要的自動佈局通行等),並且沒有像固定集合視圖寬度那樣的“幻數”。 更改集合視圖大小(例如旋轉),然後使佈局無效也應該很好。

1.創建以下流佈局子類

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2.註冊您的收藏視圖以進行自動調整大小

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3.在您的單元格子類中使用預定義寬度+自定義高度

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

這聽起來像你所要求的是一種使用UICollectionView產生像UITableView這樣的佈局的方法。 如果這真的是你想要的,正確的方法是使用自定義的UICollectionViewLayout子類(可能類似於SBTableLayout )。

另一方面,如果你真的問是否有一個乾淨的方式來使用默認的UICollectionViewFlowLayout來做到這一點,那麼我相信沒有辦法。 即使使用iOS8的自動調整大小的單元,也不是簡單的。 正如你所說的,基本問題是流程佈局的機制沒有辦法修復一個維度並讓另一個維度做出反應。 (另外,即使可以,在需要兩次佈局傳遞來確定多行標籤的大小時也會有額外的複雜性,這可能與自定義單元格想要通過調用systemLayoutSizeFittingSize來計算所有大小的方法不符)。

然而,如果你仍然想用流佈局創建一個類似tableview的佈局,使用確定其自身大小的單元格,並自然地​​對集合視圖的寬度作出響應,當然這是可能的。 仍然是混亂的方式。 我已經做了一個“尺寸單元”,即一個未顯示的UICollectionViewCell,控制器只保留它來計算單元尺寸。

這種方法有兩個部分。 第一部分是收集視圖委託來計算正確的單元格大小,方法是採用集合視圖的寬度並使用大小單元計算單元格的高度。

在你的UICollectionViewDelegateFlowLayout中,你實現了這樣一個方法:

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

這將導致集合視圖根據封閉集合視圖設置單元格的寬度,但然後請求大小單元計算高度。

第二部分是讓尺寸單元使用自己的AL約束來計算高度。 由於多線UILabel有效地需要兩階段佈局過程,這可能比應該更困難。 該工作在preferredLayoutSizeFittingSize方法中完成,如下所示:

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(此代碼根據“高級收集視圖”中WWDC2014會話的示例代碼中的Apple示例代碼進行調整。)

有幾點需要注意。 它使用layoutIfNeeded()來強制整個單元格的佈局,以便計算和設置標籤的寬度。 但這還不夠。 我相信你也需要設置preferredMaxLayoutWidth以便標籤將自動佈局使用該寬度。 只有這樣才能使用systemLayoutSizeFittingSize ,以便讓單元格在考慮標籤的同時計算其高度。

我喜歡這種方法嗎? 沒有!! 它感覺太複雜,並且它佈置了兩次。 但只要性能不成問題,我寧願在運行時執行兩次佈局,而不是在代碼中定義兩次,這似乎是唯一的另一種選擇。

我的希望是,最終自動調整大小的單元格的工作方式會有所不同,這會變得更簡單。

示例項目在工作中顯示它。

但為什麼不使用自我測量的細胞呢?

從理論上講,iOS8的新設備“自我測量細胞”應該使這不必要。 如果您已經使用自動佈局(AL)定義單元格,那麼集合視圖應該足夠聰明,可以讓它自己調整大小並正確放置。 實際上,我還沒有看到任何可以使用多線標籤的例子。 我認為this部分是因為自動調節細胞機制仍然是越野車。

但我敢打賭,這主要是因為Auto Layout和標籤慣用的技巧,那就是UILabels需要基本上兩步的佈局過程。 我不清楚你如何使用自動調整大小的單元執行這兩個步驟。

就像我說的,這對於不同的佈局來說真的是一份工作。 它是流程佈局本質的一部分,它定位具有大小的事物,而不是固定寬度並讓他們選擇高度。

那麼preferredLayoutAttributesFittingAttributes如何:?

我認為preferredLayoutAttributesFittingAttributes:方法是一個紅色的鯡魚。 這只是與新的自動調整細胞機制一起使用。 所以只要這種機制不可靠,這不是答案。

和systemlayoutSizeFittingSize有什麼關係:?

你說得對,文件很混亂。

systemLayoutSizeFittingSize:systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:上的文檔都暗示應該只傳遞UILayoutFittingCompressedSizeUILayoutFittingExpandedSize作為targetSize 。 但是,方法簽名本身,頭部註釋以及函數的行為表明它們正在響應targetSize參數的確切值。

事實上,如果您設置了UICollectionViewFlowLayoutDelegate.estimatedItemSize ,為了啟用新的自定義單元格機制,該值似乎作為targetSize傳入。 而UILabel.systemLayoutSizeFittingSize似乎返回與UILabel.systemLayoutSizeFittingSize完全相同的值。 這是可疑的,因為systemLayoutSizeFittingSize的參數應該是一個粗略的目標,並且sizeThatFits:的參數應該是最大外接大小。

更多資源

雖然認為這樣的例行要求應該需要“研究資源”,但我認為它確實令人傷心。 好的例子和討論是:





autolayout