Как интерполировать пользовательские свойства UICollectionViewLayoutAttributes с помощью UICollectionViewTransitionLayout

У меня есть два пользовательских объекта UICollectionViewLayout, которые используют пользовательский подкласс UICollectionViewLayoutAttributes. Эти настраиваемые атрибуты добавляют одно свойство tintAlpha, которое управляет непрозрачностью отображения оверлей оттенка, прикрепленного к каждой ячейке просмотра коллекции.

Теперь я хочу перейти между этими двумя макетами, используя подкласс UICollectionViewTransitionLayout. Как настроить подклассу макета перехода для интерполирования пользовательского свойства tintAlpha на мои собственные атрибуты макета?

Я мог бы сделать что-то вроде этого:

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
     CustomLayoutAttributes *attr = [super layoutAttributesForItemAtIndexPath:indexPath];

     CustomLayoutAttributes *fromAttr = (CustomLayoutAttributes *)[self.currentLayout layoutAttributesForItemAtIndexPath:indexPath];
     CustomLayoutAttributes *toAttr = (CustomLayoutAttributes *)[self.nextLayout layoutAttributesForItemAtIndexPath:indexPath];

     CGFloat t = self.transitionProgress;
     attr.tintAlpha = (1.0f - t) * fromAttr.tintAlpha + t * toAttr.tintAlpha;

     return attr;
}

Однако это игнорирует любые изменения, применяемые к атрибутам в initialLayoutAttributesForAppearingItemAtIndexPath: и finalLayoutAttributesForDisappearingItemAtIndexPath: в текущем или следующем макете, и на самом деле это не так. Насколько я могу судить, реализация по умолчанию UICollectionViewTransitionLayout определяет соответствующие атрибуты/атрибуты и кэширует их, либо в prepareLayout, либо layoutAttributesForItemAtIndexPath:. Было бы так полезно иметь некоторый публичный API на UICollectionViewTransitionLayout, чтобы позволить нам получить доступ к этим объектам атрибутов/в атрибуты, как если бы я попытался реализовать свою собственную логику о том, следует ли использовать начальные/конечные атрибуты и стандартные атрибуты связаны с некоторыми отличиями от реализации по умолчанию.

Есть ли лучший способ интерполировать эти пользовательские атрибуты во время перехода макета?


Update:

Я только что столкнулся с дополнительной проблемой в этом сценарии. В приведенном выше коде при получении fromAttr и toAttr непосредственно из текущего/следующего макетов, collectionView является nil для текущего макета (за исключением первого цикла цикла перехода, по крайней мере). Если макет вообще зависит от границ просмотра коллекции - рассмотрим, например, простой шаблон потока обложки - тогда будет неверно fromAttr.

Я действительно выгляжу для interpolatedLayoutAttributesFromLayoutAttributes:toLayoutAttributes:progress: на UICollectionViewTransitionLayout, который может быть переопределен подклассами.

Ответ 1

Пока не будет предложено лучшее решение, я применил следующее обходное решение...

Реализация по умолчанию вызывает текущий и следующий макеты из [super prepareLayout] для выбора и кэширования атрибутов макета, которые необходимо переходить с/на. Поскольку мы не получаем доступ к этому кешу (моя главная проблема!), Мы не можем использовать их непосредственно во время перехода. Вместо этого я создаю свой собственный кеш этих атрибутов, когда реализация по умолчанию вызывает интерполированные атрибуты макета. Это может произойти только в layoutAttributesForElementsInRect: (наступая близко к проблеме currentLayout.collectionView == nil), но, к счастью, кажется, что этот метод сначала вызывается в том же цикле запуска, что и начало перехода, и до того, как свойство collectionView установлено на nil. Это дает возможность установить наши атрибуты layout/layout и кешировать их на время перехода.

@interface CustomTransitionLayout ()
@property(nonatomic, strong) NSMutableDictionary *transitionInformation;
@end

@implementation

- (void)prepareLayout
{
    [super prepareLayout];

    if (!self.transitionInformation) {
        self.transitionInformation = [NSMutableDictionary dictionary];
    }
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // Let the super implementation tell us which attributes are required.
    NSArray *defaultLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *layoutAttributes = [NSMutableArray arrayWithCapacity:[defaultLayoutAttributes count]];
    for (UICollectionViewLayoutAttributes *defaultAttr in defaultLayoutAttributes) {
        UICollectionViewLayoutAttributes *attr = defaultAttr;
        switch (defaultAttr.representedElementCategory) {
            case UICollectionElementCategoryCell:
                attr = [self layoutAttributesForItemAtIndexPath:defaultAttr.indexPath];
                break;
            case UICollectionElementCategorySupplementaryView:
                attr = [self layoutAttributesForSupplementaryViewOfKind:defaultAttr.representedElementKind atIndexPath:defaultAttr.indexPath];
                break;
            case UICollectionElementCategoryDecorationView:
                attr = [self layoutAttributesForDecorationViewOfKind:defaultAttr.representedElementKind atIndexPath:defaultAttr.indexPath];
                break;
        }
        [layoutAttributes addObject:attr];
    }
    return layoutAttributes;
}


Переопределение layoutAttributesForElementsInRect: просто вызывает в layoutAttributesFor...atIndexPath: для каждого пути индекса элемента, который super хочет вернуть атрибуты для, который кэширует атрибуты from/to. Например, метод layoutAttributesForItemAtIndexPath: выглядит примерно так:

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSIndexPath *indexPathKey = [indexPath collectionViewKey];

    NSMutableDictionary *info = self.transitionInformation[indexPathKey];
    if (!info) {
        info = [NSMutableDictionary dictionary];
        self.transitionInformation[indexPathKey] = info;
    }

    // Logic to choose layout attributes to interpolate from.
    // (This is not exactly how the default implementation works, but a rough approximation)
    MyLayoutAttributes *fromAttributes = info[TransitionInfoFromAttributesKey];
    if (!fromAttributes) {
        MyLayoutAttributes *standardToAttributes = (MyLayoutAttributes *)[self.nextLayout layoutAttributesForItemAtIndexPath:indexPathKey];
        MyLayoutAttributes *initialAttributes = (MyLayoutAttributes *)[self.nextLayout initialLayoutAttributesForAppearingItemAtIndexPath:indexPathkey];
        if (initialAttributes && ![initialAttributes isEqual:standardToAttributes]) {
            fromAttributes = [initialAttributes copy];
        } else {
            fromAttributes = [(MyLayoutAttributes *)[self.currentLayout layoutAttributesForItemAtIndexPath:indexPathKey] copy];
        }
        info[TransitionInfoFromAttributesKey] = fromAttributes;
    }

    MyLayoutAttributes *toAttributes = info[TransitionInfoToAttributesKey];
    if (!toAttributes) {
        // ... similar logic as for fromAttributes ...
        info[TransitionInfoToAttributesKey] = toAttributes;
    }

    MyLayoutAttributes *attributes = [self interpolatedLayoutAttributesFromLayoutAttributes:fromAttributes
                                                                         toLayoutAttributes:toAttributes
                                                                                   progress:self.transitionProgress];
    return attributes;
}


Который просто оставляет новый метод, который выполняет фактическую интерполяцию, где вам нужно не только интерполировать свойства атрибута пользовательского макета, но и переопределять интерполяцию по умолчанию (center/size/alpha/transform/transform3D):

- (MyLayoutAttributes *)interpolatedLayoutAttributesFromLayoutAttributes:(MyLayoutAttributes *)fromAttributes
                                                      toLayoutAttributes:(MyLayoutAttributes *)toAttributes
                                                                progress:(CGFloat)progress
{
    MyLayoutAttributes *attributes = [fromAttributes copy];

    CGFloat t = progress;
    CGFloat f = 1.0f - t;

    // Interpolate all the default layout attributes properties.
    attributes.center = CGPointMake(f * fromAttributes.x + t * toAttributes.center.x,
                                    f * fromAttributes.y + t * toAttributes.center.y);
    // ...

    // Interpolate any custom layout attributes properties.
    attributes.customProperty = f * fromAttributes.customProperty + t * toAttributes.customProperty;
    // ...

    return attributes;
}


В резюме...

Так что разочарование в этом заключается в том, что он представляет собой огромное количество кода (многое не показано здесь для краткости), и большая его часть просто реплицирует или пытается реплицировать то, что делает реализация по умолчанию в любом случае. Это приводит к ухудшению производительности и сокращению времени разработки для чего-то, что действительно может быть , поэтому намного проще, если UICollectionViewTransitionLayout обнаружил один метод для переопределения, например:

- (UICollectionViewLayoutAttributes *)interpolatedLayoutAttributesFromLayoutAttributes:(UICollectionViewLayoutAttributes *)fromAttributes
                                                                    toLayoutAttributes:(UICollectionViewLayoutAttributes *)toAttributes
                                                                              progress:(CGFloat)progress
{
    MyLayoutAttributes *attributes = (MyLayoutAttributes *)[super interpolatedLayoutAttributesFromLayoutAttributes:fromAttributes toLayoutAttributes:toAttributes progress:progress];
    attributes.customProperty = (1.0f - progress) * fromAttributes.customProperty + progress * toAttributes.customProperty;
    return attributes;
}


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