如果我们使用 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 上一个 Star 鼓励一下 O(∩_∩)O 谢谢!
来源: https://juejin.im/post/5c8a06b951882510fd114817