在 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
}
(此代码改编自 Apple 示例代码,来自 WWDC2014 会议“高级集合视图”的示例代码。)
有几点需要注意。它使用 layoutIfNeeded() 来强制整个单元格的布局,以便计算和设置标签的宽度。但这还不够。我相信您还需要设置 preferredMaxLayoutWidth
以便标签将使用该宽度和自动布局。只有这样,您才能使用 systemLayoutSizeFittingSize
来让单元格计算其高度,同时考虑标签。
我喜欢这种方法吗?不!!感觉太复杂了,而且布局两次。但只要性能不成为问题,我宁愿在运行时执行两次布局,也不愿在代码中定义两次,这似乎是唯一的其他选择。
我希望最终自我调整大小的单元会以不同的方式工作,这一切都会变得简单得多。
Example project 在工作中展示它。
但是为什么不直接使用自定尺寸单元呢?
从理论上讲,iOS8 的“自动调整单元格”的新功能应该让这变得不必要了。如果您使用自动布局 (AL) 定义了一个单元格,那么集合视图应该足够智能,可以让它自行调整大小并正确布局。在实践中,我还没有看到任何可以使用多行标签的示例。我认为 this 部分是因为自调整单元机制仍然存在错误。
但我敢打赌,这主要是因为自动布局和标签通常很棘手,也就是说 UILabel 需要一个基本上两步的布局过程。我不清楚如何使用自定尺寸单元执行这两个步骤。
就像我说的,这确实是一个不同布局的工作。流布局本质的一部分是它定位具有大小的东西,而不是固定宽度并让它们选择它们的高度。
那么preferredLayoutAttributesFittingAttributes: 呢?
我认为 preferredLayoutAttributesFittingAttributes:
方法是一个红鲱鱼。那只能与新的自定尺寸单元机制一起使用。因此,只要该机制不可靠,这不是答案。
systemlayoutSizeFittingSize: 怎么了?
你是对的,文档令人困惑。
systemLayoutSizeFittingSize:
和 systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:
上的文档都建议您只应将 UILayoutFittingCompressedSize
和 UILayoutFittingExpandedSize
作为 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
初始化您的 Collection View Flow Layout 以启用自调整单元格:
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);
实现集合视图布局委托(大部分时间在您的视图控制器中),collectionView:layout:sizeForItemAtIndexPath:
。这里的目标是将固定高度(或宽度)设置为 Collection View 维度。 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;
}
UILabel
时,这种更简单的方法是否可以正常工作,其换行会影响单元格的固有高度?这是一个常见的情况,过去需要我描述的所有后空翻。我想知道自我回答以来iOS是否已修复它。
尝试在首选布局属性中固定宽度:
- (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 上的东西,它使用恒定宽度的单元格并支持动态类型,因此单元格的高度会随着系统字体大小的变化而更新。
是的,它可以通过编程方式使用自动布局并通过在情节提要或 xib 中设置约束来完成。您需要为宽度大小添加约束以保持不变并将高度设置为大于或等于。
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/
http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
希望这会有所帮助并解决您的问题。
cellForItemAtIndexPath
中更新约束:cell.constraintItemWidth.constant = UIScreen.main.bounds.width
。我的应用程序仅适用于肖像。您可能希望在方向更改后 reloadData
以更新约束。
不定期副业成功案例分享