Search code examples
iosios6core-animationuicollectionviewuicollectionviewlayout

Interpolation issue on custom UICollectionViewFlowLayout


I'm having an issue with a custom UICollectionViewFlowLayout

Like this video shows Issue Video, when my cell bounces back to the center (0:12 - 0:16) my transformation interpolation fails, and it jumps to the center position in 1 frame.

I isolated the issue, it manifests when distanceToCenterX == 0.0f (code below)

This is a printf of distanceToCenterX for several calls of this method for the central cell:

-92.000000           
-91.500000           
-91.000000           
-90.500000           
-90.000000           
-89.500000           
-89.000000         
-88.500000
0.000000 

You can see how it jumps from -88.5000 to 0.00 very abruptly, what could I do smooth out the transition and avoid the 1 frame jump?

Code (disclaimer, took it from Apple Examples and a Github Project)

static const CGfloat ACTIVE_DISTANCE = 100.0f

- (void)setCellAttributes:(UICollectionViewLayoutAttributes *)attributes forVisibleRect:(CGRect)visibleRect withCentralCellIndexPath:(NSIndexPath *)path
{

    // First set the general position for the attributes
    [self setGeneralPositionForAttributes:(HILayoutAttributes *)attributes withCentralAttributesIndexPath:path];

    // Then adjust transforms
    CGFloat distanceToCenterX = CGRectGetMidX(visibleRect) - attributes.center.x;

    //
    // The issue manifests when distanceToCenterX is 0
    //
    CGFloat normalizedDistance = distanceToCenterX / ACTIVE_DISTANCE;

    BOOL isLeft = (((HILayoutAttributes *)attributes).generalPosition == kGeneralCellPosition_left) ? YES : NO;
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1 / (4.6777 * self.itemSize.width);

    if (ABS(distanceToCenterX) < ACTIVE_DISTANCE) {
        if (ABS(distanceToCenterX) < TRANSLATE_DISTANCE) {
            transform = CATransform3DTranslate(CATransform3DIdentity,
                                              ((FLOW_OFFSET * ABS(distanceToCenterX)) / TRANSLATE_DISTANCE),
                                              0,
                                              (1 - ABS(normalizedDistance)) * 40000 + (isLeft ? 200 : 0));
        }

        transform.m34 = -1/(4.6777 * self.itemSize.width);
        transform = CATransform3DRotate(transform,  1 * ABS(normalizedDistance) * 45 * M_PI / 180, 0, 1, 0);

    } else {
        transform = CATransform3DTranslate(transform, FLOW_OFFSET , 0, 0);
        transform = CATransform3DRotate(transform, 1 * 45 * M_PI / 180, 0, 1, 0);
        attributes.zIndex = 0;
    }
    attributes.transform3D = transform;
}

what could I do to avoid this from happening?


Solution

  • I discussed about the same issue happening on "iOS 6 Developer's Cookbook" (Chapter 11, Recipe 5) by Erica Sadun.

    In the book there's also an example of a custom UICollectionViewFlowLayout that exhibits the same issue that I was experiencing.

    I talked to Erica about it on IRC, and we agreed that there seems to be a bug in UICollectionViewLayout, she mentioned that she saw a similar thing in radar some time ago, so it could already be reported.

    She made a more or less ugly workaround for her open source code in her book, actually you can check out her solution here. It's not elegant, but it a completely valid workaround.

    She basically emits a notification on targetContentOffsetForProposedContentOffset:withScrollingVelocity: when the proposedContentOffset.x is equal to 0, or when the collectionViewContentSize.width - bounds.Size.width (boundsSize = self.collectionView.bounds.size)

    CGPoint desiredPoint = CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
    
    if ((proposedContentOffset.x == 0) || (proposedContentOffset.x >= (self.collectionViewContentSize.width - boundsSize.width)))
    {
        NSNotification *note = [NSNotification notificationWithName:@"PleaseRecenter" object:[NSValue valueWithCGPoint:desiredPoint]];
        [[NSNotificationCenter defaultCenter] postNotification:note];
        return proposedContentOffset;
    }
    

    The controller listens to that notification and basically re-centers the CollectionView when the scrollView does end decelerating to the desiredPoint.

    - (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView
    {
        if (CGPointEqualToPoint(desiredPoint, CGRectNull.origin))
            return;
    
        CGRect rect = scrollView.bounds;
        rect.origin = desiredPoint;
        [self.collectionView scrollRectToVisible:rect animated:YES];
    
        desiredPoint = CGRectNull.origin;
    }
    

    Hope this helps someone else.