視差效果是我們?cè)谠S多 app 中經(jīng)常能夠看到的一種界面視覺效果钻洒。尤其是在滾動(dòng)列表中應(yīng)用得尤為廣泛。
我們首先來看看最終實(shí)現(xiàn)的效果:
整個(gè)效果實(shí)現(xiàn)的要點(diǎn)總結(jié)如下:
- 圖片退出速度慢于列表滑動(dòng)速度
- 圖片全程被列表覆蓋并且在退出同時(shí)淡出
- 列表下拉越界后圖片按比例放大
首先我們準(zhǔn)備工程廊谓,在所需的 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 的一個(gè)內(nèi)建屬性麻削,它用來設(shè)置是否讓內(nèi)部的 UITableView 在頂部留出一定空間來防止被導(dǎo)航條覆蓋蒸痹,因?yàn)槲覀円謩?dòng)調(diào)節(jié) inset,所以把這個(gè)屬性就設(shè)置為NO
呛哟。
然后我們用setContentInset
為 UITableView 設(shè)置內(nèi)部間距叠荠,這里我們假定圖片最大高度為300。
下面是對(duì) UIImageView 進(jìn)行設(shè)置扫责,我們?cè)O(shè)置了它的錨點(diǎn)榛鼎,這里作用是什么我們后面會(huì)講到。
最后,需要注意的是subView的添加順序者娱,要先添加圖片蜘渣,再添加列表,因?yàn)閳D片要被蓋住肺然。為了避免圖片被列表完全蓋住,我們要把列表的背景設(shè)為透明腿准,然后通過列表中的 cell 來蓋住圖片际起。
接下來我們來編寫視差效果的核心部分,計(jì)算圖片位移和縮放:
- (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;
}
}
這段代碼我不全部解釋吐葱,絕大部分大家應(yīng)該能夠自己看懂街望。
首先我們要用 valueForKey
得到 Apple 沒有對(duì)外公開的一個(gè)屬性叫做contentOffset
,它是用來表示列表滑動(dòng)距離最頂部的距離的弟跑,因?yàn)槲覀冊(cè)O(shè)置了內(nèi)補(bǔ)灾前,所以這個(gè)值會(huì)從-300開始計(jì)算。
如果列表下拉越界孟辑,那么這個(gè)值將會(huì)比-300還要小哎甲,因此我們可以依次判斷列表是否越界,一旦越界饲嗽,那么這個(gè)值得絕對(duì)值就會(huì)是圖片應(yīng)當(dāng)拉伸到的高度炭玫。因?yàn)橐缺瓤s放,所以我們計(jì)算縮放因子貌虾,然后交給 transform 來縮放圖片吞加,而不是直接設(shè)置圖片的 frame。
這里就要提到之前我們?cè)O(shè)置的 anchorPoint
了尽狠,為什么要設(shè)置它呢衔憨?因?yàn)槟J(rèn)情況下 transform 的中心點(diǎn)在整個(gè) UIView 的中心位置,這樣圖片縮放的時(shí)候就會(huì)以圖片的中心進(jìn)行縮放了袄膏。為了實(shí)現(xiàn)預(yù)想的效果践图,我們就要把anchorPoint
設(shè)置為圖片的中上位置
但是,這樣設(shè)置之后哩陕,圖片就會(huì)下降150像素平项,所以我們把 frame 的 y 設(shè)置為-150。
至于其他部分悍及,我們還設(shè)置了 UINavigationBar 的透明度和圖片的透明度闽瓢。
至此我們就基本實(shí)現(xiàn)了視差效果的邏輯和計(jì)算部分,但是這個(gè)makeParallaxEffect
函數(shù)應(yīng)該什么時(shí)候被調(diào)用呢心赶?這里我們就要利用到 Runtime 的一個(gè)重要特性 —— KVO扣讼。即當(dāng)contentOffset
發(fā)生變化時(shí)執(zhí)行一個(gè)回調(diào),這樣我們就可以實(shí)時(shí)地計(jì)算視差效果了缨叫。
那這個(gè) KVO 在哪里添加呢椭符?我之前添加在了viewDidLoad
中荔燎,但是發(fā)現(xiàn)當(dāng) ViewController被 pop 后 app 會(huì) crash。最后我在viewWillDisappear
中 remove 掉了這個(gè) KVO销钝,問題得到解決有咨。
但是問題又來了,我們知道蒸健,iOS 7之后用戶可以通過邊緣滑動(dòng)的方式來返回上一級(jí)頁面座享,但如果我們向右滑動(dòng)的距離不足以讓頁面返回,那么viewWillDisappear
也會(huì)被調(diào)用似忧,viewWillAppear
也會(huì)被調(diào)用渣叛。所以我們索性就把 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 的透明度設(shè)置回來盯捌。
好了淳衙,至此我們就實(shí)現(xiàn)了這樣一個(gè)簡(jiǎn)單的視差效果。