在使用Twitter
的APP後,我已開發者的視覺並注意到整體與部分之間相互協調是件極其有意思的事情。這引起了我的好奇心:這是怎麼做到的?
讓我們具體地討論下這個視圖佈局:此效果不優雅嗎?它看起開就像本應如此,但你仔細的觀察後就會發現更多。隨着Scrollview
的偏移,圖層的覆蓋,動作和比例縮放是那麼的平滑連貫… … 實在是太喜歡這個效果了。
So,就讓我們立刻實現這個效果吧。
首先,先看下最終效果:
結構描述
在寫代碼之前,我想給你一個關於如何構建UI的簡單意見。
打開Main.storyboard
文件,在這個控制器裏面你會發現2個主要的對象。第一個是一個呈現Header
的視圖,第二個是Scrollview
,它包含了Avatar
和賬號相關的其他信息,如:username
標籤和Follow
按鈕。還有一個被叫做Sizer
的視圖,它是爲了確保Scrollview
擁有足夠大的垂直滑動的空間。
就像你看到的那樣,這個結構非常的簡單。稍微注意一下就可發現Header
的外部放置了一個Scrollview
,而不是與其他元素放置在一起。雖然沒必嚴格如此,但這樣會使它的結構變動更加靈活。
編碼
如果你仔細的看了最後的動畫,將會注意到你要管理2個不同的動作:
- 向下拉(當
Scrollview
已經停靠在屏幕的頂部的時候) - 上下滑動
第二個動作可以細分爲4個小步驟:
- 向上滑動,一直到導航條默認的大小並停靠在屏幕的頂部。
- 向上滑動,
Avatar
開始逐漸變小。 - 當
Header
被固定後,Avatar
會移動到它的下邊。 - 當
username
標籤抵達Header
的頂部時,一個新的白色Label
將會從Header
中心的底部展現。這時Header
的背景圖片將會用高斯模糊渲染。
打開ViewController
讓我們一個一個的實現這些步驟。
構建管理者
首先要做的事情很明顯,就是獲取關於Scrollview
的偏移量offset
。我們可以通過UIScrollViewDelegate
協議實現scrollViewDidScroll
方法。
在一個View
上執行最簡單地動畫方式是使用Core Animation
逐漸的進行三維變換,並給layer.transform
賦予新值。
關於Core Animation
可以參考這篇文章
這些是scrollViewDidScroll:
方法的第一部分
CGFloat offset = scrollView.contentOffset.y;
CATransform3D avatarTransform = CATransform3DIdentity;
CATransform3D headerTransform = CATransform3DIdentity;
在這裏我們獲取一個當前垂直偏移量`offset`,並初始化2個`CATransform3D`變量。
下拉
下拉動作的管理:
if (offset < 0) {
CGFloat headerScaleFactor = -(offset) / header.bounds.size.height;
CGFloat headerSizevariation = (header.bounds.size.height * (1.0 + headerScaleFactor) - header.bounds.size.height) / 2.0;
headerTransform = CATransform3DTranslate(headerTransform, 0, headerSizevariation, 0);
headerTransform = CATransform3DScale(headerTransform, 1.0 + headerScaleFactor, 1.0 + headerScaleFactor, 0);
header.layer.transform = headerTransform;
}
首先,我們檢查offset
是否爲負數:用戶在下拉的過程中,將會進入Scrollview
的彈性區域。
剩下的代碼就是簡單的數學邏輯。
Header
的擴大是因爲它的上邊緣固定於屏幕的頂部,而底部的圖片在等比縮放。
the transformation is made by scaling and subsequently translating to the top for a value equal to the size variation of the view.
實際上,移動ImageView
圖層的中點到頂部並同時縮放它,你可以獲得相同的效果。
headerScaleFactor
是用來被計算的一部分。我們想用offset
適當的對Header
進行縮放。換句話說,當offset
是Header
高度的2倍時,headerScaleFactor
必須是2.0。
我們需要管理的第二個動作是上下滑動。讓我們看看,如何一步步通過UI的主要元素完成變換的。
頭部(第一階段)
當前的offset
應該大於0。Header
應該隨offset
進行垂直變換,直到它期望的高度(我們後面將會講解Header
的高斯模糊)。
headerTransform = CATransform3DTranslate(headerTransform, 0, MAX(-offset_HeaderStop, -offset), 0);
這句代碼非常簡單。我們只需定義一個讓`Header`在此停止移動的最小值。
讓我感到羞愧的是我比較懶!所以我寫死了一些數值,像`offset_HeaderStop`。其實,我們可以通過計算UI元素的位置來獲取相同的效果。下次有空再改吧。
頭像
Avatar
的縮放與我們處理下拉的邏輯一樣,只是在這種情況下,圖片是到達底部而不是頂部。這段代碼和上邊的比較相似,除了減小縮放的比例爲1.4。
// Avatar -----------
CGFloat avatarScaleFactor = MIN(offset_HeaderStop, offset) / avatarImage.bounds.size.height / 1.4;
CGFloat avatarSizevariation = (avatarImage.bounds.size.height * (1.0 + avatarScaleFactor) - avatarImage.bounds.size.height) / 2.0;
avatarTransform = CATransform3DTranslate(avatarTransform, 0, avatarSizevariation, 0);
avatarTransform = CATransform3DScale(avatarTransform, 1.0-avatarScaleFactor, 1.0-avatarScaleFactor, 0);
就像你看到的,當`Header`停止變化時,我們用`MIN`函數來使`Avatar`的縮放停止。
此時,我們根據當前`offset`設置最頂層的圖層。除非`offset`小於等於`offset_HeaderStop`,最頂層的圖層是`Avatar`,否則是`Header`。
if (offset <= offset_HeaderStop) {
if (avatarImage.layer.zPosition < header.layer.zPosition) {
header.layer.zPosition = 0;
}
} else {
if (avatarImage.layer.zPosition >= header.layer.zPosition) {
header.layer.zPosition = 2;
}
}
}
白色Label
這段代碼是白色Label
的動畫:
// ------------ Label
CATransform3D labelTransform = CATransform3DMakeTranslation(0, MAX(-distance_W_LabelHeader, offset_B_LabelHeader - offset), 0);
headerLabel.layer.transform = labelTransform;
這裏有2個令我感到羞愧的變量值:當`offset`等於`offset_B_LabelHeader`時,黑色的`username`標籤剛到觸碰到`Header`的底部。
distance_W_LabelHeader
是Header
底部與白色Label
終點之間的距離。
這個變換是通過此邏輯計算:黑色Label
觸碰到Header
,白色Label
就會立即出現,並且到達Header
中點位置就停止移動。所以我們使用下面代碼創建Y
值:
MAX(-distance_W_LabelHeader, offset_B_LabelHeader - offset)
高斯模糊
最後一個效果是Header
的模糊。爲了得到合適的解決方案,我用了3個不同的庫… … 我也嘗試過用OpenGL ES
創建基類,但實時更新模糊總是非常緩慢。
然後我意識到我可以對模糊僅僅計算一次,將不模糊和模糊的圖片進行重疊,只是改變alpha
值。我非常確信,Twitter
就是這樣做的。
在viewDidAppear
中,我們計算Header
的模糊值並隱藏它,設置alpha
值爲0。
// Header - Blurred Image
headerBlurImageView = [[UIImageView alloc] initWithFrame:header.bounds];
headerBlurImageView.image = [[UIImage imageNamed:@"header_bg"] blurredImageWithRadius:10 iterations:20 tintColor:[UIColor clearColor]];
headerBlurImageView.contentMode = UIViewContentModeScaleAspectFill;
headerBlurImageView.alpha = 0.0;
[header insertSubview:headerBlurImageView belowSubview:headerLabel];
header.clipsToBounds = YES;
模糊視圖是用過FXBlurView
實現的。
在scrollViewDidScroll:
方法中,我們只需根據offset
設置alpha
:
// ------------ Blur
headerBlurImageView.alpha = MIN(1.0, (offset - offset_B_LabelHeader) / distance_W_LabelHeader);
這個計算的背後邏輯是:alpha
最大值是1,當黑色Label
觸碰到Header
時模糊效果開始出現,當白色到達最終位置時,也將停止繼續模糊。
就這樣!
我希望你喜歡這個教程。學習如何重現這種很棒的動畫效果對我來說是很大的樂趣。
Swift代碼:Download Source
OC代碼:Download Source