Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a stretchy UICollectionView like Evernote on iOS 7

I've been working on trying to recreate the stretchy collection view that Evernote uses in iOS 7 and I'm really close to having it working. I've managed to create a custom collection view flow layout that modifies the layout attribute transforms when the content offset y value lies outside collection view bounds. I'm modifying the layout attributes in the layoutAttributesForElementsInRect method and it behaves as expected except that the bottom cells can disappear when you hit the bottom of the scroll view. The further you pull the content offset the more cells can disappear. I think the cells basically get clipped off. It doesn't happen at the top though and I'd expect to see the same behavior in both places. Here's what my flow layout implementation looks like right now.

@implementation CNStretchyCollectionViewFlowLayout
{
    BOOL        _transformsNeedReset;
    CGFloat     _scrollResistanceDenominator;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        // Set up the flow layout parameters
        self.minimumInteritemSpacing = 10;
        self.minimumLineSpacing = 10;
        self.itemSize = CGSizeMake(320, 44);
        self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);

        // Set up ivars
        _transformsNeedReset = NO;
        _scrollResistanceDenominator = 800.0f;
    }

    return self;
}

- (void)prepareLayout
{
    [super prepareLayout];
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // Set up the default attributes using the parent implementation
    NSArray *items = [super layoutAttributesForElementsInRect:rect];

    // Compute whether we need to adjust the transforms on the cells
    CGFloat collectionViewHeight = self.collectionViewContentSize.height;
    CGFloat topOffset = 0.0f;
    CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
    CGFloat yPosition = self.collectionView.contentOffset.y;

    // Update the transforms if necessary
    if (yPosition < topOffset)
    {
        // Compute the stretch delta
        CGFloat stretchDelta = topOffset - yPosition;
        NSLog(@"Stretching Top by: %f", stretchDelta);

        // Iterate through all the visible items for the new bounds and update the transform
        for (UICollectionViewLayoutAttributes *item in items)
        {
            CGFloat distanceFromTop = item.center.y;
            CGFloat scrollResistance = distanceFromTop / 800.0f;
            item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
        }

        // Update the ivar for requiring a reset
        _transformsNeedReset = YES;
    }
    else if (yPosition > bottomOffset)
    {
        // Compute the stretch delta
        CGFloat stretchDelta = yPosition - bottomOffset;
        NSLog(@"Stretching bottom by: %f", stretchDelta);

        // Iterate through all the visible items for the new bounds and update the transform
        for (UICollectionViewLayoutAttributes *item in items)
        {
            CGFloat distanceFromBottom = collectionViewHeight - item.center.y;
            CGFloat scrollResistance = distanceFromBottom / 800.0f;
            item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
        }

        // Update the ivar for requiring a reset
        _transformsNeedReset = YES;
    }
    else if (_transformsNeedReset)
    {
        NSLog(@"Resetting transforms");
        _transformsNeedReset = NO;
        for (UICollectionViewLayoutAttributes *item in items)
            item.transform = CGAffineTransformIdentity;
    }

    return items;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    // Compute whether we need to adjust the transforms on the cells
    CGFloat collectionViewHeight = self.collectionViewContentSize.height;
    CGFloat topOffset = 0.0f;
    CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
    CGFloat yPosition = self.collectionView.contentOffset.y;

    // Handle cases where the layout needs to be rebuilt
    if (yPosition < topOffset)
        return YES;
    else if (yPosition > bottomOffset)
        return YES;
    else if (_transformsNeedReset)
        return YES;

    return NO;
}

@end

I also zipped up the project for people to try out. Any help would be greatly appreciated as I'm pretty new to creating custom collection view layouts. Here's the link to it:

https://dl.dropboxusercontent.com/u/2975688/StackOverflow/stretchy_collection_view.zip

Thanks everyone!

like image 891
cnoon Avatar asked Nov 01 '22 11:11

cnoon


1 Answers

I was able to solve the problem. I'm not sure if there's actually a bug in iOS or not, but the issue was that the cells were actually getting translated outside the content view of the collection view. Once the cell would get translated far enough, it would get clipped off. I find it interesting that this does not happen in the simulator for non-retina displays, but does with retina displays which is why I feel this may actually be a bug.

With that in mind, a workaround for now is to add padding to the top and bottom of the collection view by overriding the collectionViewContentSize method. Once you do this, if you add padding to the top, you need to adjust the layout attributes for the cells as well so they are in the proper location. The final step is to set the contentInset on the collection view itself to adjust for the padding. Leave the scroll indicator insets alone since those are fine. Here's the implementation of my final collection view controller and the custom flow layout.

CNStretchyCollectionViewController.m

@implementation CNStretchyCollectionViewController

static NSString *CellIdentifier = @"Cell";

- (void)viewDidLoad
{
    // Register the cell
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];

    // Tweak out the content insets
    CNStretchyCollectionViewFlowLayout *layout = (CNStretchyCollectionViewFlowLayout *) self.collectionViewLayout;
    self.collectionView.contentInset = layout.bufferedContentInsets;

    // Set the delegate for the collection view
    self.collectionView.delegate = self;
    self.collectionView.clipsToBounds = NO;

    // Customize the appearance of the collection view
    self.collectionView.backgroundColor = [UIColor whiteColor];
    self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleDefault;
}

#pragma mark - UICollectionViewDataSource Methods

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
    if ([indexPath row] % 2 == 0)
        cell.backgroundColor = [UIColor orangeColor];
    else
        cell.backgroundColor = [UIColor blueColor];

    return cell;
}

@end

CNStretchyCollectionViewFlowLayout.m

@interface CNStretchyCollectionViewFlowLayout ()

- (CGSize)collectionViewContentSizeWithoutOverflow;

@end

#pragma mark -

@implementation CNStretchyCollectionViewFlowLayout
{
    BOOL            _transformsNeedReset;
    CGFloat         _scrollResistanceDenominator;
    UIEdgeInsets    _contentOverflowPadding;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        // Set up the flow layout parameters
        self.minimumInteritemSpacing = 10;
        self.minimumLineSpacing = 10;
        self.itemSize = CGSizeMake(320, 44);
        self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);

        // Set up ivars
        _transformsNeedReset = NO;
        _scrollResistanceDenominator = 800.0f;
        _contentOverflowPadding = UIEdgeInsetsMake(100.0f, 0.0f, 100.0f, 0.0f);
        _bufferedContentInsets = _contentOverflowPadding;
        _bufferedContentInsets.top *= -1;
        _bufferedContentInsets.bottom *= -1;
    }

    return self;
}

- (void)prepareLayout
{
    [super prepareLayout];
}

- (CGSize)collectionViewContentSize
{
    CGSize contentSize = [super collectionViewContentSize];
    contentSize.height += _contentOverflowPadding.top + _contentOverflowPadding.bottom;
    return contentSize;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // Set up the default attributes using the parent implementation (need to adjust the rect to account for buffer spacing)
    rect = UIEdgeInsetsInsetRect(rect, _bufferedContentInsets);
    NSArray *items = [super layoutAttributesForElementsInRect:rect];

    // Shift all the items down due to the content overflow padding
    for (UICollectionViewLayoutAttributes *item in items)
    {
        CGPoint center = item.center;
        center.y += _contentOverflowPadding.top;
        item.center = center;
    }

    // Compute whether we need to adjust the transforms on the cells
    CGFloat collectionViewHeight = [self collectionViewContentSizeWithoutOverflow].height;
    CGFloat topOffset = _contentOverflowPadding.top;
    CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height + _contentOverflowPadding.top;
    CGFloat yPosition = self.collectionView.contentOffset.y;

    // Update the transforms if necessary
    if (yPosition < topOffset)
    {
        // Compute the stretch delta
        CGFloat stretchDelta = topOffset - yPosition;
        NSLog(@"Stretching Top by: %f", stretchDelta);

        // Iterate through all the visible items for the new bounds and update the transform
        for (UICollectionViewLayoutAttributes *item in items)
        {
            CGFloat distanceFromTop = item.center.y - _contentOverflowPadding.top;
            CGFloat scrollResistance = distanceFromTop / _scrollResistanceDenominator;
            item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
        }

        // Update the ivar for requiring a reset
        _transformsNeedReset = YES;
    }
    else if (yPosition > bottomOffset)
    {
        // Compute the stretch delta
        CGFloat stretchDelta = yPosition - bottomOffset;
        NSLog(@"Stretching bottom by: %f", stretchDelta);

        // Iterate through all the visible items for the new bounds and update the transform
        for (UICollectionViewLayoutAttributes *item in items)
        {
            CGFloat distanceFromBottom = collectionViewHeight + _contentOverflowPadding.top - item.center.y;
            CGFloat scrollResistance = distanceFromBottom / _scrollResistanceDenominator;
            item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
        }

        // Update the ivar for requiring a reset
        _transformsNeedReset = YES;
    }
    else if (_transformsNeedReset)
    {
        NSLog(@"Resetting transforms");
        _transformsNeedReset = NO;
        for (UICollectionViewLayoutAttributes *item in items)
            item.transform = CGAffineTransformIdentity;
    }

    return items;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

#pragma mark - Private Methods

- (CGSize)collectionViewContentSizeWithoutOverflow
{
    return [super collectionViewContentSize];
}

@end

CNStretchyCollectionViewFlowLayout.h

@interface CNStretchyCollectionViewFlowLayout : UICollectionViewFlowLayout

@property (assign, nonatomic) UIEdgeInsets bufferedContentInsets;

@end

I'm actually going to through this onto Github and I'll post a link to the project once it's up. Thanks again everyone!

like image 111
cnoon Avatar answered Nov 09 '22 22:11

cnoon