I have a custom flow layout which is adjusting the attributes for cells when they are being inserted and deleted from the CollectionView with the following two functions, bu
To solve problem without hack that was proposed in the answer by gavrix
you could subclass UICollectionViewLayoutAttributes with new property CABasicAnimation *transformAnimation
, than create custom transformation with a suitable duration and assign it to attributes in initialLayoutAttributesForAppearingItemAtIndexPath
, then in UICollectionViewCell apply the attributes as needed:
@interface AnimationCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes
@property (nonatomic, strong) CABasicAnimation *transformAnimation;
@end
@implementation AnimationCollectionViewLayoutAttributes
- (id)copyWithZone:(NSZone *)zone
{
AnimationCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.transformAnimation = _transformAnimation;
return attributes;
}
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![[other class] isEqual:[self class]]) {
return NO;
}
if ([(( AnimationCollectionViewLayoutAttributes *) other) transformAnimation] != [self transformAnimation]) {
return NO;
}
return YES;
}
@end
In Layout class
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
AnimationCollectionViewLayoutAttributes* attributes = (AnimationCollectionViewLayoutAttributes* )[super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
transformAnimation.duration = 1.0f;
CGFloat height = [self collectionViewContentSize].height;
transformAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, 2*height, height)];
transformAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, attributes.bounds.origin.y, 0)];
transformAnimation.removedOnCompletion = NO;
transformAnimation.fillMode = kCAFillModeForwards;
attributes.transformAnimation = transformAnimation;
return attributes;
}
+ (Class)layoutAttributesClass {
return [AnimationCollectionViewLayoutAttributes class];
}
then in UICollectionViewCell apply the attributes
- (void) applyLayoutAttributes:(AnimationCollectionViewLayoutAttributes *)layoutAttributes
{
[[self layer] addAnimation:layoutAttributes.transformAnimation forKey:@"transform"];
}
Building on @rotava's answer, you can temporarily set the animation speed by using a batch update of the collection view:
[self.collectionView performBatchUpdates:^{
[self.collectionView.viewForBaselineLayout.layer setSpeed:0.2];
[self.collectionView insertItemsAtIndexPaths: insertedIndexPaths];
} completion:^(BOOL finished) {
[self.collectionView.viewForBaselineLayout.layer setSpeed:1];
}];
UICollectionView
initiates all animations internally using some hardcoded value. However, you can always override that value until animations are committed.
In general, process looks like this:
applying attributes is done under each UICollectionViewCell and you can override animationDuration in appropriate method. The problem is that UICollectionViewCell has public method applyLayoutAttributes:
BUT it's default implementation is empty!. Basically, UICollectionViewCell has other private method called _setLayoutAttributes:
and this private method is called by UICollectionView and this private method calls applyLayoutAttributes:
at the end. Default layout attributes, like frame, position, transform are applied with current animationDuration
before applyLayoutAttributes:
is called.
That said, you have to override animationDuration
in private method _setLayoutAttributes:
- (void) _setLayoutAttributes:(PSTCollectionViewLayoutAttributes *)layoutAttributes
{
[UIView setAnimationDuration:3.0];
[super _setLayoutAttributes:layoutAttributes];
}
This is obviously, not applestore-safe. You can use one of those runtime hacks to override this private method safely.
You can change UICollectionView layout.speed property, that should change animation duration of your layout...
An update to @AshleyMills since forBaselineLayout is deprecated
This works
self.collectionView.performBatchUpdates({ () -> Void in
let indexSet = IndexSet(0...(numberOfSections - 1))
self.collectionView.insertSections(indexSet)
self.collectionView.forFirstBaselineLayout.layer.speed = 0.5
}, completion: { (finished) -> Void in
self.collectionView.forFirstBaselineLayout.layer.speed = 1.0
})
You can set the layer's speed property (like in Rotoava's Answer) to change the control the speed of the animation. The problem is you are using arbitrary values because you do not know the actual duration of the insertion animation.
Using this post you can figure out what the default animation duration is.
newAnimationDuration = (1/layer.speed)*originalAnimationDuration
layer.speed = originalAnimationDuration/newAnimationDuration
If you wanted to make the animation 400ms long, in your layout you would:
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
//set attributes here
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
CGFloat originalAnimationDuration = [CATransaction animationDuration];
CGFloat newAnimationDuration = 0.4f;
cell.layer.speed = originalAnimationDuration/newAnimationDuration;
return attributes;
}
In my case I had cells which could be dragged off screen and I wanted to change the duration of the deletion animation based on the speed of the pan gesture.
In the gesture recognizer (which should be part of your collection view):
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender
{
CGPoint dragVelocityVector = [sender velocityInView:self.collectionView];
CGFloat dragVelocity = sqrt(dragVelocityVector.x*dragVelocityVector.x + dragVelocityVector.y*dragVelocityVector.y);
switch (sender.state) {
...
case UIGestureRecognizerStateChanged:{
CustomLayoutClass *layout = (CustomLayoutClass *)self.collectionViewLayout;
layout.dragSpeed = fabs(dragVelocity);
...
}
...
}
Then in your customLayout:
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
CGFloat animationDistance = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
CGFloat originalAnimationDuration = [CATransaction animationDuration];
CGFloat newAnimationDuration = animationDistance/self.dragSpeed;
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
cell.layer.speed = originalAnimationDuration/newAnimationDuration;
return attributes;
}