iOS系统导航栏自定义标题动画跳变解析

2,001 阅读6分钟

如果我们使用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谢谢!