Stretchy Header Animation using collection view custom layout

Ashis Laha
4 min readJan 20, 2019

--

Recently we have started app skinning with new UX design. The “Product Details Page” of any e-commerce app contains the similar information and the format is exactly same:

  1. image of the product comes to the top
  2. Characteristics of the product
  3. user rating, similar recommended product, last viewed products etc. come at bottom

Let’s focus on the stretchy product image animation when the user pulls down the collection view, the image will grow with some visual effect like below:

Implementation:

Let’s discuss about each and every step:

Step A: Collection view set-up

  1. Assign custom layout CustomPDPLayout instead of default UICollectionViewFlowLayout. We will discuss more about that class in later.

2. Register Cell and Header view

collectionView.register(AttributeCell.self, forCellWithReuseIdentifier: cellId)collectionView.register(PDPHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId)

AttributeCell (cell) contains a text label and PDPHeaderView(Header view) contains an image.

3. Set cell size and Header size using UICollectionViewDelegateFlowLayout

private let padding: CGFloat = 16// cell sizefunc collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {return CGSize(width: collectionView.frame.width - 2 * padding, height: 50)}// header sizefunc collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {return CGSize(width: collectionView.frame.width, height: 250)}

4. configure minimumInteritemSpacing, minimumLineSpacing, sectionInset etc. from collectionViewLayout (You can still use UICollectionViewDelegateFlowLayout)

if let layout = collectionViewLayout as? CustomPDPLayout {layout.minimumLineSpacing = 8 // spacing between 2 celllayout.sectionInset = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) // adding inset to the section}

Rest of the collection view configuration, you can check the source code here:

Step B: Configure custom Layout of collection view:

  1. There are couple of layoutAttributes methods which can be overridden to get the custom effect for cell, decorated view, header view/footer view (supplementary view) etc.

Here, we are using the most general one

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

2. Extracting the layout attributes from super

let layoutAttributes = super.layoutAttributesForElements(in: rect)

3. Loop through each attribute and identify which one you want to modify. For our case, we want to modify the Section Header (for additional check you can use indexPath whether we want stretchy on 1st section header or something else).

layoutAttributes?.forEach({ (attribute) in

if attribute.representedElementKind == UICollectionView.elementKindSectionHeader { ... }
})

4. Determine the scroll direction, stretch when the user pulls down

guard let collectionView = collectionView else { return }let contentOffsetY = collectionView.contentOffset.yif contentOffsetY < 0 { ... }

5. Set the new frame of attribute based on contentOffset

let width = collectionView.frame.width// as contentOffsetY is -ve, the height will increase based on contentOffsetYlet height = attribute.frame.height - contentOffsetYattribute.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)

and return the layoutAttributes at last.

6. invalidate the layout for new bounds so that the layout will be redrawn freshly

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

That’s it for stretching the header.

Step C: Do some Visual Effect of Header while pulling down the collection

This is the final step where we will add a blur visual effect view on header and do some animation using UIViewPropertyAnimator.

1. Add an UIViewPropertyAnimator property (A class that animates changes to views and allows the dynamic modification of those animations) in Header View to animate it.

animator = UIViewPropertyAnimator(duration: 4.0, curve: .easeInOut, animations: { ... })

2. Add a blur visual effect view to Header View.

let blurEffect = UIBlurEffect(style: .regular)let visulaEffectView = UIVisualEffectView(effect: blurEffect)

3. Capture the Header view in Controller to modify the animator property.

private var headerView: PDPHeaderView?override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {...
// once header has been created, update the instance variable
headerView = header
...
}

4. Animate the header view using fractionComplete property of UIViewPropertyAnimator based on contentOffset(pull down/up) of scroll view

override func scrollViewDidScroll(_ scrollView: UIScrollView) {let contentOffsetY = scrollView.contentOffset.yif contentOffsetY < 0 { // pull downheaderView?.animator?.fractionComplete = abs(contentOffsetY) / 200} else { // pull upheaderView?.animator?.fractionComplete = 0.0 // totally visible}}

And that’s all folks :-)

Source-code:

Do the right thing, even when no one is looking. This is called “integrity”. Thank you.

If you want to see more work of mine, please check here:

--

--

Ashis Laha

Senior Software Engineer at Microsoft (ex- Walmart, Olacabs). Exploring 3-D gaming, Healthcare projects etc.