web-dev-qa-db-fra.com

Séparateurs de ligne personnalisés UICollectionView

Je veux faire séparateurs noirs 2pt in UICollectionView pour notre nouvelle application. Capture d'écran de notre application est ci-dessous. Nous ne pouvions pas utiliser UITableView, car nous avons des animations d'insertion/suppression personnalisées, des effets de défilement et de parallaxe, etc.

Example

13
Anton Gaenko

J'ai commencé avec trois idées pour le faire:

  • mettre en œuvre ces séparateurs à droite à l'intérieur des cellules
  • utiliser fond noir uni avec minimumLineSpacing, nous verrons donc le fond dans les espaces entre les cellules
  • utiliser layout personnalisé et implémenter ces séparateurs comme décorations

Les deux premières variantes ont été rejetées à cause d'une incohérence idéologique, d'animations personnalisées et d'un contenu inférieur à la collection. De plus, j'ai déjà une mise en page personnalisée.

Je vais décrire les étapes avec une sous-classe personnalisée de UICollectionViewFlowLayout

--1--

Implémentez la sous-classe UICollectionReusableView personnalisée.

@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--

Dites mise en page pour utiliser des décorations personnalisées. Faites également un espacement entre les cellules.

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

--3--

Dans la sous-classe UICollectionViewFlowLayout personnalisée, nous devrions renvoyer UICollectionViewLayoutAttributes pour les décorations de 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--

Pour rect visible, nous devrions retourner les chemins d'index des décorations visibles.

- (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--

Nous devrions aussi implémenter 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--

Parfois, je trouvais que cette solution donnait des problèmes visuels avec l’apparence de décorations, ce qui a été corrigé avec l’application de initialLayoutAttributesForAppearingDecorationElementOfKind.

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

C'est tout. Pas trop de code mais bien fait.

41
Anton Gaenko

Excellente suggestion de Anton, mais je pense que la mise en œuvre dans la sous-classe FlowLayout peut être encore plus simple. Étant donné que la super-implémentation de - (NSArray *) layoutAttributesForElementsInRect: (CGRect) rect renvoie déjà les attributs de présentation des cellules, y compris leur cadre et indexPath, vous avez suffisamment d'informations pour calculer les cadres des séparateurs en ne surchargeant que cette méthode et en analysant la structure de cellule. les attributs:

- (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];
}
11
Werner Altewischer

Solution rapide dans Swift

1. Créez le fichier CustomFlowLayout.Swift et collez le code suivant

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. Configurer le flux personnalisé

Dans le générateur d'interface, sélectionnez votre UICollectionViewFlow et définissez notre nouveau nom de classe CustomFlowLayout

3. Changer une couleur de séparateur

Dans SeparatorView, vous pouvez changer la couleur du séparateur dans init

4. Modifier la hauteur du séparateur

Vous pouvez le faire de deux manières différentes

  • Dans le storyboboard. Changer une propriété Min Spacing for Lines

OR

  • Dans le code. Définir la valeur pour minimumLineSpacing 

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

Merci, Anton & Werner, tous les deux m'ont aidé - j'ai pris votre aide pour créer une solution de glisser-déposer, en tant que catégorie sur UICollectionView, pensant partager les résultats:

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

En utilisant le runtime Objective-C et certains swizzling , des séparateurs de cellules peuvent être ajoutés avec quelques lignes à toute UICollectionView existante dont la présentation est/hérite de UICollectionViewFlowLayout.

Exemple d'utilisation:

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

Quelques notes:

  • La hauteur/largeur du séparateur peut être déterminée par section, en utilisant collectionView:layout:minimumLineSpacingForSectionAtIndex:, retombant sur minimumLineSpacing si non implémenté
  • Construit pour gérer la direction de défilement horizontale ou verticale

J'espère que ça aide

2
beebcon

Voici la version de Anton Gaenko mais implémentée en C #, cela pourrait être utile pour les utilisateurs de 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);
    }
}
1
Pataphysicien