Sucha's Blog ~ Archive for July, 2021

21年7月17日 周六 21:10

定制 UINavigationBar 的转场动画

最近 app UI 改版,交互设计师给出了 UINavigationController 下 Push/Pop 的转场动画,Push 的时候,fromView 会被缓慢往左推,toView 往左慢慢全部盖住 fromView,盖住之前,fromView 大概往左侧移动了 1/3 个页面;Pop 的时候则相反。

单单这样的转场动画是容易实现的,网上的教程一大堆,但难点在 UINavigationBar 也需要实现同样的交互。问题是 UINavigationBar 的动画是系统提供的,UIViewController 仅仅提供左、右、中的 CustomView,而且就我们的 App 来说,首页是隐藏的,在二级页面才会显示出来。由于 UINavigationBar 是系统管理的,转场动画的框架是没有对 UINavigationBar 提供支持的,得自己做。

网上搜索了一下,美团的技术博客提供了思路,具体我用的方案,在 iOS 14 模拟器上验证了蛮久,加上一些调试,如下:

Push 时,对 fromView 的 UINavigationBar 截图;如果是 hidden 状态则不用截图。toView 的 NavigationBar 是假的,但却是用真的 UINavigationBarItem 来构建的,真的 UINavigationBar,系统还未开始绘制呢。假的 NavigationBar 使用了 UINavigationBarItem 的 leftBarItem、rightBarItem 和 titleView 或者 title string。

这里需要注意将 fromViewController 上面对真的 UINavigationBarItem 的链接进行切断,比如 titleView 给他设置一个空的 UIView,leftBarItems、rightBarItems 给设置空的数组来填充。

fromView 截图的 UINavigationBar 按照转场动画的时长 Push 到左侧,toView 带的假的 NavigationBar 则从右侧 Push 进来,等动画结束,将 UINavigationBarItem 上的数据再拿给 fromViewController 使用,将假的 NavigationBar removeFromSuperview。

Pop 的时候需要区分是按返回按钮的 Pop,还是全局右划可中断的 interactive Pop。

先说简单的按返回按钮的 Pop,fromView 在对 UINavigationBar 截图后往右,toView 带着截图的 NavigationBar 出来,等动画结束,截图 NavigationBar removeFromSuperview 就好,这里比较简单。

可中断的 interactive Pop 复杂一些,举例我们用的是 FDFullscreenPopGesture,它其实是截获系统提供的全局右划返回功能。但系统的全局右划返回,当 UINavigationBar 的 backItem 被设置为 nil 等时候,是不起作用的。这个库是拿到系统手势检测 view,添加手势,检测到后调用系统的全局右划返回接口,发起是可中断的右划返回动画。

这个库暴露出来的检测手势 gesture 实例,我们后面会用到。

可中断的全局右划返回 Pop 跟直接调用 UINavigationController 的 popViewController 很不一样,由于动画可以中断,因此需要在 fromView 上建立假的 NavigationBar,截图就好。

手势的开始,来自于监听上面说到的 gesture 的 target action,可以监听 begin、change、end 事件,并计算右滑的百分比,用于调整截图 NavigationBar 跟随 fromView。难点来了,手势结束的时候,实际上右划动画也许结束,也许会中断返回,具体会如何,其实是系统判定的,我们并不知道后面动画要怎么样的,包括时长多久也都不知道,因此这里,需要建立一个跟踪机制,我用的是 DisplayLink 来做的,模拟器上看,比较跟手,但是太极端的情况,也会偶尔拉跨。

另外上面说到的假的 NavigationBar 将 UINavigationBarItem 给真的 UINavigationBar 时,其实会闪一下;以及截图 NavigationBar 的时机,实际上比转场动画要早,也就是截图代码不能放在转场动画开始的地方,那个地方才截图的话,太晚了。

因此需要在 UINavigationController 的 push、pop 接口地方做截图,另外全局右划的可中断 pop,截图是在手势开始的 begin,因为流程是 begin 开始后,才会调用到 UINavigationController 的 pop。

太多细节细节,不现场调试根本说不清楚,也不晓得会不会在后面的系统更新中变化,这里大体说了思路,以及需要注意的地方。

当然上面也提到过最后的效果,其实蛮不错的,

说一下前提,如果 UINavigationBar 设置了 translant 且真的用上了半透的话,效果应该是不行的,因为系统真实的 UINavigationBar 动画在底下会动,得实际遮住才行,因此这套东西,我是觉得只能用在非 translant 的场景下,当然,如果只是设置了 translant,但实际上又带了 opaque 的背景,仅仅是为了带一丢丢高斯模糊背景的话,是没问题的。

21年7月11日 周日 12:08

PinStackView

目前工程主要使用 AutoLayout,但一些复杂页面上面,先不说各种约束优先级的问题,单单就因为约束不对造成的约束失败,在 debug console 里面打印一大堆,都不好定位在哪里,别说去修复了。

总觉得大部分的场景,其实使用固定 frame 的 layout 就足够了,不需要那么复杂,于是我在工程里面引进了 PinLayout

因为使用 PinLayout 构建复杂页面确实太辛苦,其只用于搭建不复杂的元素,之前使用的是 StackViewLayout,但测试反馈问题比较多,我自己也发现,不大稳定,偶尔还有 crash,毕竟这个库是未完成的状态,然后还自带内存泄漏。

于是就想着自己写一个,毕竟 StackViewLayout 也用了这么久,接口已经蛮熟悉的了,而在功能上面,需要的又不多。

于是就有了 PinStackView,这个库相比 PinStackLayout 来说,简单很多,其实只有一个文件,另外,其处理流程上也是很简单的。

配置

最后增加了一个 autoSizeChangedCallback 的回调函数,如果 style 设置为了 auto,PinStackView 在 axis 上的长度由其 item 动态长度计算得到,每次对 bounds 有更改,都会 callback 通知。

另外上面的设置其实隐含了一些前提,在 fixed style 时,PinStackView 的大小是外部设置的,此时才有 start、end、equal 这些 distribution;而在 auto style 下,只有 start distribution,其实很好理解。

管理接口

下面是管理 view 的接口,添加、插入都会返回 PinStackItemInfo

item 配置

对于每个管理 view,在添加、插入的时候,都绑定了一个 PinStackItemInfo 用来描述其 layout 信息,下面的接口都可以进行链式配置

上面第二点,如果设置了 alignment 或者 alignSelf 为 stretch,会覆盖 item 在 cross axis 的长度

计算流程

建议看代码吧,其实没啥好说的,fxied style 下面,equal 就是平均 axis 的长度,start、end 的计算是最复杂的,因为可以带 grow(),需要先计算固定长度的,后面再分配动态长度的。

auto style 其实相对好计算,计算完所有管理 view 沿着 axis 的长度后,对比 PinStackView 在 axis 的长度,有变化的话就修改 PinStackView 的长度,然后 callback。说一下 callback,这个通知,可以回调到外部依赖 PinStackView frame 的地方,重新设定相关的 layout。

这个当然有可能造成 layout 的循环,之前的 StackViewLayout 就有这样的场景,我在这里将这个依赖交给了用户,用户自己来控制。

PinStackView 里面对于管理 view 的宽高计算是一个焦点,代码在 calcViewSize() 里面

将 StackViewLayout 换成 PinStackView 后,循环 layout 的场景不再出现,layout 也挺正常的,但是公司人员变动太大,貌似这个需求测完了,但上线遥遥无期,再说吧。