UICollectionView装饰视图

49
有没有人为iOS 6 UICollectionView实现过装饰视图?在网上找不到任何关于实现装饰视图的教程。基本上在我的应用程序中,我有多个部分,我只想在每个部分后面显示一个装饰视图。这应该很容易实现,但我一直没有成功。这让我抓狂了......谢谢。

你有看过这个教程吗?http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12 我不确定他们是否已经做过了。但是去那里看看是值得的。 - iDev
1
@ACB 在那个教程中没有装饰视图,只有标题和单元格。 - Guido Hendriks
3
是的,raywnderlich的教程没有装饰视图,即使他们使用的组件中有一个展示了装饰视图。也许他们在最后一刻决定不在完成的项目中展示装饰视图。哇... 装饰视图真是个大谜团。我希望苹果能提供一些关于这个主题的示例代码。 - vtruong
你可能想要查看WWDC会议的内容。我几个月前参加过,记得他们谈到了这个问题,但是具体细节我不太记得了。 - Guido Hendriks
请注意,标准流布局类仅支持部分标题和部分页脚视图,不支持装饰视图。要支持装饰视图,您需要子类化UICollectionViewFlowLayout。 - masam
显示剩余2条评论
4个回答

102

这是一个关于在Swift中使用集合视图布局装饰视图的教程(这是Swift 3,Xcode 8 seed 6)。


装饰视图并不属于UICollectionView的功能;它们实际上属于UICollectionViewLayout。没有任何UICollectionView方法(或委托或数据源方法)提到装饰视图。UICollectionView对它们一无所知;它只是按照指示执行操作。

要提供任何装饰视图,您需要一个UICollectionViewLayout子类;该子类可以自由定义其自己的属性和委托协议方法,以定制其如何配置装饰视图,但完全取决于您。

为了说明问题,我将子类化UICollectionViewFlowLayout,以在集合视图的内容矩形的顶部施加一个标题标签。这可能是一个愚蠢的装饰视图用法,但它完美地阐述了基本原理。为简单起见,我将首先硬编码整个内容,不允许客户端自定义此视图的任何方面。

在布局子类中实现装饰视图有四个步骤:

  1. 定义UICollectionReusableView子类。

  2. 通过调用register(_:forDecorationViewOfKind:),将UICollectionReusableView子类注册到布局中(而不是集合视图)。初始化器是这样做的好地方。

  3. 实现layoutAttributesForDecorationView(ofKind:at:)方法,以返回定位UICollectionReusableView所需的布局属性。为构造布局属性,请调用init(forDecorationViewOfKind:with:)并配置属性。

  4. 重写layoutAttributesForElements(in:)方法,使其返回包含layoutAttributesForDecorationView(ofKind:at:)方法结果的数组。

最后一步是导致装饰视图出现在集合视图中的原因。当集合视图调用layoutAttributesForElements(in:)时,它发现返回的数组包括指定类型的装饰视图的布局属性。集合视图对装饰视图一无所知,因此它回到布局,请求此类型的装饰视图的实际实例。您已经注册了此类装饰视图以对应于UICollectionReusableView子类,因此将实例化UICollectionReusableView子类,并返回该实例,并使集合视图根据布局属性进行定位。

让我们遵循这些步骤。首先定义UICollectionReusableView子类:

class MyTitleView : UICollectionReusableView {
    weak var lab : UILabel!
    override init(frame: CGRect) {
        super.init(frame:frame)
        let lab = UILabel(frame:self.bounds)
        self.addSubview(lab)
        lab.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        lab.font = UIFont(name: "GillSans-Bold", size: 40)
        lab.text = "Testing"
        self.lab = lab
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

现在我们转向我们的UICollectionViewLayout子类,我将其称为MyFlowLayout。在布局的初始化器中注册了MyTitleView;我还定义了一些私有属性,这些属性在剩余的步骤中会用到:

private let titleKind = "title"
private let titleHeight : CGFloat = 50
private var titleRect : CGRect {
    return CGRect(10,0,200,self.titleHeight)
}
override init() {
    super.init()
    self.register(MyTitleView.self, forDecorationViewOfKind:self.titleKind)
}

实现layoutAttributesForDecorationView(ofKind:at:)

override func layoutAttributesForDecorationView(
    ofKind elementKind: String, at indexPath: IndexPath) 
    -> UICollectionViewLayoutAttributes? {
        if elementKind == self.titleKind {
            let atts = UICollectionViewLayoutAttributes(
                forDecorationViewOfKind:self.titleKind, with:indexPath)
            atts.frame = self.titleRect
            return atts
        }
        return nil
}

重写layoutAttributesForElements(in:)方法;这里的索引路径是任意的(在上面的代码中我忽略了它):

override func layoutAttributesForElements(in rect: CGRect) 
    -> [UICollectionViewLayoutAttributes]? {
        var arr = super.layoutAttributesForElements(in: rect)!
        if let decatts = self.layoutAttributesForDecorationView(
            ofKind:self.titleKind, at: IndexPath(item: 0, section: 0)) {
                if rect.intersects(decatts.frame) {
                    arr.append(decatts)
                }
        }
        return arr
}

这有效!一个名为“Testing”的标题标签将出现在集合视图的顶部。


现在我将展示如何使标签可定制化。不使用标题“Testing”,我们将允许客户端设置一个属性来确定标题。我将为我的布局子类提供一个公共title属性:

class MyFlowLayout : UICollectionViewFlowLayout {
    var title = ""
    // ...
}

使用此布局的人应设置此属性。例如,假设此集合视图显示50个美国州:

func setUpFlowLayout(_ flow:UICollectionViewFlowLayout) {
    flow.headerReferenceSize = CGSize(50,50)
    flow.sectionInset = UIEdgeInsetsMake(0, 10, 10, 10)
    (flow as? MyFlowLayout)?.title = "States" // *
}

现在我们来到一个有趣的谜题。我们的布局有一个“title”属性,其值需要以某种方式传达给我们的MyTitleView实例。但是什么时候可能发生这种情况呢?我们不负责实例化MyTitleView;当集合视图在后台请求该实例时,它会自动发生。MyFlowLayout实例和MyTitleView实例没有交集的时刻。

解决方案是使用布局属性作为信使。MyFlowLayout从未与MyTitleView相遇,但它确实创建了传递给集合视图以配置MyFlowLayout的布局属性对象。因此,布局属性对象就像一个信封。通过子类化UICollectionViewLayoutAttributes,我们可以将任何所需的信息包含在信封中,例如标题:

class MyTitleViewLayoutAttributes : UICollectionViewLayoutAttributes {
    var title = ""
}

这里是我们的信封!现在我们重新编写 layoutAttributesForDecorationView 的实现。当我们实例化布局属性对象时,我们实例化我们的子类并设置其 title 属性:

override func layoutAttributesForDecorationView(
    ofKind elementKind: String, at indexPath: IndexPath) -> 
    UICollectionViewLayoutAttributes? {
        if elementKind == self.titleKind {
            let atts = MyTitleViewLayoutAttributes( // *
                forDecorationViewOfKind:self.titleKind, with:indexPath)
            atts.title = self.title // *
            atts.frame = self.titleRect
            return atts
        }
        return nil
}

最后,在 MyTitleView 中,我们实现 apply(_:) 方法。当集合视图配置装饰视图时,将调用此方法,并使用布局属性对象作为其参数!因此,我们提取出 title 并将其用作标签的文本:

class MyTitleView : UICollectionReusableView {
    weak var lab : UILabel!
    // ... the rest as before ...
    override func apply(_ atts: UICollectionViewLayoutAttributes) {
        if let atts = atts as? MyTitleViewLayoutAttributes {
            self.lab.text = atts.title
        }
    }
}
很容易看出您可以扩展示例以使标签功能(如字体和高度)可自定义。由于我们正在子类化UICollectionViewFlowLayout,因此可能还需要进行进一步修改,以通过向下推其他元素来为装饰视图腾出空间。另外,从技术上讲,我们应该覆盖MyTitleView中的isEqual(_:)方法,以区分不同的标题。所有这些都留给读者作为练习。

2
@matt 非常感谢你关于使用布局属性作为信使与装饰视图通信的部分!这是一个有趣的API - 您不直接实例化或引用装饰视图。 - s.ka
这是否意味着无法通过Storyboard添加装饰视图? - Heuristic
现在我们转向我们的UICollectionViewLayout子类,我将其称为MyFlowLayout。但是,“class MyFlowLayout:UICollectionViewFlowLayout”。有点困惑。UICollectionViewFlowLayout还是UICollectionViewLayout? - McDonal_11
@MariánČerný 谢谢,我会看一下的! - matt
1
如果您子类化并实现任何自定义布局属性,则还必须覆盖继承的isEqual:方法以比较属性的值。在iOS 7及更高版本中,如果这些属性未更改,则集合视图不会应用布局属性。它通过使用isEqual:方法比较旧属性对象和新属性对象来确定属性是否已更改。 - Igor Vasilev
显示剩余10条评论

23

我使用自定义布局解决了这个问题,具体方法如下:

创建一个UICollectionReusableView的子类,例如添加一个UIImageView到其中:

@implementation AULYFloorPlanDecorationViewCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        UIImage *backgroundImage = [UIImage imageNamed:@"Layout.png"];
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame];
        imageView.image = backgroundImage;
        [self addSubview:imageView];
    }
    return self;
}

@end

然后在您的控制器中的viewDidLoad方法中,使用以下代码(将code替换为您自定义的布局)注册此子类:

AULYAutomationObjectLayout *automationLayout = (AULYAutomationObjectLayout *)self.collectionView.collectionViewLayout;
[automationLayout registerClass:[AULYFloorPlanDecorationViewCell class]  forDecorationViewOfKind:@"FloorPlan"];
在您的自定义布局中,实现以下方法(或类似方法):
- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
    layoutAttributes.frame = CGRectMake(0.0, 0.0, self.collectionViewContentSize.width, self.collectionViewContentSize.height);
    layoutAttributes.zIndex = -1;
    return layoutAttributes;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *allAttributes = [[NSMutableArray alloc] initWithCapacity:4];

    [allAttributes addObject:[self layoutAttributesForDecorationViewOfKind:@"FloorPlan" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]];

    for (NSInteger i = 0; i < [self.collectionView numberOfItemsInSection:0]; i++)
    {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        [allAttributes addObject:layoutAttributes];
    }
    return allAttributes;
}

似乎没有相关的文档,但是下面这篇文章让我朝着正确的方向前进了:iOS中的集合视图编程指南

更新:对于装饰视图,最好通过继承UICollectionReusableView类来实现,而不是UICollectionViewCell类。


谢谢,这让我找到了正确的方向。看起来你只是在为第0个区域显示单个装饰视图。我的集合视图有多个区域。我仍然需要弄清楚如何为每个具有不同项目数量的区域显示相同的装饰视图。 - vtruong
是的,我只显示了一个装饰视图,但是您可以通过在layoutAttributesForElementsInRect中添加更多行来显示其他装饰视图:[allAttributes addObject:[self layoutAttributesForDecorationViewOfKind:@"FloorPlan" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]]; (您可以为第2、3、4等部分添加更多内容。然后,您必须确保设置布局属性,以便该部分的边界正确。要找出有多少个部分以及它们包含多少个元素,您应该能够访问self.collectionView.dataSource - dominikk
1
我是否必须子类化UICollectionViewLayout才能使用装饰视图?这可以从FlowLayout中完成吗? - Drew C
1
@DrewC 你可以继承UICollectionViewFlowLayout并重写上述提到的方法。 - SimpsOff
1
为什么这个答案没有被接受?它是完整和正确的。唯一没有解释的是如何将自定义布局属性从布局通信到装饰视图,而这很容易弄清楚。 - matt

5
这是在MonoTouch中实现的方法:

以下是具体步骤:

public class DecorationView : UICollectionReusableView
{
    private static NSString classId = new NSString ("DecorationView");
    public static NSString ClassId { get { return classId; } }

    UIImageView blueMarble;

    [Export("initWithFrame:")]
    public DecorationView (RectangleF frame) : base(frame)
    {
        blueMarble = new UIImageView (UIImage.FromBundle ("bluemarble.png"));

        AddSubview (blueMarble);    
    }
}


public class SimpleCollectionViewController : UICollectionViewController
{
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();
        //Register the cell class (code for AnimalCell snipped)
        CollectionView.RegisterClassForCell (typeof(AnimalCell), AnimalCell.ClassId);
        //Register the supplementary view class (code for SideSupplement snipped)
        CollectionView.RegisterClassForSupplementaryView (typeof(SideSupplement), UICollectionElementKindSection.Header, SideSupplement.ClassId);

        //Register the decoration view
        CollectionView.CollectionViewLayout.RegisterClassForDecorationView (typeof(DecorationView), DecorationView.ClassId);
    }

 //...snip...
}

public class LineLayout : UICollectionViewFlowLayout
{
    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect (RectangleF rect)
    {
        var array = base.LayoutAttributesForElementsInRect (rect);
        /* 
        ...snip content relating to cell layout...
        */
        //Add decoration view
        var attributesWithDecoration = new List<UICollectionViewLayoutAttributes> (array.Length + 1);
        attributesWithDecoration.AddRange (array);
        var decorationIndexPath = NSIndexPath.FromIndex (0);
        var decorationAttributes = LayoutAttributesForDecorationView (DecorationView.ClassId, decorationIndexPath);
        attributesWithDecoration.Add (decorationAttributes);

        var extended = attributesWithDecoration.ToArray<UICollectionViewLayoutAttributes> ();

        return extended;
    }

    public override UICollectionViewLayoutAttributes LayoutAttributesForDecorationView (NSString kind, NSIndexPath indexPath)
    {
        var layoutAttributes = UICollectionViewLayoutAttributes.CreateForDecorationView (kind, indexPath);
        layoutAttributes.Frame = new RectangleF (0, 0, CollectionView.ContentSize.Width, CollectionView.ContentSize.Height);
        layoutAttributes.ZIndex = -1;
        return layoutAttributes;
    }
//...snip...
}

最终结果类似于: 输入图像描述


注:此处为IT技术相关内容,涉及图片展示。


0
在我的情况下: 我想从UITableView升级到UICollectionView。 uitableview sections >>> 附加视图 uitableview headerView >>> 装饰视图
在我的情况下,我感觉子类化布局和做其他事情太多了,只是为了简单的“headerView”(装饰)。
因此,我的解决方案只是创建headerview(而不是section)作为第一个单元格, 而第一节(第0节大小为零)则为第1节。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接