The layout effect to achieve

UICollectionView custom layout, is to implement its own UICollectionView layout,

Instead of using the system the UICollectionViewFlowLayout

The layout looks like this:

You pull it down, the top two pieces are adsorbed, the lattice is adsorbed

Pull it up, and the second piece on top snaps


The realization idea of layout effect

Realized effect:You pull it down, the top two pieces are adsorbed, and the lattice is adsorbed

The conventional wisdom is that

Control the direction of the gesture, only down, not up

Layout control is used here

UICollectionView is a subclass of UIScrollView,

The UIScrollView has a contentOffset as it rolls,

ContentOffset is a point, a CGPoint

Drop-down, give each view (two top view and grid) plus a translational transform, CGAffineTransform. Translation, it is good

And when you pull it down, it looks like it’s not moving, but it’s actually the two top views with the grid view, and the container view of the scrollView, scrolling in sync

Realized effect:Pull it up, and the second piece on top snaps

The conventional wisdom is that

The second block of the top consists of two widgets: a supplement view to the UICollectionView, and an independent control

When pulling up, the second top block of the supplementary view is pulled out of the interface, and the independent top block, which is hidden by default, is displayed

The layout control is used directly here

The second block at the top contains only one copy and serves as a SupplementaryView

As you pull up, use the contentOffset of UICollectionView to get your current position,

I’m going to add a translational radiation transform to hold the position

The second top block has to be on top of the grid view, and the zIndex of the second top block is bigger than the zIndex of the grid views, OK

The concrete realization of the layout effect

Custom layout, the system gives us three useful entry points

  • Give the layout information to the supplementary view

All supplementary view layout information

func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

  • Give the grid view layout information

Layout information for all grid views

func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

  • View layout information for the visible area of the screen

Summary of Layout Information

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

The general layout is very simple, do these three methods, done

The special thing about this layout effect is that it can be displayed in different ways as it scrolls, two of which are described above

The conventional wisdom is that

Trigger with func scrollViewDidScroll(_ scrollView: UIScrollView) listening to the contentOffset

ScrollViewDidScroll (_ scrollView: UIScrollView)

In this callback, the layout of the system is applied, and the effect is mediocre

Layout control, of course

Func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool,

Because the collectionView bounds property changes every time the user scrolls through the interface

ShouldInvalidateLayout (forBoundsChange:) is a method that should write a rule that determines when to refresh the layout.

Grid view collectionView bounds property change, shouldInvalidateLayout (forBoundsChange:) judgment through, again into the prepare () method of calculation.

override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { if oldBounds.size ! = newBounds.size { cache.removeAll(keepingCapacity: true) } return true }Copy the code

This is a scroll to refresh the layout

Func prepare()

In func prepare(), the layout information is initialized

override public func prepare() { guard let collectionView = collectionView, collectionView.numberOfItems(inSection: 0) > 0 else {return} Github prepareCache() contentHeight = 0 zIndex = 0 oldBounds = collectionView.bounds let itemSize = CGSize(width: collectionViewWidth, height: CellHeight) / / at the top of the first piece of the layout of the information let headerAttributes = CustomLayoutAttributes (forSupplementaryViewOfKind: Element.header.kind, with: IndexPath(item: 0, section: 0) ) prepareElement(size: headerSize, type: .header, attributes: HeaderAttributes) / / at the top of the block 2 layout information let menuAttributes = CustomLayoutAttributes (forSupplementaryViewOfKind: Element.menu.kind, with: IndexPath(item: 0, section: 0)) prepareElement(size: menuSize, type: .menu, attributes: MenuAttributes) // Grid view layout for item in 0.. < collectionView.numberOfItems(inSection: 0) { let cellIndexPath = IndexPath(item: item, section: 0) let attributes = CustomLayoutAttributes(forCellWith: cellIndexPath) let lineInterSpace = settings.minimumLineSpacing attributes.frame = CGRect( x: settings.minimumInteritemSpacing, y: contentHeight + lineInterSpace, width: itemSize.width, height: itemSize.height ) attributes.zIndex = zIndex contentHeight = attributes.frame.maxY cache[.cell]?[cellIndexPath] = Attributes += 1} // Make sure that the layout of the top 2 block zIndex, > lattice view cache[.menu]?.first?.value. ZIndex = zIndex}Copy the code

The layout information is aggregated and dynamically adjusted according to the offset contentOffset

override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let collectionView = collectionView else { return nil } visibleLayoutAttributes.removeAll(keepingCapacity: true) for (type, elementInfos) in cache { for (indexPath, Attributes) in elementInfos {attributes. Transform =.identity UpdateSupplementaryViews (Type, attributes: Attributes, collectionView: collectionView, indexPath: IndexPath) // Only care about layout information that is displayed on screen // Layout information that is not on screen No need to worry if the attributes. Frame. Intersects the rect () {if type = =. The cell {/ / dynamic adjustment, the layout of the grid view information updateCells (attributes, collectionView: collectionView, indexPath: indexPath) } visibleLayoutAttributes.append(attributes) } } } return visibleLayoutAttributes }Copy the code

Adjust the layout information of the top two supplementaryViews dynamically

The corresponding translation affine transform is set according to the offset contentoffset. y

The first block on the top, we added an alpha control

  private func updateSupplementaryViews(_ type: Element, attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
    switch type {
    case .header:
        attributes.transform = CGAffineTransform(translationX: 0, y: contentOffset.y)
        attributes.headerOverlayAlpha = min(settings.headerOverlayMaxAlphaValue, contentOffset.y / headerSize.height)
    case .menu:
      print(contentOffset.y)
      if contentOffset.y < 0{
        attributes.transform = CGAffineTransform(translationX: 0, y: attributes.initialOrigin.y - headerSize.height + contentOffset.y)
      }
      else{
        attributes.transform = CGAffineTransform(translationX: 0, y: max(attributes.initialOrigin.y, contentOffset.y) - headerSize.height)
      }
        
    default:
      break
    }
  }
Copy the code

Dynamic adjustment of grid view layout information

The corresponding translation affine transform is set according to the offset contentoffset. y

 private func updateCells(_ attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
     if contentOffset.y < 0{
       attributes.transform = CGAffineTransform(translationX: 0, y: attributes.initialOrigin.y + contentOffset.y)
     }

 }
Copy the code

And finally,

System, UICollectionViewFlowLayout, standard of shelf,

Each section can have only one valid supplementary view header and footer

(Because the size of header and footer is divided by section)

So the layout here, one section with two headers

Actually, write your own layout UICollectionViewFlow,

Not only the position of each supplementary view and grid view, random placement

The number of supplementary views can also be allocated arbitrarily,

The maximum number of supplementary views = Kind * IndexPath

How many Kind, free to set

In this layout, this is how it’s handled

So let’s initialize,


override public func prepare() {
    // ...
	let headerAttributes = CustomLayoutAttributes(
      forSupplementaryViewOfKind: Element.header.kind,
      with: IndexPath(item: 0, section: 0)
    )
	// ...
    let menuAttributes = CustomLayoutAttributes(
      forSupplementaryViewOfKind: Element.menu.kind,
      with: IndexPath(item: 0, section: 0))
    prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
    // ...
}
Copy the code

To register again,

public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { switch elementKind { case Element.header.kind: return cache[.header]? [indexPath] case CustomLayout.Element.menu.kind: return cache[.menu]? [indexPath] default: return nil } }Copy the code

Custom layout, set clear, more flexible

Making a link