Uicollectionview: Animate Cell Size Change on Selection

UICollectionView: Animate cell size change on selection

[UIView transitionWithView:collectionView 
duration:.5
options:UIViewAnimationOptionTransitionCurlUp
animations:^{

//any animatable attribute here.
cell.frame = CGRectMake(3, 14, 100, 100);

} completion:^(BOOL finished) {

//whatever you want to do upon completion

}];

Play around with something along those lines inside your didselectItemAtIndexPath method.

Animate UICollectionView cell size change and reposition surrounding cells

Basically you need to do three steps

1.invalidate the collectionViewLayout

2.perform batch updates:

 ...
//invalidate layout
[collectionView performBatchUpdates:^{
// make your cell at indexPath return a new size
} completion:^(BOOL finished) {

}];

3.return the new size at sizeForItemAtIndexpath

How to animate a UICollectionView cell selection

The animated in UICollectionView's selectItem(at:animated:scrollPosition:) determines whether the item to-be-selected, if not in view or at required position already, should be scrolled to in an animated fashion or not.

If it's in view then this animated property doesn't really do anything, afaik.

Same for the animated in deselectItem(at:animated:). It doesn't do anything and is simply there.

The only thing I see affecting the layout engine is if the collectionView scrolls and you have animations in the didSelectItemAt then it will render these animations ineffective. You would have to delay the animations occurring in the cell (see last example in this answer)


As you already know but for others, if you want to animate the cell selection event then you will have to do it yourself in the collectionView(_:didSelectItemAt:) delegate.

Example:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)

//Briefly fade the cell on selection
UIView.animate(withDuration: 0.5,
animations: {
//Fade-out
cell?.alpha = 0.5
}) { (completed) in
UIView.animate(withDuration: 0.5,
animations: {
//Fade-out
cell?.alpha = 1
})
}

}

The above is fine if the user taps on a cell but if you programmatically call selectItem(at:animated:scrollPosition:), it won't trigger the above collectionView(_:didSelectItemAt:) delegate and you would need to explicitly call it to run your selection animation.

Example (Add-on to previous):

func doSelect(for aCollectionView: UICollectionView,
at indexPath: IndexPath) {
aCollectionView.selectItem(at: indexPath,
animated: true,
scrollPosition: .centeredVertically)

//DispatchQueue after sometime because scroll animation renders
//the animation block in `collectionView(_:didSelectItemAt:)` ineffective
DispatchQueue.main.asyncAfter(deadline: .now() + 0.27) { [weak self] in
self?.collectionView(aCollectionView,
didSelectItemAt: indexPath)
}
}

How to correctly animate change of size of a paged UICollectionView

I managed to solve this. There's a proof-of-concept GitHub repo here.

There are a few moving parts:

  • There are two pieces of UIView scaling code. I suspect these could be combined with suitable delegate references as the nested view controllers are embedded. The first piece scales (statically) correctly when the Collection View cell is created. The second is an animated scale/translate when the sidebar size is changed.

  • I added a FlowLayout subclass to remove flicker as the collection's layout is invalidated during scaling.

  • The sidebar-related transform in the top-level VC uses most of the tricks in the book - invalidateLayout(), performBatchUpdates(...), layoutIfNeeded() etc. as well as animating the contentOffset to the correct value. Some manual tracking of the correct page/slide is required.

  • There's still a slight flicker occasionally at the start of resizing. This may not be an issue with my particular colour-scheme which will be black on dark gray. Bonus points if anyone can suggest how to track this down or alleviate it.

UICollectionView animating cell size change causes undesired behavior

This is a UICollectionViewFlowLayout bug, but there is a workaround. The problem is that the attributes returned by initialLayoutAttributesForAppearingItemAtIndexPath: have the wrong frames. Specifically, they have the frames of the final, on screen position rather than the initial, off screen position. You just need to override this method and return correct frames. The basic structure of the override would look something like this:

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *pose = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
if () {
CGRect frame = pose.frame;
frame.origin.y = ;
pose.frame = frame;
}
return pose;
}

You will be responsible for identifying scenarios where the frames need to be adjusted, which may involve keeping your own internal state. I haven't worked through this logic, but I did do a crude test and was able to get smooth animation.

Generalizing a bit, it is my experience that there UICollectionViewFlowLayout is so buggy and there are so many corner cases to contend with if you've got items moving on and off screen combined with any inserts, deletions, or moves, that I've found it easier to roll my own simple layouts. If you're not going to do any inserts, deletions, or moves, then overriding UICollectionViewFlowLayout may well be your best bet.

Let me know if you need more help.

EDIT

If you're interested in looking at a 3-rd party layout, I open sourced my custom grid layout VCollectionViewGridLayout and added an example project demonstrating smooth expanding cell height. Try running the Expand project. There is also a Sort & Filter project with animated sorting and filtering. Both projects let you toggle between flow layout and grid layout so you can see the improvement.

UICollectionView animate cell image when selected

let's say you want to animate (eg: with a fading) the transition between two images, when you change the image (in your isSelected) you might use this code:

UIView.transition(with: self, duration: 0.25, options: UIViewAnimationOptions.transitionCrossDissolve, animations: {
// change the image
}

Animating particular cells in UICollectionView

I was able to make same type of animation by playing with UICollectionViewFlowLayout.

You can view the final result: https://imgur.com/a/lNoD2

Basically I used UPCarouselFlowLayout Library to achieve this animation.

I added sideItemOffsetShift to the custom flowLayout:

//  UPCarouselFlowLayout.swift
// UPCarouselFlowLayoutDemo
//
// Created by Paul Ulric on 23/06/2016.
// Copyright © 2016 Paul Ulric. All rights reserved.

public enum UPCarouselFlowLayoutSpacingMode {
case fixed(spacing: CGFloat)
case overlap(visibleOffset: CGFloat)
}

class CustomLayout: UICollectionViewFlowLayout {
fileprivate struct LayoutState {
var size: CGSize
var direction: UICollectionViewScrollDirection
func isEqual(_ otherState: LayoutState) -> Bool {
return self.size.equalTo(otherState.size) && self.direction == otherState.direction
}
}

@IBInspectable open var sideItemScale: CGFloat = 0.6
@IBInspectable open var sideItemAlpha: CGFloat = 0.0
@IBInspectable open var sideItemShift: CGFloat = 0.0
@IBInspectable open var sideItemOffsetShift: CGFloat = 50.0
open var spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 10)

fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)


override open func prepare() {
super.prepare()

self.scrollDirection = .horizontal
let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)

if !self.state.isEqual(currentState) {
self.setupCollectionView()
self.updateLayout()
self.state = currentState
}
}

fileprivate func setupCollectionView() {
guard let collectionView = self.collectionView else { return }
if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
}

fileprivate func updateLayout() {
guard let collectionView = self.collectionView else { return }

let collectionSize = collectionView.bounds.size
let isHorizontal = (self.scrollDirection == .horizontal)

let yInset = (collectionSize.height - self.itemSize.height) / 2
let xInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset)

let side = isHorizontal ? self.itemSize.width : self.itemSize.height
let scaledItemOffset = (side - side*self.sideItemScale) / 2
switch self.spacingMode {
case .fixed(let spacing):
self.minimumLineSpacing = spacing - scaledItemOffset
case .overlap(let visibleOffset):
let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
let inset = isHorizontal ? xInset : yInset
self.minimumLineSpacing = inset - fullSizeSideItemOverlap
}
}

override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}

override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map({ self.transformLayoutAttributes($0) })
}

fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let isHorizontal = (self.scrollDirection == .horizontal)

let collectionCenter = isHorizontal ? collectionView.frame.size.width/2 : collectionView.frame.size.height/2
let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y) - offset

let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance

let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
let shift = (1 - ratio) * self.sideItemShift
let offsetShift = (1 - ratio) * self.sideItemOffsetShift
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)

if isHorizontal {
attributes.center.y = attributes.center.y + shift

if normalizedCenter < collectionCenter {
attributes.center.x = attributes.center.x - offsetShift
} else {
attributes.center.x = attributes.center.x + offsetShift
}
} else {
attributes.center.x = attributes.center.x + shift
}

return attributes
}

override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView , !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }

let isHorizontal = (self.scrollDirection == .horizontal)

let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide

var targetContentOffset: CGPoint
if isHorizontal {
let closest = layoutAttributes.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
}
else {
let closest = layoutAttributes.sorted { abs($0.center.y - proposedContentOffsetCenterOrigin) < abs($1.center.y - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
}

return targetContentOffset
}
}

And in my viewController I have:

override func viewDidLoad() {
super.viewDidLoad()

// scroll to index 1
let indexPath = IndexPath(item: 1, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)

DispatchQueue.main.async {
self.updateCollectionView()
}
}

func updateCollectionView() {
if let layout = collectionView?.collectionViewLayout as? CustomLayout {

UIView.animate(withDuration: 0.5, animations: {
layout.sideItemAlpha = 0.6
layout.sideItemOffsetShift = 0
self.collectionView?.collectionViewLayout.invalidateLayout()
})
}
}

So by default when the collection is loaded, side items are hidden. Then we call updateCollectionView() to update the sideItemOffsetShift and sideItemAlpha.



Related Topics



Leave a reply



Submit