49

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

4

4 回答 4

98

这是 Swift 中的集合视图布局装饰视图教程(这是 Swift 3,Xcode 8 种子 6)。


装饰视图不是 UICollectionView 功能;它们本质上属于 UICollectionViewLayout。没有 UICollectionView 方法(或委托或数据源方法)提及装饰视图。UICollectionView 对它们一无所知;它只是做它被告知的事情。

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

为了说明,我将继承 UICollectionViewFlowLayout 以在集合视图的内容矩形的顶部施加一个标题标签。这可能是对装饰视图的愚蠢使用,但它完美地说明了基本原理。为简单起见,我将从对整个内容进行硬编码开始,使客户端无法自定义此视图的任何方面。

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

  1. 定义一个 UICollectionReusableView 子类。

  2. 通过调用将 UICollectionReusableView 子类注册到布局(而不是集合视图)register(_:forDecorationViewOfKind:)。布局的初始化程序是执行此操作的好地方。

  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”出现在集合视图的顶部。


现在我将展示如何使标签可定制。我们将允许客户端设置确定标题的属性,而不是标题“测试”。我会给我的布局子类一个公共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 的子类,因此可能还需要进行一些进一步的修改,以便通过下推其他元素来为装饰视图腾出空间。此外,从技术上讲,我们应该isEqual(_:)在 MyTitleView 中覆盖以区分不同的标题。所有这些都留给读者作为练习。

于 2016-08-30T02:57:10.417 回答
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 的控制器中使用以下代码注册此子类(用您的自定义布局替换代码)

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 的集合视图编程指南

更新:为装饰视图而不是 UICollectionViewCell 子类化 UICollectionReusableView 可能更好

于 2012-10-10T17:10:44.257 回答
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...
}

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

于 2013-01-03T23:38:43.127 回答
0

就我而言:我想从 UITableView 升级到 UICollectionView。

uitableview 部分 >>> 补充视图

uitableview headerView >>> 装饰视图

就我而言,我觉得子类化布局并做其他事情,对于简单的“headerView”(装饰)来说“太多了”

所以我的解决方案只是创建标题视图(不是部分)作为第一个单元格 和第1部分作为第一部分(第 0 部分的大小为零)

于 2018-06-19T06:48:29.253 回答