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

 随洋恒黯的天使 发布于 2022-12-18 18:45

在iOS 8中,UICollectionViewFlowLayout支持根据自己的内容大小自动调整单元格大小.这根据其内容在宽度和高度上调整单元的大小.

是否可以为所有单元格的宽度(或高度)指定固定值,并允许其他尺寸调整大小?

对于一个简单的示例,请考虑单元格中的多行标签,其中约束将其定位到单元格的两侧.可以通过不同方式调整多行标签的大小以容纳文本.单元格应填充集合视图的宽度并相应地调整其高度.相反,单元格的大小是随意调整的,当单元格大小大于集合视图的不可滚动维度时,它甚至会导致崩溃.


iOS 8引入了方法systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:对于集合视图中的每个单元格,布局在单元格上调用此方法,传入估计的大小.对我来说有意义的是在单元格上覆盖此方法,传递给定的大小并将水平约束设置为required,并将垂直约束设置为低优先级.这样,水平尺寸固定为布局中设置的值,垂直尺寸可以灵活.

像这样的东西:

- (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布局.

3 个回答
  • 在iOS 9中使用几行代码执行此操作的简单方法 - 水平方式示例(将其高度固定到其Collection View高度):

    使用a 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;
    }
    

    2022-12-18 18:48 回答
  • 这里有一些比其他一些答案更清晰的方法,而且效果很好.它应该是高性能的(集合视图加载速度快,没​​有不必要的自动布局传递等),并且没有像固定集合视图宽度那样的任何"幻数".更改集合视图大小(例如,在旋转时),然后使布局无效也应该很有效.

    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
    }
    

    2022-12-18 18:48 回答
  • 听起来你要求的是一种使用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)定义了一个单元格,那么集合视图应该足够智能,以使其自身大小并正确显示.在实践中,我还没有看到任何使用多行标签的例子.我认为这部分是因为自我调整细胞机制仍然是错误的.

    但我敢打赌,这主要是因为自动布局和标签的常见技巧,即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的代码,"具有集合视图的高级用户界面"

    2022-12-18 18:48 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有