[Из песочницы] Пишем универсальный UICollectionViewLayout

UICollectionView может иметь практически любое расположение элементов. Элементы могут иметь как фиксированные размеры, так и динамические. В данной публикации внимание будет уделено только тем UICollectionViewLayout, размеры элементов которых фиксированы и задаются определенным алгоритмом (типичный пример — расположение иконок на экране Home вашего iPhone). Так же будет сделана попытка описать подход к формированию единого UICollectionViewLayout.

Главная особенность подобных UICollectionViewLayout состоит в том, что в каждом таком расположении можно выделить некую закономерность, по которой происходит повторение размеров и положений элементов. В дальнейшем будем называть это шаблоном. Соответственно, чтобы сделать UICollectionViewLayout нужно составить алгоритм, который формирует шаблон. Пример шаблона повторения:

image


Все подобные UICollectionViewLayout будут отличаться друг от друга лишь алгоритмом формирования размеров и положений элементов коллекции. Отсюда возникает желание сделать такой UICollectionViewLayout, который будет требовать лишь алгоритм формирования шаблона, а все дальнейшие действия он возьмет на себя.

Если внимательно посмотреть на предыдущий пример, то можно заметить, что внутри шаблона встречаются такие блоки элементов, которые повторяются несколько раз за шаблон. Принимая во внимание возможность повторения блоков внутри шаблона логичным шагом будет разбиение шаблона на массив блоков.

image

Сложив все идеи вместе и описав все участвующие в формировании шаблона вещи протоколами, получилось примерно следующее:
Протоколы (swift код)
public protocol SquareMosaicBlock {
    
    // количество CGRect в блоке
    func frames() -> Int
    // массив [CGRect] для определенной ширины коллекции и смещения относительно начала
    func frames(origin: CGFloat, width: CGFloat) -> [CGRect]
}

public protocol SquareMosaicPattern {
    
    // массив блоков
    var blocks: [SquareMosaicBlock] { get }
}

public protocol SquareMosaicLayoutDataSource: class {
    
    func pattern() -> SquareMosaicPattern
}


 — Достаточно ли протоколов SquareMosaicPattern, SquareMosaicBlock для описания любого расположения элементов?
 — Расположение элементов коллекции на изображении выше основано только на этих двух протоколах. Поэтому, скорее всего, любое расположение можно описать лишь этими протоколами»

Приводить пример реализации самого UICollectionViewLayout, использующего данные протоколы, и делать его детальный обзор не входит в планы на данную публикацию. Любой желающий сможет самостоятельно проанализировать подход и реализацию, перейдя в репозиторий (вся логика описана всего лишь на ~100 строчках кода). Так же там можно найти пример использования.

Однако, привести пример формирования объектов, соответствующих протоколам шаблона и блока непременно следует. Для наглядности будут описаны расположения элементов для коллекции, которая изображена на картинках.

Объекты, соответствующие протоколам (swift код)
struct SnakeSquareMosaicPattern: SquareMosaicPattern {
    
    var blocks: [SquareMosaicBlock] {
        return [
            OneTwoSquareMosaicBlock(),
            ThreeRightSquareMosaicBlock(),
            TwoOneSquareMosaicBlock(),
            ThreeRightSquareMosaicBlock()
        ]
    }
}

public struct OneTwoSquareMosaicBlock: SquareMosaicBlock {
    
    public func frames() -> Int {
        return 3
    }
    
    public func frames(origin: CGFloat, width: CGFloat) -> [CGRect] {
        let sideMin = width / 3.0
        let sideMax = width - sideMin
        var frames = [CGRect]()
        frames.append(CGRect(x: 0, y: origin, width: sideMax, height: sideMax))
        frames.append(CGRect(x: sideMax, y: origin, width: sideMin, height: sideMin))
        frames.append(CGRect(x: sideMax, y: origin + sideMax - sideMin, width: sideMin, height: sideMin))
        return frames
    }
}

public struct TwoOneSquareMosaicBlock: SquareMosaicBlock {
    
    public func frames() -> Int {
        return 3
    }
    
    public func frames(origin: CGFloat, width: CGFloat) -> [CGRect] {
        let sideMin = width / 3.0
        let sideMax = width - sideMin
        var frames = [CGRect]()
        frames.append(CGRect(x: 0, y: origin, width: sideMin, height: sideMin))
        frames.append(CGRect(x: 0, y: origin + sideMax - sideMin, width: sideMin, height: sideMin))
        frames.append(CGRect(x: sideMin, y: origin, width: sideMax, height: sideMax))
        return frames
    }
}

public struct ThreeRightSquareMosaicBlock: SquareMosaicBlock {
    
    public func frames() -> Int {
        return 3
    }
    
    public func frames(origin: CGFloat, width: CGFloat) -> [CGRect] {
        let side = width / 3.0
        var frames = [CGRect]()
        frames.append(CGRect(x: side + side, y: origin, width: side, height: side))
        frames.append(CGRect(x: side, y: origin, width: side, height: side))
        frames.append(CGRect(x: 0, y: origin, width: side, height: side))
        return frames
    }
}


В итоге у нас получился вполне универсальный класс SquareMosaicLayout. На его основе можно составлять разные наборы расположений элементов UICollectionView. Анимированно переходить от одного шаблона к другому. Переиспользовать блоки разных шаблонов для составления новых.

Следующим этапом развития будет создание и пополнение предустановленных наборов блоков и шаблонов с разнообразными расположениями элементов. Так же хотелось бы добавить поддержку различных видов анимаций и SupplementaryViews / DecorationViews

Вывод: Самое главное, чего удалось добиться — не требуется менять внутренности класса SquareMosaicLayout для составления нового расположения элементов.

P.S. Доступно для установки через cocoapods: pod 'SquareMosaicLayout'

Комментарии (0)

© Habrahabr.ru