原文链接:Gorgeous parallax scrolling with UITableViewCells
原作者:Krishan
译者:真珠奶茶小土逗
视差滚动是一个非常强大的武器,它可以帮助我们实现非常炫酷的展示效果。如果不能够正确使用,它将会搞得我们非常恼火,但是当我们实现它的时候,它就会展示出它非凡的魔力。对我而言很幸运,我喜欢钻研一些新奇的玩意儿,因此我已经迫不及待想要实现它了😝。
今天我将展示我是如何在一个TableView的Cell中实现视差滚动的效果。我之前没有看到像我这样使用布局约束实现的方法,所以我觉得我应该拿出来和你们一起分享。如果你不想听我在这里长篇大论,你可以直接查看我在github上的示例项目。
现在我们假定你已经设置好了一个TableView并且已经有了一个包含Image的UITableViewCell的子类,我这里把它定义为ImageCell。注意:一定要将Image的显示设置为Aspect Fill,处理视差滚动这个设置至关重要!
我的没有添加视差滚动的基础工程看起来像这样:
我们希望每个cell的视差偏移依赖于cell在TableView中的位置,因此我们将在TableView中调用一个滚动监听方法来使cell的背景Image发生偏移。
这就是我们的目标:
当cell(红色矩形框)在屏幕底部的时候,Image固定在cell的顶部,当cell在屏幕顶部的时候,Image固定在cell的底部。
所以,首先让我们来看一下如何在TableView中监视一个cell的位置。将下面的代码加入到你的已经创建了TableView的ViewController中:
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView == self.tblMain) {
for indexPath in self.tblMain.indexPathsForVisibleRows() as! [NSIndexPath] {
self.setCellImageOffset(self.tblMain.cellForRowAtIndexPath(indexPath) as! ImageCell, indexPath: indexPath)
}
}
}
注意:我的UITableView使用Outlet连接,叫做tblMain
!
上面的那个方法监视Table的滑动进度,查看哪个cell是可见的,然后对可见的cell调用setCellImageOffset
方法。下面是setCellImageOffset
方法的具体实现:
func setCellImageOffset(cell: ImageCell, indexPath: NSIndexPath) {
var cellFrame = self.tblMain.rectForRowAtIndexPath(indexPath)
var cellFrameInTable = self.tblMain.convertRect(cellFrame, toView:self.tblMain.superview)
var cellOffset = cellFrameInTable.origin.y + cellFrameInTable.size.height
var tableHeight = self.tblMain.bounds.size.height + cellFrameInTable.size.height
var cellOffsetFactor = cellOffset / tableHeight
cell.setBackgroundOffset(cellOffsetFactor)
}
这个方法有一点复杂,下面我们将逐步说明:
1.我们找到了cell在TableView中的frame;
2.根据表的父视图的坐标计算cell的frame,这是为了让我们在屏幕上获取cell的位置而不是它在列表中的位置(列表中每个cell都是固定的);
3.从顶部获取cell的偏移,这本来应该只是在表格中cell的y坐标,但是我们在此基础上添加cell的高度,目的是即使cell的顶部已经超过边缘,它也可以保持视差;
4.获取表格的可见高度。同样的在Table原有高度的基础上添加cell的高度,即使cell部分移出屏幕,图像也可以保持移动;
5.我们通过将表格的可见部分中的cell的位置除以表的可见部分的总高度,计算我们要偏移背景的程度(一个比值,范围从0到1)。
哇,还是比较困难的!现在我们需要在ImageCell中实现setBackgroundOffset
方法来更新图像,现在的ImageCell看起来像这样:
class ImageCell: UITableViewCell {
@IBOutlet weak var imgBack: UIImageView!
//...(other stuff like title)...
首先是连接图像顶部和底部的布局约束。如果你使用了顶部+高度组合来添加约束(或底部+高度),则需要稍微调整以下步骤。(译者注:下面的操作步骤是都以顶部+底部的约束来实现的)
接下来找到图像的顶部和底部的约束,并将它们连接到类中的Outlets,现在的代码应该类似这样:
class ImageCell: UITableViewCell {
@IBOutlet weak var imgBack: UIImageView!
@IBOutlet weak var imgBackTopConstraint: NSLayoutConstraint!
@IBOutlet weak var imgBackBottomConstraint: NSLayoutConstraint!
let imageParallaxFactor: CGFloat = 20
var imgBackTopInitial: CGFloat!
var imgBackBottomInitial: CGFloat!
...
}
你们敏锐的眼睛👀 可能已经注意到我在上面添加了一些额外的变量。 imageParallaxFactor
是一个控制变量,我们将使用它来定义我们想要的视差多少,后面我们再说这个有趣的变量😋。imgBack *
初始变量用于跟踪约束的起始值。现在我们来设定一下。
我们可以在UITableViewCell的awakeFromNib
方法中对cell进行初始化,像下面这样(感谢/u/lyinsteve指出这一点):
override func awakeFromNib() {
self.clipsToBounds = true
self.imgBackBottomConstraint.constant -= 2 * imageParallaxFactor
self.imgBackTopInitial = self.imgBackTopConstraint.constant
self.imgBackBottomInitial = self.imgBackBottomConstraint.constant
}
让我们捋一下这个代码块:
1.允许cell裁剪它的边界,这符合我这里的实际情况,因为我的图像占用了整个cell的背景,所以如果你的单元格更小,你将需要不同的裁剪;
2.我们将底部约束设置为原始值减去2 *视差量。这具有延长图像的效果;
3.最后记录约束的初始值。
现在来实现setBackgroundOffset
方法:
func setBackgroundOffset(offset:CGFloat) {
var boundOffset = max(0, min(1, offset))
var pixelOffset = (1-boundOffset)*2*imageParallaxFactor
self.imgBackTopConstraint.constant = self.imgBackTopInitial - pixelOffset
self.imgBackBottomConstraint.constant = self.imgBackBottomInitial + pixelOffset
}
这是滚动视差的核心部分。在这里,我们移动图像的顶部和底部约束,使其与偏移上下移动,移动的核心原则是:
- 在cell的偏移量/Table的高度=0时(单元格即将从屏幕顶部滚动):
top = 2 * imageParallaxSize
bottom = 0 - 在cell的偏移量/Table的高度=1时(单元格即将从屏幕底部消失):
top = 0
bottom = 2 * imageParallaxSize
(译者注:这个核心原则可能比较难以理解,我也看了很久,在这里说一下我对这个原则的简单看法来帮助大家更好地理解作者的思路:作者实现视差滚动的原理是通过监测cell的滚动拿到一个滚动后的位置和原位置的差值,将这个差值除以Table的高度得到一个比值,将这个比值通过上述算法,即setBackgroundOffset(offset:CGFloat)
得到一个约束的偏差值,我们在滚动cell时改变Image的上下约束即可实现视差滚动的效果,下面我逐行分析一下这个核心方法的代码:
1.偏差比值有可能大于1,而之前我们设置的比值是0到1的,所以需要处理一下传入的偏差比值,将其控制在0到1之间;
2.根据上一步算出的偏差比值计算Image约束的偏差值,imageParallaxFactor
是一个控制偏差大小的变量,至于为什么要用1-boundOffset,这个说实话我也不是特别清楚,在我看来直接使用boundOffset反而才是正确的,但是通过测试发现两种方式都可以实现我们想要的效果,目前这块儿比较疑惑,如果有读者可以理解的话欢迎反馈给我,大家共同进步😉;
3.减小上边界约束;
4.增加下边界约束。)
现在运行这个项目,你会发现并不是我们想要的效果😓,它本应该是很酷炫的。剩下的唯一一个问题就是:视图的首次加载和您在屏幕上的首次滑动之间有一个突然的移动。这是因为背景图像的初始位置仍然是我们在storyboard中指定的(以及我们上面的细微调整)。这些单元格在它们放置在桌面视图中时已经被偏移了,但是由于没有滚动,背景还没有偏移,所以我们来解决一下。
下面的解决方法是很棒的。我们需要调用一个很方便的方法:tableView: willDisplayCell: forRowAtIndexPath
。这个方法在第一次显示cell的时候被调用来完成视图的初始化。所以在这里,我们将根据cell放置的位置来偏移cell的背景View:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
var imageCell = cell as! ImageCell
self.setCellImageOffset(imageCell, indexPath: indexPath)
}
(如果您在gif效果图中看不到视差,请关注标题和相对于背景的位置😉)
注意:我将视差设置为了一个较高的值(70),目的是为了让视差效果在gif图中更好地展示,我强烈不建议在实际项目中这么做,视差的值设置为0.1*cell的高度是比较合适的👍。
很棒,我们已经完成了!如果您觉得看这些长篇大论太过麻烦,您可以看一下下面这两个主要的类,它们比较简短,您可以从中快速找到或获取您想要的东西😛:
感谢阅读!像往常一样,欢迎留下您的评论和想法!
(译者注:本文翻译已获得作者授权)