UICollection View Flow Layout Vertical Align

2020-05-21 09:20发布

By default, when you are using the flow layout in a collection view, cells are centered vertically. Is there a way to change this alignment ?

enter image description here

10条回答
混吃等死
2楼-- · 2020-05-21 09:52

following code worked for me

@interface TopAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout

- (void)alignToTopForSameLineElements:(NSArray *)sameLineElements;

@end

@implementation TopAlignedCollectionViewFlowLayout

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
{
    NSArray *attrs = [super layoutAttributesForElementsInRect:rect];
    CGFloat baseline = -2;
    NSMutableArray *sameLineElements = [NSMutableArray array];
    for (UICollectionViewLayoutAttributes *element in attrs) {
        if (element.representedElementCategory == UICollectionElementCategoryCell) {
            CGRect frame = element.frame;
            CGFloat centerY = CGRectGetMidY(frame);
            if (ABS(centerY - baseline) > 1) {
                baseline = centerY;
                [self alignToTopForSameLineElements:sameLineElements];
                [sameLineElements removeAllObjects];
            }
            [sameLineElements addObject:element];
        }
    }
    [self alignToTopForSameLineElements:sameLineElements];//align one more time for the last line
    return attrs;
}

- (void)alignToTopForSameLineElements:(NSArray *)sameLineElements
{
    if (sameLineElements.count == 0) {
        return;
    }
    NSArray *sorted = [sameLineElements sortedArrayUsingComparator:^NSComparisonResult(UICollectionViewLayoutAttributes *obj1, UICollectionViewLayoutAttributes *obj2) {
        CGFloat height1 = obj1.frame.size.height;
        CGFloat height2 = obj2.frame.size.height;
        CGFloat delta = height1 - height2;
        return delta == 0. ? NSOrderedSame : ABS(delta)/delta;
    }];
    UICollectionViewLayoutAttributes *tallest = [sorted lastObject];
    [sameLineElements enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *obj, NSUInteger idx, BOOL *stop) {
        obj.frame = CGRectOffset(obj.frame, 0, tallest.frame.origin.y - obj.frame.origin.y);
    }];
}

@end
查看更多
smile是对你的礼貌
3楼-- · 2020-05-21 09:53

@DongXu: Your solution worked for me too. Here is the Xamarin.iOS version if it:

public class TopAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout
{
    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
    {
        if (base.LayoutAttributesForElementsInRect(rect) is UICollectionViewLayoutAttributes[] attrs)
        {
            // Find all the cells and group them together by the rows they appear on
            var cellsGroupedByRow = attrs
                .Where(attr => attr.RepresentedElementCategory == UICollectionElementCategory.Cell)
                // The default flow layout aligns cells in the middle of the row.
                // Thus, cells with the same Y center point are in the same row.
                // Convert to int, otherwise float values can be slighty different for cells on the same row and cause bugs.
                .GroupBy(attr => Convert.ToInt32(attr.Frame.GetMidY()));

            foreach (var cellRowGroup in cellsGroupedByRow)
            {
                TopAlignCellsOnSameLine(cellRowGroup.ToArray());
            }

            return attrs;
        }

        return null;
    }

    private static void TopAlignCellsOnSameLine(UICollectionViewLayoutAttributes[] cells)
    {
        // If only 1 cell in the row its already top aligned.
        if (cells.Length <= 1) return;

        // The tallest cell has the correct Y value for all the other cells in the row
        var tallestCell = cells.OrderByDescending(cell => cell.Frame.Height).First();

        var topOfRow = tallestCell.Frame.Y;

        foreach (var cell in cells)
        {
            if (cell.Frame.Y == topOfRow) continue;

            var frame = cell.Frame;

            frame.Y = topOfRow;

            cell.Frame = frame;
        }
    }
}
查看更多
你好瞎i
4楼-- · 2020-05-21 10:01

@DongXu's answer is correct. However, I suggest to make those calculations in UICollectionViewFlowLayout's prepare() method. It will prevent multiple calculations on the same cell's attributes. Moreover, prepare() is better place to manage attributes cache.

查看更多
Emotional °昔
5楼-- · 2020-05-21 10:02

I have used something similar to the before answers. In my case I want to align cells by colum with different heights.

import UIKit

class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        if let attributes = super.layoutAttributesForElements(in: rect) {
            let sectionElements: [Int : [UICollectionViewLayoutAttributes]] = attributes
                .filter {
                    return $0.representedElementCategory == .cell //take cells only
                }.groupBy {
                    return $0.indexPath.section //group attributes by section
            }

            sectionElements.forEach { (section, elements) in
                //get suplementary view (header) to align each section
                let suplementaryView = attributes.first {
                    return $0.representedElementCategory == .supplementaryView && $0.indexPath.section == section
                }
                //call align method
                alignToTopSameSectionElements(elements, with: suplementaryView)
            }

            return attributes
        }

        return super.layoutAttributesForElements(in: rect)
    }

    private func alignToTopSameSectionElements(_ elements: [UICollectionViewLayoutAttributes], with suplementaryView: UICollectionViewLayoutAttributes?) {
        //group attributes by colum 
        let columElements: [Int : [UICollectionViewLayoutAttributes]] = elements.groupBy {
            return Int($0.frame.midX)
        }

        columElements.enumerated().forEach { (columIndex, object) in
            let columElement = object.value.sorted {
                return $0.indexPath < $1.indexPath
            }

            columElement.enumerated().forEach { (index, element) in
                var frame = element.frame

                if columIndex == 0 {
                    frame.origin.x = minimumLineSpacing
                }

                switch index {
                case 0:
                    if let suplementaryView = suplementaryView {
                        frame.origin.y = suplementaryView.frame.maxY
                    }
                default:
                    let beforeElement = columElement[index-1]
                    frame.origin.y = beforeElement.frame.maxY + minimumInteritemSpacing
                }

                element.frame = frame
            }
        }
    }
}

public extension Array {

    func groupBy <U> (groupingFunction group: (Element) -> U) -> [U: Array] {

        var result = [U: Array]()

        for item in self {

            let groupKey = group(item)

            if result.has(groupKey) {
                result[groupKey]! += [item]
            } else {
                result[groupKey] = [item]
            }
        }

        return result
    }
}

This is the result of this layout:

enter image description here

查看更多
登录 后发表回答