iOS系統導航欄自定義標題動畫跳變解析

阿曌發表於2019-03-14

如果我們使用iOS系統的導航欄,自己設定titleView,leftItem和rightItem,當titleView長度達到一定時,push會出現titleView左右跳變的情況,本文將分析跳變原因及解決辦法。

導航欄的內部佈局

在一個全新的APP,自定義導航欄的左中右後,檢視佈局,會發現,導航欄內部佈局如下

在這裡插入圖片描述

設定了自定義leftItem,titleView和rightItem,在導航欄中,我們自定義的view都會被_UITAMICAdaptorView包裹,其中leftItem和rightItem在_UITAMICAdaptorView外還會包裹一層_UIButtonBarStackView,最後佈局在_UINavigationBarContentView中。

在導航欄內部佈局的左邊塊、中間塊和右邊塊,以下簡稱ABC,整個螢幕寬為Width。

以下以iPhone XS Max為例,gap1為20,gap2為6。

安全區域

A不論寬度如何(包括為0),一定會距離左邊gap1。

C不論寬度如何(包括為0),一定會距離右邊gap1。

B就算再寬,也一定會距離A和C各gap2。

在這裡插入圖片描述
(A設定寬40,B設定寬414,C設定寬40)

當A和C寬度設為0時,B距離螢幕左右各(gap1+gap2)。

在這裡插入圖片描述

當A和C設定為nil時,B距離螢幕左右各12(gap3)。

在這裡插入圖片描述

對齊方式

當增加A的寬度時,A是以左邊不動,右邊增加來加寬的,B的寬度會因A寬度增加而壓縮,A最寬不超過C.left-gap2*2。

在這裡插入圖片描述

當增加C的寬度時,C是以右邊不動,左邊增加來加寬的,B的寬度會因C寬度增加而壓縮,C最寬不超過A.right-gap2*2。

在這裡插入圖片描述

當調節B的寬度時,B預設是以導航欄中心為錨點,左右同時增加,且最大不會超過 162(Width-A.width-B.width-gap12-gap22)

在這裡插入圖片描述

當把ABC全部調成螢幕寬時,B會被完全擠沒,AC平分除了安全區域的所有空間(Width-gap12-gap22)

在這裡插入圖片描述

導航欄標題欄動畫

從左到右的跳變的產生

首先理解了前面的佈局,可知道B的x座標的相對於A的計算公式

B.left = Max( (Width - B.width)/2 , A.right+gap2)

B的x座標理想情況下是(Width - B.width)/2,也就是動畫結束位置,實際x座標位置可能是(Width - B.width)/2或者(A.right+gap2)(兩者取最大值),也就是最後佈局位置。

當實際位置為A.right+gap2時,說明動畫初始位置在實際位置左邊,就會出現push時,導航欄title左側有個從左到右的跳變。

在這裡插入圖片描述

從右到左的跳變的產生

同理,B的right座標的相對於C的計算公式

B.right = Min( (Width + B.width)/2 , C.left-gap2)

B的right座標理想情況下是 (Width + B.width)/2,也就是動畫結束位置,實際位置可能是(Width + B.width)/2或者(C.left-gap2)(兩者取最小值),也就是最後佈局位置。

當實際位置為(C.left-gap2)時,說明動畫初始位置在實際位置右邊,就會出現push時,導航欄title右側有個從右到左的跳變。

在這裡插入圖片描述

防止跳變的結論

為了防止上述兩種跳變,只要令B的left實際位置為 (Width - B.width)/2,B的right實際位置為 (Width + B.width)/2,也就是

求 (Width - B.width)/2 > (A.right+gap2) 且 (Width + B.width)/2 < C.left-gap2 的 B.width的取值範圍? 因已知 A.right = gap1 + A.width + gap2,且 C.left = Width - gap2 - C.width - gap1 可求得B的寬度限制為 B.width < Width - gap12 - gap22 - A.width2 且 B.width < Width - gap12 - gap22 - C.width2 也就是 B.width < Width - gap12 - gap22 - Max(A.width, C.width)*2

翻譯成中文就是B的寬度不能超過螢幕寬減去固定的安全區域再減去A和C之中最寬的2倍。

解決了?

不,還沒完,到目前這步,是手Q8.0.0之前的做法,設定了A和C可能存在的最大寬度(因為AC的寬度是可能會變的,比如左邊沒有未讀訊息和有99條未讀寬度是不一樣的,再比如右邊可能有一個圖示或兩個圖示),然後得到的B的寬度就很窄了。

如圖,B和A之間還有一大段距離沒有利用上,如果想利用上這段空間,又不希望出現跳變,該怎麼辦呢?

推翻從右到左的跳變

首先要再回到導航欄標題欄動畫 - 從右到左的跳變的產生,其實因為系統動畫本身就是從右到左,所以看不出來有跳變,會令人以為是正常的動畫,以下兩張圖,就動畫而言,不會令人有跳變的感覺。

在這裡插入圖片描述
在這裡插入圖片描述

會有跳變的感覺是因為加上內容後,B的內容從C中滑過

在這裡插入圖片描述
在這裡插入圖片描述

但一般情況下,C放置的都是圖示,空白區域很大,B的內容從C有動畫滑過其實可以接受。

如果可以接受,那麼B的寬度就變為了只依賴A的寬度

B.width < Width - gap12 - gap22 - A.width*2

不接受“推翻從右到左的跳變”

不行,追求完美的人說,我就是這麼一點點跳變都不能接受,而且,上面的方法只解決了C大於A的情況,A大於C的情況還是有問題呀!

好,下面重點介紹下planB——

內容越界方案

首先,ABC裡的內容,是可以超過ABC的寬度限制顯示的!(後面ABC的內容各稱為abc)

什麼意思呢,回到上一張圖,當我把A的內容“< left”的x座標設為-20,a就頂著螢幕左邊出現了。

如果我把ABC寬度都調為0,再看內容的顯示:

在這裡插入圖片描述

可以看到除了a的x座標被我設了-20,b和c都是以B和C的x座標為原點顯示的,並且是全部顯示,不會因為寬度為0就不顯示,也就是結論:ABC內容的顯示不會被其寬度影響,但是會位置會受ABC的x座標的影響。(當然前提你自己不能給自定義的view設定clipsToBounds為真)

也就是說,在"防止跳變的結論"基礎上,我們可以把b的位置根據AC寬度進行調整,如下圖

在這裡插入圖片描述

C比A寬,B和A之間空餘了X的寬度(X.width = C.width - A.width),那麼b的x起始點位置就可以計算為 -X.width(也就是A.width - C.width),b的最大寬度為Width - A.width - C.width - gap12 - gap22;

在這裡插入圖片描述

同理假如A比C寬,B和C之間就空餘了X的寬度(X.width = A.width - C.width),那麼b的x座標為0,b的寬度為Width - A.width - C.width - gap12 - gap22。

在這裡插入圖片描述

綜上,計算b的公式為

b.left = Min(0, A.width - C.width) b.width = Width - A.width - C.width - gap12 - gap22

當B的背景顏色置為透明時,看效果就只看到B的內容了(以下兩圖區別在於右圖B背景設為透明)

在這裡插入圖片描述
在這裡插入圖片描述

(PS.由實踐看出,當a的x座標處於安全區域gap1內時,push動畫會有一個該區域從無到有的變化,同理當c的right位置處於最右邊的安全區域也有,所以建議A和C的內容不要越過安全區域,但是這個也是有解決辦法的,以後再說。)

基於以上方案,也可以一開始就把B的寬度設為0,然後每次只需要計算b的座標和寬度就行了,還可以通過計算令B把左右gap2的區域也佔掉。

在手Q上的實踐效果:左圖長標題,右圖短標題(左邊的未讀訊息數從無到有)

在這裡插入圖片描述
在這裡插入圖片描述

附:不同機型下gap1和gap2的值

新增gap3(當A和C設為nil,B距離螢幕左右距離)

在這裡插入圖片描述

綜上,可以判斷

if (SCREEN_WIDTH > 375) {
    gap1 = 20;
    gap3 = 12
} else {
    gap1 = 16;
    gap3 = 8;
}
    gap2 = 6;
複製程式碼

Demo原始碼:github.com/Xieyupeng52…

如果有幫助到你,請給我Github上一個Star鼓勵一下O(∩_∩)O謝謝!

相關文章