How to interpolate custom UICollectionViewLayoutAttributes properties with UICollectionViewTransitionLayout

后端 未结 1 532
夕颜
夕颜 2021-02-05 22:22

I have two custom UICollectionViewLayout objects that use a custom UICollectionViewLayoutAttributes subclass. These custom attributes add a single prop

1条回答
  •  隐瞒了意图╮
    2021-02-05 22:40

    Until a better solution is proposed, I have implemented the following workaround...

    The default implementation calls into the current & next layouts from [super prepareLayout] to choose & cache the layout attributes that need to be transitioned from/to. Because we don't get access to this cache (my main gripe!), we can't use them directly during the transition. Instead, I construct my own cache of these attributes when the default implementation calls through for the interpolated layout attributes. This can only happen in layoutAttributesForElementsInRect: (treading close to the problem of currentLayout.collectionView == nil), but fortunately it seems this method is first called in the same run loop as the transition starting, and before the collectionView property is set to nil. This gives an opportunity to establish our from/to layout attributes and cache them for the duration of the transition.

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


    The override of layoutAttributesForElementsInRect: simply calls into the layoutAttributesFor...atIndexPath: for each element index path that super wants to return attributes for, which caches the from/to attributes as it goes. For example, the layoutAttributesForItemAtIndexPath: method looks something like this:

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


    Which just leaves a new method that does the actual interpolation, which is where you have to not only interpolate the custom layout attribute properties, but reimplement the default interpolation (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;
    }
    


    In Summary...

    So what's frustrating about this is that it's a massive amount of code (much isn't shown here for brevity), and most of it is just replicating or trying to replicate what the default implementation is doing anyway. This results in worse performance, and wastes development time for something that could really be so much simpler if UICollectionViewTransitionLayout exposed a single method to override, such as:

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


    The good thing about this workaround is that you don't have to reimplement the code that decides which layout attributes are visible at the start/end of the transition - the default implementation does that for us. Nor do we have to get the attributes for everything each time the layout is invalidated, and then check for items that intersect the visible rect.

    0 讨论(0)
提交回复
热议问题