Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to interpolate custom UICollectionViewLayoutAttributes properties with UICollectionViewTransitionLayout

I have two custom UICollectionViewLayout objects that use a custom UICollectionViewLayoutAttributes subclass. These custom attributes add a single property tintAlpha that controls the opacity of a tint overlay view attached to each collection view cell.

I now want to transition between these two layouts, using a UICollectionViewTransitionLayout subclass. How can I configure the transition layout subclass to interpolate the custom tintAlpha property on my custom layout attributes?

I could do something like this:

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

However this will ignore any changes applied to the attributes in initialLayoutAttributesForAppearingItemAtIndexPath: & finalLayoutAttributesForDisappearingItemAtIndexPath: in the current or next layout, and so is not actually correct. As far as I can tell, the default implementation of UICollectionViewTransitionLayout determines the appropriate from/to attributes and caches them, either in prepareLayout or layoutAttributesForItemAtIndexPath:. It would be so useful to have some public API on UICollectionViewTransitionLayout to allow us to access these from/to attributes objects, as if I try and implement my own logic on whether to use the initial/final attributes vs the standard attributes there are bound to be some discrepencies from the default implementation.

Is there a better way to interpolate these custom attributes during a layout transition?


Update:

I have just encountered an additional problem with this scenario. In the code above, when getting fromAttr & toAttr directly from the current/next layouts, the collectionView is nil for the current layout (beyond the first run loop of the transition at least). If layout depends at all on the collection view's bounds - consider a simple cover flow layout for example - then the fromAttr will be incorrect.

I'm really pining for a interpolatedLayoutAttributesFromLayoutAttributes:toLayoutAttributes:progress: on UICollectionViewTransitionLayout that can be overridden by subclasses.

like image 870
Stuart Avatar asked Mar 18 '14 12:03

Stuart


1 Answers

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.

like image 131
Stuart Avatar answered Oct 14 '22 21:10

Stuart