【问题标题】:UICollectionView custom line separatorsUICollectionView 自定义行分隔符
【发布时间】:2015-04-25 19:36:29
【问题描述】:

我想在UICollectionView 中为我们的新应用制作2pt 黑色分隔符。我们的应用程序的屏幕截图如下。我们不能使用UITableView,因为我们有自定义插入/删除动画、滚动和视差效果等等。

【问题讨论】:

  • 必须有一种更简单的方法......

标签: ios objective-c uicollectionview uicollectionviewlayout uicollectionreusableview


【解决方案1】:

我从三个想法开始:

  • 单元格内实现这些分隔符
  • 使用纯黑色背景minimumLineSpacing,因此我们会在单元格之间的空间中看到背景
  • 使用自定义布局并将此分隔符实现为装饰

前两个变体被拒绝,因为意识形态不一致、自定义动画和内容低于集合。我也已经有了自定义布局。

我将使用UICollectionViewFlowLayout 的自定义子类来描述这些步骤。

--1--

实现自定义UICollectionReusableView 子类。

@interface FLCollectionSeparator : UICollectionReusableView

@end

@implementation FLCollectionSeparator

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
    }

    return self;
}

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    self.frame = layoutAttributes.frame;
}

@end

--2--

说布局使用自定义装饰。还要在单元格之间设置行距。

UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*) self.newsCollection.collectionViewLayout;
[layout registerClass:[FLCollectionSeparator class] forDecorationViewOfKind:@"Separator"];
layout.minimumLineSpacing = 2;

--3--

在自定义UICollectionViewFlowLayout 子类中,我们应该返回UICollectionViewLayoutAttributes 以获取来自layoutAttributesForElementsInRect 的装饰。

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    ... collect here layout attributes for cells ... 

    NSMutableArray *decorationAttributes = [NSMutableArray array];
    NSArray *visibleIndexPaths = [self indexPathsOfSeparatorsInRect:rect]; // will implement below

    for (NSIndexPath *indexPath in visibleIndexPaths) {
        UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForDecorationViewOfKind:@"Separator" atIndexPath:indexPath];
        [decorationAttributes addObject:attributes];
    }

    return [layoutAttributesArray arrayByAddingObjectsFromArray:decorationAttributes];
}

--4--

对于可见的矩形,我们应该返回可见的装饰索引路径。

- (NSArray*)indexPathsOfSeparatorsInRect:(CGRect)rect {
    NSInteger firstCellIndexToShow = floorf(rect.origin.y / self.itemSize.height);
    NSInteger lastCellIndexToShow = floorf((rect.origin.y + CGRectGetHeight(rect)) / self.itemSize.height);
    NSInteger countOfItems = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:0];

    NSMutableArray* indexPaths = [NSMutableArray new];
    for (int i = MAX(firstCellIndexToShow, 0); i <= lastCellIndexToShow; i++) {
        if (i < countOfItems) {
            [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
        }
    }
    return indexPaths;
}

--5--

我们还应该实现layoutAttributesForDecorationViewOfKind

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
    CGFloat decorationOffset = (indexPath.row + 1) * self.itemSize.height + indexPath.row * self.minimumLineSpacing;
    layoutAttributes.frame = CGRectMake(0.0, decorationOffset, self.collectionViewContentSize.width, self.minimumLineSpacing);
    layoutAttributes.zIndex = 1000;

    return layoutAttributes;
}

--6--

有时我发现此解决方案会在装饰外观上出现视觉故障,通过实施 initialLayoutAttributesForAppearingDecorationElementOfKind 已解决此问题。

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingDecorationElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)decorationIndexPath {
    UICollectionViewLayoutAttributes *layoutAttributes =  [self layoutAttributesForDecorationViewOfKind:elementKind atIndexPath:decorationIndexPath];
    return layoutAttributes;
}

就是这样。代码不多,但做得对。

【讨论】:

  • 如果现在有超出前几行的行,是否有办法阻止该行显示。好像不管有没有内容都会生成行
  • @CoderNinja 你应该看看我们从数据源得到numberOfItemsInSection的第4步,所以只有当你有单元格内容时才应该显示分隔符
【解决方案2】:

Swift 中的快速解决方案

1.创建 CustomFlowLayout.swift 文件并粘贴下一段代码

import UIKit

private let separatorDecorationView = "separator"

final class CustomFlowLayout: UICollectionViewFlowLayout {

    override func awakeFromNib() {
        super.awakeFromNib()
        register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView)
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect) ?? []
        let lineWidth = self.minimumLineSpacing

        var decorationAttributes: [UICollectionViewLayoutAttributes] = []

        // skip first cell
        for layoutAttribute in layoutAttributes where layoutAttribute.indexPath.item > 0 {
            let separatorAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: separatorDecorationView,
                                                                      with: layoutAttribute.indexPath)
            let cellFrame = layoutAttribute.frame
            separatorAttribute.frame = CGRect(x: cellFrame.origin.x,
                                              y: cellFrame.origin.y - lineWidth,
                                              width: cellFrame.size.width,
                                              height: lineWidth)
            separatorAttribute.zIndex = Int.max
            decorationAttributes.append(separatorAttribute)
        }

        return layoutAttributes + decorationAttributes
    }

}

private final class SeparatorView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .red
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        self.frame = layoutAttributes.frame
    }
}

2。设置自定义流程

在界面构建器中选择您的 UICollectionViewFlow 并设置我们的新类名CustomFlowLayout

3.更改分隔符颜色

在 SeparatorView 中,您可以更改 init 中分隔符的颜色

4.更改分隔符的高度

你可以用两种不同的方式来做

  • 在故事板中。更改属性Min Spacing for Lines

  • 在代码中。为minimumLineSpacing 设置值

    override func awakeFromNib() {
        super.awakeFromNib()
        register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView)
        minimumLineSpacing = 2 }
    

【讨论】:

  • 小提示:如果要插入和删除单元格,使用.isHidden.alpha 比在索引0 处保留装饰器更容易。
  • awakeFromNib 在以编程方式执行 UI 时甚至不会被调用,因此必须在创建 FlowLayout 之后注册装饰器并指定间距。
  • 对于编程使用需要 awakeFromNib 更改为:覆盖 init() { super.init() register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView) minimumLineSpacing = 2 } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
【解决方案3】:

Anton 的建议很好,但我认为 FlowLayout 子类中的实现可以更简单。因为 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 的超级实现已经返回了单元格的布局属性,包括它们的 frame 和 indexPath,所以您有足够的信息来计算分隔符的框架,只需覆盖此方法并内省单元格布局属性:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *layoutAttributesArray = [super layoutAttributesForElementsInRect:rect];

    CGFloat lineWidth = self.minimumLineSpacing;
    NSMutableArray *decorationAttributes = [[NSMutableArray alloc] initWithCapacity:layoutAttributesArray.count];

    for (UICollectionViewLayoutAttributes *layoutAttributes in layoutAttributesArray) {
        //Add separator for every row except the first
        NSIndexPath *indexPath = layoutAttributes.indexPath;
        if (indexPath.item > 0) {
            UICollectionViewLayoutAttributes *separatorAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:kCellSeparatorKind withIndexPath:indexPath];
            CGRect cellFrame = layoutAttributes.frame;

            //In my case I have a horizontal grid, where I need vertical separators, but the separator frame can be calculated as needed
            //e.g. top, or both top and left
            separatorAttributes.frame = CGRectMake(cellFrame.origin.x - lineWidth, cellFrame.origin.y, lineWidth, cellFrame.size.height);
            separatorAttributes.zIndex = 1000;
            [decorationAttributes addObject:separatorAttributes];
        }
    }
    return [layoutAttributesArray arrayByAddingObjectsFromArray:decorationAttributes];
}

【讨论】:

  • 这是一个很好的附加答案。公认的答案是完全正确的,但在几乎所有情况下,您都可以使用这种方法大大简化第 3 步和第 4 步,并完全跳过第 5 步! UICV 布局的诀窍是尽可能少地做。
  • 我用这个作为一个很好的简化以及接受的答案,效果很好!
  • 这似乎只在您不插入或删除单元格时才有效。然后你也必须实现func layoutAttributesForXXXView(ofKind elementKind: String, at indexPath: IndexPath)
【解决方案4】:

谢谢,Anton 和 Werner,他们都帮助了我 - 我已经在您的帮助下制作了拖放解决方案,作为 UICollectionView 上的一个类别,我想我会分享结果:

UICollectionView+Separators.h

#import <UIKit/UIKit.h>

@interface UICollectionView (Separators)

@property (nonatomic) BOOL sep_useCellSeparators;
@property (nonatomic, strong) UIColor *sep_separatorColor;

@end

UICollectionView+Separators.m

#import "UICollectionView+Separators.h"
@import ObjectiveC;

#pragma mark -
#pragma mark -

@interface UICollectionViewLayoutAttributes (SEPLayoutAttributes)

@property (nonatomic, strong) UIColor *sep_separatorColor;

@end

@implementation UICollectionViewLayoutAttributes (SEPLayoutAttributes)

- (void)setSep_separatorColor:(UIColor *)sep_separatorColor
{
    objc_setAssociatedObject(self, @selector(sep_separatorColor), sep_separatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIColor *)sep_separatorColor
{
    return objc_getAssociatedObject(self, @selector(sep_separatorColor));
}

@end

#pragma mark -
#pragma mark -

@interface SEPCollectionViewCellSeparatorView : UICollectionReusableView

@end

@implementation SEPCollectionViewCellSeparatorView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.backgroundColor = [UIColor blackColor];
    }

    return self;
}

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    self.frame = layoutAttributes.frame;

    if (layoutAttributes.sep_separatorColor != nil)
    {
        self.backgroundColor = layoutAttributes.sep_separatorColor;
    }
}

@end

#pragma mark -
#pragma mark -

static NSString *const kCollectionViewCellSeparatorReuseId = @"kCollectionViewCellSeparatorReuseId";

@implementation UICollectionViewFlowLayout (SEPCellSeparators)

#pragma mark - Setters/getters

- (void)setSep_separatorColor:(UIColor *)sep_separatorColor
{
    objc_setAssociatedObject(self, @selector(sep_separatorColor), sep_separatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    [self invalidateLayout];
}

- (UIColor *)sep_separatorColor
{
    return objc_getAssociatedObject(self, @selector(sep_separatorColor));
}

- (void)setSep_useCellSeparators:(BOOL)sep_useCellSeparators
{
    if (self.sep_useCellSeparators != sep_useCellSeparators)
    {
        objc_setAssociatedObject(self, @selector(sep_useCellSeparators), @(sep_useCellSeparators), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

        [self registerClass:[SEPCollectionViewCellSeparatorView class] forDecorationViewOfKind:kCollectionViewCellSeparatorReuseId];
        [self invalidateLayout];
    }
}

- (BOOL)sep_useCellSeparators
{
    return [objc_getAssociatedObject(self, @selector(sep_useCellSeparators)) boolValue];
}

#pragma mark - Method Swizzling

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(layoutAttributesForElementsInRect:);
        SEL swizzledSelector = @selector(swizzle_layoutAttributesForElementsInRect:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (NSArray<UICollectionViewLayoutAttributes *> *)swizzle_layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *layoutAttributesArray = [self swizzle_layoutAttributesForElementsInRect:rect];

    if (self.sep_useCellSeparators == NO)
    {
        return layoutAttributesArray;
    }

    CGFloat lineSpacing = self.minimumLineSpacing;

    NSMutableArray *decorationAttributes = [[NSMutableArray alloc] initWithCapacity:layoutAttributesArray.count];

    for (UICollectionViewLayoutAttributes *layoutAttributes in layoutAttributesArray)
    {
        NSIndexPath *indexPath = layoutAttributes.indexPath;

        if (indexPath.item > 0)
        {
            id <UICollectionViewDelegateFlowLayout> delegate = (id <UICollectionViewDelegateFlowLayout>)self.collectionView.delegate;
            if ([delegate respondsToSelector:@selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:)])
            {
                lineSpacing = [delegate collectionView:self.collectionView layout:self minimumLineSpacingForSectionAtIndex:indexPath.section];
            }

            UICollectionViewLayoutAttributes *separatorAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:kCollectionViewCellSeparatorReuseId withIndexPath:indexPath];
            CGRect cellFrame = layoutAttributes.frame;

            if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
            {
                separatorAttributes.frame = CGRectMake(cellFrame.origin.x - lineSpacing, cellFrame.origin.y, lineSpacing, cellFrame.size.height);
            }
            else
            {
                separatorAttributes.frame = CGRectMake(cellFrame.origin.x, cellFrame.origin.y - lineSpacing, cellFrame.size.width, lineSpacing);
            }

            separatorAttributes.zIndex = 1000;

            separatorAttributes.sep_separatorColor = self.sep_separatorColor;

            [decorationAttributes addObject:separatorAttributes];
        }
    }

    return [layoutAttributesArray arrayByAddingObjectsFromArray:decorationAttributes];
}

@end

#pragma mark -
#pragma mark -

@implementation UICollectionView (Separators)

- (UICollectionViewFlowLayout *)sep_flowLayout
{
    if ([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]])
    {
        return (UICollectionViewFlowLayout *)self.collectionViewLayout;
    }
    return nil;
}

- (void)setSep_separatorColor:(UIColor *)sep_separatorColor
{
    [self.sep_flowLayout setSep_separatorColor:sep_separatorColor];
}

- (UIColor *)sep_separatorColor
{
    return [self.sep_flowLayout sep_separatorColor];
}

- (void)setSep_useCellSeparators:(BOOL)sep_useCellSeparators
{
    [self.sep_flowLayout setSep_useCellSeparators:sep_useCellSeparators];
}

- (BOOL)sep_useCellSeparators
{
    return [self.sep_flowLayout sep_useCellSeparators];
}

@end

使用 Objective-C 运行时和一些 swizzling,单元格分隔符可以通过几行添加到任何现有的 UICollectionView,其布局是/继承自 UICollectionViewFlowLayout

示例用法:

#import "UICollectionView+Separators.h"
...
self.collectionView.sep_useCellSeparators = YES;
self.collectionView.sep_separatorColor = [UIColor blackColor];

几个注意事项:

  • 可以使用collectionView:layout:minimumLineSpacingForSectionAtIndex: 确定每个部分的分隔符高度/宽度,如果未实现则回退到minimumLineSpacing
  • 专为处理水平或垂直滚动​​方向而设计

希望对你有帮助

【讨论】:

  • 在 Swizzling 时也要小心——这恰好在我的项目中非常方便,它使用了许多不同的集合视图和 Flow Layout 的子类
  • 我不喜欢在 Objective C 和 Swift 中混合使用下划线和驼峰式大小写。我不确定为什么你甚至需要 sep_ 前缀。并且缩写可能对您(现在)有意义,但其他开发人员在阅读您的代码时会有认知负担,您将来也可能如此。
  • 谢谢,虽然我理解您的推理,但我发现最好在类类别上使用此类前缀以确保可区分性并避免任何可能的冲突
  • 一般来说这很好用,但是我发现如果你执行诸如删除单元格[tabsCollectionView performBatchUpdates:^{ [tabEntries removeObjectAtIndex: cellToClose.cellIndex]; [tabsCollectionView deleteItemsAtIndexPaths: @[removedIndexPath]]; } completion: NULL]; 之类的操作它会崩溃(没有 UICollectionViewLayoutAttributes 实例用于 -layoutAttributesForDecorationViewOfKindOfKind: kCollectionViewCellSeparatorReuseId at path ...)。
【解决方案5】:

这是来自 Anton Gaenko 的版本,但使用 C# 实现,这可能对 Xamarin 用户有用:

[Register(nameof(FLCollectionSeparator))]
public class FLCollectionSeparator : UICollectionReusableView
{
    public FLCollectionSeparator(CGRect frame) : base(frame)
    {
        this.BackgroundColor = UIColor.Black;
    }
    public FLCollectionSeparator(IntPtr handle) : base(handle)
    {
        this.BackgroundColor = UIColor.Black;
    }
    public override void ApplyLayoutAttributes(UICollectionViewLayoutAttributes layoutAttributes)
    {
        this.Frame = layoutAttributes.Frame;
    }
}

[Register(nameof(UILinedSpacedViewFlowLayout))]
public class UILinedSpacedViewFlowLayout : UICollectionViewFlowLayout
{
    public const string SeparatorAttribute = "Separator";
    private static readonly NSString NSSeparatorAttribute = new NSString(SeparatorAttribute);
    public UILinedSpacedViewFlowLayout() : base() { this.InternalInit(); }
    public UILinedSpacedViewFlowLayout(NSCoder coder) : base (coder) { this.InternalInit(); }
    protected UILinedSpacedViewFlowLayout(NSObjectFlag t) : base(t) { this.InternalInit(); }
    private void InternalInit()
    {
        this.RegisterClassForDecorationView(typeof(FLCollectionSeparator), NSSeparatorAttribute);
    }
    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
    {
        return LayoutAttributesForElementsInRect_internal(rect).ToArray();
    }
    private IEnumerable<UICollectionViewLayoutAttributes> LayoutAttributesForElementsInRect_internal(CGRect rect)
    {
        foreach (var baseDecorationAttr in base.LayoutAttributesForElementsInRect(rect))
        {
            yield return baseDecorationAttr;
        }
        foreach (var indexPath in this.IndexPathsOfSeparatorsInRect(rect))
        {
            yield return this.LayoutAttributesForDecorationView(NSSeparatorAttribute, indexPath);
        }
    }
    private IEnumerable<NSIndexPath> IndexPathsOfSeparatorsInRect(CGRect rect)
    {
        int firstCellIndexToShow = (int)(rect.Y / this.ItemSize.Height);
        int lastCellIndexToShow  = (int)((rect.Y + rect.Height) / this.ItemSize.Height);
        int countOfItems = (int)this.CollectionView.DataSource.GetItemsCount(this.CollectionView, 0);
        for (int i = Math.Max(firstCellIndexToShow, 0); i <= lastCellIndexToShow; i++)
        {
            if (i < countOfItems)
            {
                yield return NSIndexPath.FromItemSection(i, 0);
            }
        }
    }
    public override UICollectionViewLayoutAttributes LayoutAttributesForDecorationView(NSString kind, NSIndexPath indexPath)
    {
        UICollectionViewLayoutAttributes layoutAttributes = base.LayoutAttributesForDecorationView(kind, indexPath);
        var decorationOffset = (indexPath.Row + 1) * this.ItemSize.Height + indexPath.Row * this.MinimumLineSpacing + this.HeaderReferenceSize.Height;
        layoutAttributes = UICollectionViewLayoutAttributes.CreateForDecorationView(kind, indexPath);
        layoutAttributes.Frame = new CGRect(0, decorationOffset, this.CollectionViewContentSize.Width, this.MinimumLineSpacing);
        layoutAttributes.ZIndex = 1000;
        return layoutAttributes;
    }
    public override UICollectionViewLayoutAttributes InitialLayoutAttributesForAppearingDecorationElement(NSString elementKind, NSIndexPath decorationIndexPath)
    {
        return base.InitialLayoutAttributesForAppearingDecorationElement(elementKind, decorationIndexPath);
    }
}

【讨论】:

【解决方案6】:

在@SlavikVoloshyn 答案中以编程方式使用此部分:

override func awakeFromNib() {
    super.awakeFromNib()
    register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView)
}

需要改成:

override init() {
    super.init()
    register(SeparatorView.self, forDecorationViewOfKind: separatorDecorationView)
    minimumLineSpacing = 2
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-13
    • 2012-10-07
    • 2019-12-19
    • 1970-01-01
    相关资源
    最近更新 更多