Custom Collection View Layouts(二)

本节我们来实现一个下拉效果,具体效果如下:

当鼠标下拉时,注意观察 DEVCON 的图标,以及黑色纹理背景图的变化:DEVCON 图标是呈放大效果,而背景黑色纹理图则是被缩放了,相对呈现出一种景深变化的效果。要做出这种效果,我们还是一步步来。

一、Stretchy Headers

首先我们先来实现一个有弹性的 header,同样的是先创建一个自定义的 UICollectionViewFlowLayout,为什么是 UICollectionViewFlowLayout 而不是通用的 UICollectionViewLayout 呢?因为只有 UICollectionViewFlowLayout 和他的子类才支持 headers 和 footers。

具体定义的 DIYLayout 如下:

class DIYLayout: UICollectionViewFlowLayout {

  override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
    let layoutAttributes = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]
    let offset = collectionView!.contentOffset
    // 向下拉,offset.y 为负值,向上为正值
    if (offset.y < 0) {
      let deltaY = fabs(offset.y)
      // 遍历找出 header 对应的 attributes,然后设置他的 frame
      for attributes in layoutAttributes {
        if let elementKind = attributes.representedElementKind {
          if elementKind == UICollectionElementKindSectionHeader {
            var frame = attributes.frame
            frame.size.height = max(0, headerReferenceSize.height + deltaY)
            frame.origin.y = CGRectGetMinY(frame) - deltaY
            attributes.frame = frame
          }
        }
      }
    }
    return layoutAttributes
  }
  // 告诉 layout 对象,当 collection View 的 bounds 发生改变时,需要更新 layout
  override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    return true
  }
}

上面两个方法都很简单,第一个方法是用 deltaY 来记录下拉偏移量,然后对 header 的 frame 做出补偿,使其看上去随着下拉动作整个 height 被拉长了。另一个方法用来更新 layout

第一个方法中我们用到了 UICollectionViewLayoutAttributes 的 representedElementKind 属性,该属性用来标识特定的 supplementary 或 decoration view 相关的 attributes

这里首先应当查看的是 UICollectionViewLayoutAttributes 的 representedElementCategory 属性,该属性是一个 enum,定义如下:

enum UICollectionElementCategory : UInt {  
    case Cell
    case SupplementaryView
    case DecorationView
}

representedElementCategory 返回的是 .Cell 时,representedElementKind 为 nil

接下来我们在 collectionViewController 中的 viewDidLoad 中设置 header 的初始 height

layout.headerReferenceSize = CGSize(width: width, height: 180)

最后别忘了修改 storyboard 中 layout 为自定义的 DIYLayout

本小节最终 Demo 代码在这里

二、Adding Depth

现在我们来增加一些景深,也就是下拉时,BackgroundImageView 会收缩,而 ForegroundImageView 会放大,对比看起来有一种景深增加的效果

1、首先我们还是先增加一个自定义的 UICollectionViewLayoutAttributes 子类,用来将 deltaY 传递给相关 View

class DIYLayoutAttributes: UICollectionViewLayoutAttributes {

  var deltaY: CGFloat = 0

  override func copyWithZone(zone: NSZone) -> AnyObject {
    let copy = super.copyWithZone(zone) as! DIYLayoutAttributes
    copy.deltaY = deltaY
    return copy
  }

  override func isEqual(object: AnyObject?) -> Bool {
    if let attributes = object as? DIYLayoutAttributes {
      if attributes.deltaY == deltaY {
        return super.isEqual(object)
      }
    }
    return false
  }

}

紧接着更新 DIYLayout 类,让其使用我们新创建的 DIYLayoutAttributes,在 DIYLayout 类中增加以下方法

override class func layoutAttributesClass() -> AnyClass {  
    return DIYLayoutAttributes.self
  }

最后在 - layoutAttributesForElementsInRect: 方法中将通用的 UICollectionViewLayoutAttributes 替换为子类 DIYLayoutAttributes,并且将 deltaY 传递给 attributes

attributes.deltaY = deltaY  

2、下面我们在 storyboard 中增加一些约束,并做一些调整,为 header 增加以下约束:

其中 Header.width 和 Header.height 的约束关系应为:

修改 View ModelScale To Fill

同理对 foregroundImageView 进行设置

3、在 ScheduleHeaderView 中将 BackgroundImageView Height ConstraintForegroundImageView Height Constraint 都与 deltaY 相关联。当 layout 发生变化时 applyLayoutAttributes: 方法会被不断调用,因此我们在该方法中设置 deltaYConstraint 的联动

class ScheduleHeaderView: UICollectionReusableView {

  @IBOutlet private weak var backgroundImageView: UIView!
  @IBOutlet private weak var backgroundImageViewHeightLayoutConstraint: NSLayoutConstraint!

  @IBOutlet private weak var foregroundImageView: UIView!
  @IBOutlet private weak var foregroundImageViewHeightLayoutConstraint: NSLayoutConstraint!

  private var backgroundImageViewHeight: CGFloat = 0
  private var foregroundImageViewHeight: CGFloat = 0

  override func awakeFromNib() {
    super.awakeFromNib()
    backgroundImageViewHeight = CGRectGetHeight(backgroundImageView.bounds)
    foregroundImageViewHeight = CGRectGetHeight(foregroundImageView.bounds)
  }

  override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
    super.applyLayoutAttributes(layoutAttributes)
    let attributes = layoutAttributes as! DIYLayoutAttributes
    backgroundImageViewHeightLayoutConstraint.constant = backgroundImageViewHeight - attributes.deltaY
    foregroundImageViewHeightLayoutConstraint.constant = foregroundImageViewHeight + attributes.deltaY
  }

}

本小节最终 Demo 代码在这里

三、Limiting Stretch

现在景深效果看起来已经 Ok 了,但还有一个问题,就是下拉的时候,backgroundViewImage 会不断的缩放,直到不能再被缩小,这个时候再下拉的话,会使整个 Header Rect 的 origin 离开原点,与此同时,foregroundImageView 也会无限度的放大,超出屏幕尺寸。这显然不是我们想要的,注意下拉到最底下时,Header 最上面露出的一条白边

为了解决这个问题,我们在 DIYLayout 中增加一个 maximumStretchHeight 来对整个 header 的 height 做限制(即最大不能超过 maximumStretchHeight)

var maximumStretchHeight: CGFloat = 0

class DIYLayout: UICollectionViewFlowLayout {  
    ...
    if let elementKind = attributes.representedElementKind {
        if elementKind == UICollectionElementKindSectionHeader {
            var frame = attributes.frame
            // 修改 header 的 height,使其最大不能超过 maximumStretchHeight
            frame.size.height = min(max(minY, headerReferenceSize.height + deltaY), maximumStretchHeight)
    ...
}

...

接着在 UICollectionViewController 对象中的 viewDidLoad 方法中对 maximumStretchHeight 进行赋值,让其等于 width

layout.maximumStretchHeight = width  

现在下拉时 backgroundViewImage 不会露出丑丑的白边了,下面来修正 foregroundImageView 无限放大的问题:

在 ScheduleHeaderView 中添加一个属性 previousHeight 用来记录上一次的 attributes height:

private var previousHeight: CGFloat = 0  

我们下面在 applyLayoutAttributes: 中做一些判断:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {  
    super.applyLayoutAttributes(layoutAttributes)
    let attributes = layoutAttributes as! DIYLayoutAttributes

    // 每次先获取 当前 attributes 的 height
    let height = CGRectGetHeight(attributes.frame)
    // 和上次记录的 height 作比较,只有不相等才做以下操作
    if previousHeight != height {
      backgroundImageViewHeightLayoutConstraint.constant = backgroundImageViewHeight - attributes.deltaY
      foregroundImageViewHeightLayoutConstraint.constant = foregroundImageViewHeight + attributes.deltaY
      previousHeight = height
    }
  }

当 attributes 的 height 达到 maximumStretchHeight 时,就不会再增大了,此时 previousHeight == height 即使 deltaY 继续增加也不会再对 contraint 产生任何影响了。

最后 finished demo 地址 请自行 convert to lastest swift syntax


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!