使用自动布局在UICollectionView中指定单元的一个维度

在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布局。


这听起来像你所要求的是一种使用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)定义单元格,那么集合视图应该足够聪明,可以让它自己调整大小并正确放置。 实际上,我还没有看到任何可以使用多线标签的例子。 我认为这部分是因为自动调节细胞机制仍然是越野车。

但我敢打赌,这主要是因为Auto Layout和标签惯用的技巧,那就是UILabels需要基本上两步的布局过程。 我不清楚你如何使用自动调整大小的单元执行这两个步骤。

就像我说的,这对于不同的布局来说真的是一份工作。 它是流程布局本质的一部分,它定位具有大小的事物,而不是固定宽度并让他们选择高度。

那么preferredLayoutAttributesFittingAttributes如何:?

我认为preferredLayoutAttributesFittingAttributes:方法是一个红色的鲱鱼。 这只是与新的自动调整细胞机制一起使用。 所以只要这种机制不可靠,这不是答案。

和systemlayoutSizeFittingSize有什么关系:?

你说得对,文件很混乱。

systemLayoutSizeFittingSize:systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:上的文档都暗示应该只传递UILayoutFittingCompressedSizeUILayoutFittingExpandedSize作为targetSize 。 但是,方法签名本身,头部注释以及函数的行为表明它们正在响应targetSize参数的确切值。

实际上,如果您设置了UICollectionViewFlowLayoutDelegate.estimatedItemSize ,为了启用新的自定义大小的单元机制,该值似乎作为targetSize传入。 而UILabel.systemLayoutSizeFittingSize似乎返回完全相同的值UILabel.sizeThatFits 。 这是可疑的,因为systemLayoutSizeFittingSize的参数应该是一个粗略的目标,并且sizeThatFits:的参数应该是最大外接大小。

更多资源

虽然认为这样的例行要求应该需要“研究资源”,但我认为它确实令人伤心。 好的例子和讨论是:

  • http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html
  • http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html
  • WWDC2014会话232的代码,“集合视图的高级用户界面”

  • 有一个比这里的其他答案更干净的方法,它运作良好。 它应该是高性能的(集合视图加载速度快,没​​有不必要的自动布局通行等),并且没有像固定集合视图宽度那样的“幻数”。 更改集合视图大小(例如旋转),然后使布局无效也应该很好。

    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
    }
    

    一个简单的方法在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;
    }
    
    链接地址: http://www.djcxy.com/p/49967.html

    上一篇: Specifying one Dimension of Cells in UICollectionView using Auto Layout

    下一篇: How to implement a JPanel that shows/hide content depending on its width?