Animate a UICollectionView cell on selection

15,323

Solution 1

After trying several (and rather hacky) solutions, I solved this by writing my own UICollectionView layout. Previous solutions I attempted:

  • Add a custom UIView overtop the UICollectionView and attempt to fake the original cell moving by overlaying it, dimming the background, and animating the new UIView
  • Animate the cell without creating a brand new layout

I was trying to avoid it, but after I got into coding it, it was a lot less painful than I originally thought.

Here is my code, for anyone interested:

.h:

@interface FMStackedLayout : UICollectionViewLayout

@property(nonatomic,assign) CGPoint     center;
@property(nonatomic,assign) NSInteger   cellCount;

-(id)initWithSelectedCellIndexPath:(NSIndexPath *)indexPath;

@end

.m:

#define ITEM_WIDTH  128.0f
#define ITEM_HEIGHT 180.0f

static NSUInteger       const RotationCount         = 32;
static NSUInteger       const RotationStride        = 3;
static NSUInteger       const PhotoCellBaseZIndex   = 100;

@interface FMStackedLayout ()

@property(strong,nonatomic) NSArray     *rotations;
@property(strong,nonatomic) NSIndexPath *selectedIndexPath;

@end

@implementation FMStackedLayout

#pragma mark - Lifecycle
-(id)initWithSelectedCellIndexPath:(NSIndexPath *)indexPath{
    self = [super init];
    if (self) {
        self.selectedIndexPath = indexPath;
        [self setup];
    }
    return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self){
        [self setup];
    }
    return self;
}

-(void)setup{
    NSMutableArray *rotations = [NSMutableArray arrayWithCapacity:RotationCount];
    CGFloat percentage = 0.0f;

    for (NSUInteger i = 0; i < RotationCount; i++) {
        // Ensure that each angle is different enough to be seen
        CGFloat newPercentage = 0.0f;
        do {
            newPercentage = ((CGFloat)(arc4random() % 220) - 110) * 0.0001f;
        } while (fabsf(percentage - newPercentage) < 0.006);

        percentage = newPercentage;

        CGFloat angle = 2 * M_PI * (1.0f + percentage);
        CATransform3D transform = CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
        [rotations addObject:[NSValue valueWithCATransform3D:transform]];
    }

    self.rotations = rotations;
}

#pragma mark - Layout
-(void)prepareLayout{
    [super prepareLayout];

    CGSize size = self.collectionView.frame.size;
    self.cellCount = [self.collectionView numberOfItemsInSection:0];
    self.center = CGPointMake(size.width / 2.0, size.height / 2.0);
}

-(CGSize)collectionViewContentSize{
    return self.collectionView.frame.size;
}

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attributes.size = CGSizeMake(ITEM_WIDTH, ITEM_HEIGHT);
    attributes.center = self.center;
    if (indexPath.item == self.selectedIndexPath.item) {
        attributes.zIndex = 100;
    }else{
        attributes.transform3D = [self transformForPersonViewAtIndex:indexPath];
    }

    return attributes;
}

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSMutableArray *attributes = [NSMutableArray array];
    for (NSInteger i = 0; i < self.cellCount; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i
                                                     inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }
    return attributes;
}

#pragma mark - Private
-(CATransform3D)transformForPersonViewAtIndex:(NSIndexPath *)indexPath{
    NSInteger offset = (indexPath.section * RotationStride + indexPath.item);
    return [self.rotations[offset % RotationCount] CATransform3DValue];
}

@end

Then, on your UICollectionView, you call

MyLayout *stackedLayout = [[MyLayout alloc] initWithSelectedCellIndexPath:indexPath];
[stackedLayout invalidateLayout];
[self.collectionView setCollectionViewLayout:stackedLayout
                                    animated:YES];

Animations between cells will be handled by the custom layout.

Solution 2

Seems to be a multi-part question, so I'll answer part of it.

  1. UICollectionviewCells do not automatically adjust on highlight or selection. It will only tell you when the cell is highlighted or selected, with one exception:

For the most part, the collection view modifies only the properties of a cell to indicate that it is selected or highlighted; it does not change the visual appearance of your cells, with one exception. If a cell’s selectedBackgroundView property contains a valid view, the collection view shows that view when the cell is highlighted or selected.

Otherwise, you must do the visual highlighting manually. Usually by adjusting either the .alpha property of the entire cell, or swapping out the .image property of a background UIImageView in the cell itself using one or more of the following methods, which are accessible in the <UICollectionViewDelegate> Protocol:

collectionView:didDeselectItemAtIndexPath:
collectionView:didHighlightItemAtIndexPath:
collectionView:didSelectItemAtIndexPath:
collectionView:didUnhighlightItemAtIndexPath:
collectionView:shouldDeselectItemAtIndexPath:
collectionView:shouldHighlightItemAtIndexPath:
collectionView:shouldSelectItemAtIndexPath:

As for the rest of your question,I wish I could be of more help, but I'm not as fluent at the custom view animation.

Share:
15,323

Related videos on Youtube

Mark Struzinski
Author by

Mark Struzinski

Application developer from Bel Air, MD. I work with .NET and the Microsoft stack for my day job. I'm learning Objective-C and Cocoa as I design my first iPhone app.

Updated on June 04, 2022

Comments

  • Mark Struzinski
    Mark Struzinski about 2 years

    I have a basic grid in a UICollectionView. It's a simple 2 column, multiple row layout using the UICollectionViewDelegateFlowLayout. When a cell is selected, I want to dim the background, float the cell to the center of the screen and then have a workflow based on the selected cell. I'm fairly new to UICollectionViews, and I'm not sure of the best way to go about this.

    Should I have a custom layout of the UICollectionView for when a cell is selected?

    Or is there a way I can animate the selected cell without having to create a new layout

    If anyone can just get me going on the right direction, I think I'll be good to research how to execute it.

  • Mark Struzinski
    Mark Struzinski over 11 years
    Hi. I have already attempted to animate from the collectionView:didSelectItemAtIndexPath:. Per the question, though, I want to actually move the cell to the center of the screen to focus on it. I'm wondering now if this doesn't mean I should use a modal view controller and do something to make it appear the cell is animating.
  • Fiveagle
    Fiveagle about 11 years
    This is not an answer. Looks like you have the answer but are not interested in sharing.
  • Timuçin
    Timuçin about 11 years
    @Mark Struzinski can you please share your solution?
  • Mark Struzinski
    Mark Struzinski about 11 years
    It's a rather long bit of code, but I was not holding back for any particular reason. I've posted it above for anyone interested. Not sure why there were downvotes here. The final solution used a method that is thoroughly documented in the Apple docs: developer.apple.com/library/ios/#documentation/uikit/referen‌​ce/…. I had been trying to avoid it because it's quite a bit of work.
  • Colas
    Colas almost 10 years
    Can you have a control over the duration of the animation?