原文鏈接: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 *
初始變量用于跟蹤約束的起始值?,F在我們來設定一下。
我們可以在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的高度是比較合適的??。
很棒,我們已經完成了!如果您覺得看這些長篇大論太過麻煩,您可以看一下下面這兩個主要的類,它們比較簡短,您可以從中快速找到或獲取您想要的東西??:
感謝閱讀!像往常一樣,歡迎留下您的評論和想法!
(譯者注:本文翻譯已獲得作者授權)