Custom Collection View Layouts(五)

这是 Ray 视频关于 Custom Collection View Layouts 的最后一部分,终于要写完了。本节我们要实现一个 Timbre 应用,照例先来看一下成品效果:

还是一步步来:

一、Cell Transform

首先我们来解决 cell 的旋转角度问题,本小节目标我们要实现一个下面的效果:

其实 layoutAttributes 已经为我们提供了 transform 属性,我们可以使用这个属性来实现旋转:

还是来看下具体实现:

CGAffineTransformMakeRotation: 需要以一个角弧度做参数,我们因此先来写一个 handle function 将角度转换成角弧度

func degreesToRadians(degrees: Double) -> CGFloat {  
    return CGFloat(M_PI * (degrees/180.0))
}

接着我们就可以在 TimbreLayout 中添加如下方法:

class TimbreLayout: UICollectionViewFlowLayout {  
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        let layoutAttributes = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]

        for attribute in layoutAttributes {
            attribute.transform = CGAffineTransformMakeRotation(degreesToRadians(-14))
        }

        return layoutAttributes
    }
}

方法也非常简单,遍历每个 attribute,然后逆时针旋转 14 度,运行效果如下:

我们发现 cell 的左上角和右下角都被截断了,现在来修正一下,思路嘛就是将 cell 高度不变,宽度变窄一些,这样旋转的时候位置就刚好合适

修改如下:

for attribute in layoutAttributes {  
            let frame = CGRectInset(attribute.frame, 12, 0)
            attribute.frame = frame
            attribute.transform = CGAffineTransformMakeRotation(degreesToRadians(-14))
        }

同理要解决 first cell 和 last cell 旋转时超出 top 和 bottom 也非常容易,在 viewDidLoad 里添加:

collectionView!.contentInset = UIEdgeInsets(top: 50, left: 0, bottom: 50, right: 0)  

接下来将色块替换成真实的 photo,和第四节提到方法一样,为 cell 添加 imageView,设置 layout,修改 Mode 为 Aspect Fill,设置完毕后运行,我们会发现 image 也随着 cell 旋转了,毕竟 imageView 是 cell 的 subview。如果我们不想让 photo 旋转,解决办法也很简单,让 photo 再顺时针旋转 14 度就 OK,在 cell 中添加 applyLayoutAttributes: 方法:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {  
    super.applyLayoutAttributes(layoutAttributes)

    imageView.transform = CGAffineTransformMakeRotation(degreesToRadians(14.0))
}

本小节最终 Demo 代码在这里

二、View Clipping

现在运行一下 Demo,会发现所有的 cell 都缺了一角

真实情况是 cell 逆时针旋转了 14 度,而 imageView 相对 cell 又顺时针旋转了 14 度(相当于 imageView 保持原位)

用 view debug 看一下

再来看一眼 Ray 给出的图,同样是红圈的部分没有内容,因此是透明的:

解决办法也相当简单,在 cell 中添加一个 Container View ,该 View 的宽度比 cell 要宽一些,然后把 imageView 放进去,这样就能就能多显示一些,不会有空白出现

我们来具体实现一下,拖一个 view 到 cell 中,设置好约束:

接着把 imageView 的约束 clear 掉,然后拖进刚创建的 Container View 中,重新按原约束设置,最后勾选 Container View 的 Clip Subviews 完毕后再次运行,Bingo,一切正常了。

现在还有两个个问题:

  • cell 这样显示很丑,我们不想显示 cell 四个角,想让 imageView 充满整个边界
  • 有些 imageView 颜色比较浅,上面的文字看不清楚

解决这两个问题也很简单:

  • 把 cell 的 Clip Subviews 选项关闭,你可以在 StoryBoard 中取消勾选,也可以在 awakeFromNib() 中设置 clipsToBounds = false
  • 在 imageView 上再加一层蒙版(UIView)这里我们使用了 Ray 提供的现成的 GradientView 类来做蒙版,该类提供了 @IBDesignable@IBInspectable 可以很方便地设置渐变

    加了蒙版的 cell:

本小节最终效果如下:

本小节最终 Demo 代码在这里

三、Parallax Effect

最后一个部分,我们来添加一个随列表滚动的一个视觉透视(视觉差)效果,原理也很简单,就是 cell 向一个方向滑动时,相应的 imageView 向相反的方向去运动,我们可以通过操控 autolayout constraint 来实现 imageView 向 scrollView 相反的方向去滑动,这里唯一要计算的就是滑动距离

来看下实现:

首先为 cell 增加一个新属性,并和 SB 中的 Center Y Alignment - ContainerView - Image View 连线:

@IBOutlet private weak var imageViewCenterYLayoutConstraint: NSLayoutConstraint!

继续添加代码:

var parallaxOffset: CGFloat = 0 {  
    didSet {
        imageViewCenterYLayoutConstraint.constant = parallaxOffset
    }
}

func updateparallaxOffset(collectionViewBounds bounds: CGRect) {  
    let center = CGPoint(x: CGRectGetMidX(bounds), y: CGRectGetMidY(bounds))
    // 找出每个 cell 相对于 collectionView 中心点的偏移量
    let offsetFromCenter = CGPoint(x: center.x - self.center.x, y: center.y - self.center.y)
    // cell 的最大偏移量
    let maxVerticalOffset = CGRectGetHeight(bounds) / 2 + CGRectGetHeight(self.bounds) / 2
    let scaleFactor = 40 / maxVerticalOffset
    parallaxOffset = -offsetFromCenter.y * scaleFactor
}

上述代码很简单 updateparallaxOffset: 用来更新 parallaxOffset,这里需要注意的是 parallaxOffset 用 offsetFromCenter.y 和 scaleFactor 的乘积来表示,其中 scaleFactor 是固定值,而 offsetFromCenter.y 表示偏离中心点的距离。这就意味 collectionView 滚动一段距离,离中心点越远的 cell 对应的 parallaxOffset 的值就越大,离中心点越近的 cell 对应的 parallaxOffset 值就越小。这也符合我们的视觉习惯,目光平视中心点,滚动起来两边的视觉差要大一些。

注意计算 scaleFactor 的时候又用到了一个 Magic Number 40,作者的说法是试出来的,你也可以自行尝试不同的数值...... (╯‵□′)╯︵┻━┻

目前为止,还差一步,我们需要滚动 collectionView 时来运行 updateparallaxOffset: ,还好我们有 scrollViewDidScroll:

extension TutorialsViewController {  
    override func scrollViewDidScroll(scrollView: UIScrollView) {
        let cells = collectionView!.visibleCells() as! [TutorialCell]
        let bounds = collectionView!.bounds
        for cell in cells {
            cell.updateparallaxOffset(collectionViewBounds: bounds)
        }
    }
}

运行试试看,视觉差效果已经添加成功了。最后还有一个小 bug ,当 App 第一次运行滚动时,imageView 的位置会跳一下,这是因为我们还没有为其设置初始值,可以在 - collectionView:cellForItemAtIndexPath: 方法中更新 parallaxOffset

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {  
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("TutorialCell", forIndexPath: indexPath) as! TutorialCell
    cell.tutorial = tutorials[indexPath.item]

    cell.updateparallaxOffset(collectionViewBounds: collectionView.bounds)   
    return cell
  }

最后再次运行,整个 App 就完美了,最终的 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!