視差效果是我們在許多 app 中經常能夠看到的一種界面視覺效果。尤其是在滾動列表中應用得尤為廣泛。
我們首先來看看最終實現的效果:
整個效果實現的要點總結如下:
- 圖片退出速度慢于列表滑動速度
- 圖片全程被列表覆蓋并且在退出同時淡出
- 列表下拉越界后圖片按比例放大
首先我們準備工程,在所需的 ViewController 中分別加入 UITableView 和 UIImageView:
- (void)viewDidLoad {
[super viewDidLoad];
self.automaticallyAdjustsScrollViewInsets = NO;
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
[self.tableView setBackgroundColor:[UIColor colorWithWhite:1 alpha:0]];
[self.tableView setContentInset:UIEdgeInsetsMake(300, 0, 0, 0)];
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, -150, self.view.bounds.size.width, 300)];
self.imageView.layer.anchorPoint = CGPointMake(0.5f, 0);
[self.view addSubview:self.imageView];
[self.view addSubview:self.tableView];
}
下面我們一一分析,首先automaticallyAdjustsScrollViewInsets
是 UIViewController 的一個內建屬性,它用來設置是否讓內部的 UITableView 在頂部留出一定空間來防止被導航條覆蓋,因為我們要手動調節 inset,所以把這個屬性就設置為NO
。
然后我們用setContentInset
為 UITableView 設置內部間距,這里我們假定圖片最大高度為300。
下面是對 UIImageView 進行設置,我們設置了它的錨點,這里作用是什么我們后面會講到。
最后,需要注意的是subView的添加順序,要先添加圖片,再添加列表,因為圖片要被蓋住。為了避免圖片被列表完全蓋住,我們要把列表的背景設為透明,然后通過列表中的 cell 來蓋住圖片。
接下來我們來編寫視差效果的核心部分,計算圖片位移和縮放:
- (void)makeParallaxEffect {
CGPoint point = [((NSValue *) [self.tableView valueForKey:@"contentOffset"]) CGPointValue];
if (point.y < -300) {
float scaleFactor = fabs(point.y) / 300.f;
self.imageView.transform = CGAffineTransformMakeScale(scaleFactor, scaleFactor);
} else {
self.imageView.transform = CGAffineTransformMakeScale(1, 1);
}
if (point.y <= 0) {
if (point.y >= -300) {
self.imageView.transform = CGAffineTransformTranslate(self.imageView.transform, 0, (fabs(point.y) - 300) / 2.f);
}
self.imageView.alpha = fabs(point.y / 300.f);
self.navigationController.navigationBar.alpha = 1 - powf(fabs(point.y / 300.f), 3);
} else {
self.imageView.transform = CGAffineTransformTranslate(self.imageView.transform, 0, 0);
self.imageView.alpha = 0;
self.navigationController.navigationBar.alpha = 1;
}
}
這段代碼我不全部解釋,絕大部分大家應該能夠自己看懂。
首先我們要用 valueForKey
得到 Apple 沒有對外公開的一個屬性叫做contentOffset
,它是用來表示列表滑動距離最頂部的距離的,因為我們設置了內補,所以這個值會從-300開始計算。
如果列表下拉越界,那么這個值將會比-300還要小,因此我們可以依次判斷列表是否越界,一旦越界,那么這個值得絕對值就會是圖片應當拉伸到的高度。因為要等比縮放,所以我們計算縮放因子,然后交給 transform 來縮放圖片,而不是直接設置圖片的 frame。
這里就要提到之前我們設置的 anchorPoint
了,為什么要設置它呢?因為默認情況下 transform 的中心點在整個 UIView 的中心位置,這樣圖片縮放的時候就會以圖片的中心進行縮放了。為了實現預想的效果,我們就要把anchorPoint
設置為圖片的中上位置
但是,這樣設置之后,圖片就會下降150像素,所以我們把 frame 的 y 設置為-150。
至于其他部分,我們還設置了 UINavigationBar 的透明度和圖片的透明度。
至此我們就基本實現了視差效果的邏輯和計算部分,但是這個makeParallaxEffect
函數應該什么時候被調用呢?這里我們就要利用到 Runtime 的一個重要特性 —— KVO。即當contentOffset
發生變化時執行一個回調,這樣我們就可以實時地計算視差效果了。
那這個 KVO 在哪里添加呢?我之前添加在了viewDidLoad
中,但是發現當 ViewController被 pop 后 app 會 crash。最后我在viewWillDisappear
中 remove 掉了這個 KVO,問題得到解決。
但是問題又來了,我們知道,iOS 7之后用戶可以通過邊緣滑動的方式來返回上一級頁面,但如果我們向右滑動的距離不足以讓頁面返回,那么viewWillDisappear
也會被調用,viewWillAppear
也會被調用。所以我們索性就把 KVO 添加在viewWillAppear
中。
下面看最終的代碼:
- (void)viewWillAppear:(BOOL)animated {
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
[UIView beginAnimations:nil context:nil];
[self makeParallaxEffect];
[UIView commitAnimations];
}
- (void)viewWillDisappear:(BOOL)animated {
[UIView beginAnimations:nil context:nil];
self.navigationController.navigationBar.alpha = 1;
[UIView commitAnimations];
[self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if (object == self.tableView) {
[self makeParallaxEffect];
}
}
最后不要忘記在viewWillDisappear
中把 UINavigationBar 的透明度設置回來。
好了,至此我們就實現了這樣一個簡單的視差效果。