在 web 里实现 iphone 通讯录上顶浮动的效果

需求

客户是做设计的, 对细节要求很高.

顶屁股

注意看, A 到达顶部后变成浮动的, B 到达 A 的后面时, 把 A 顶上去以后, B 又变成了浮动的, 直到 C 到达后, 顺次再顶.

我将其称为 要能顶到上一个的屁股,再将其顶入进去

看似很简单的一个效果, 但是要在 HTML 里实现出来就没这么容易了.

我们来看看怎么实现这个 顶屁股 的效果

达到位置变浮动

先实现达到顶部后变为浮动, 下滑时又跟着走这个效果

监听 scroll 事件, 计算 scroll 的距离, 在其达到顶部时将要浮动的元素的 position 改为 fixed

scroll 离开顶部后, position 改回去

这个实现起来不难, 但在在 iOS 下有很大的坑, 后面再说

把上一个顶进去

网上也有个别例子, 但是都是使用 absolute + hidden + 虚拟元素的方式来实现的.

用 absolute 局限性太大, 可能外面有 n 个父层

也不能用一个虚拟的浮动元素, 改变其显示值的方案. 因为项目里浮动的这个元素是一个手风琴面板, 点击后要能收缩里面的内容, 有一些自已的功能和独特的显示在里面. 网上有的例子只需要改文本值, 我这里肯定不行了, 必须把真实的那个元素浮动在那里, 否则 clone 功能都会弄死人.

不过反正也监听了 scroll, 在下一个浮动距离上一个浮动达到需要顶起的距离时, 改变上一个浮动的 top, 让其升出屏幕外, 模拟出顶起的效果, 这样就 ok 了

        window.addEventListener('scroll', function (e) {
          if (window.scrollY - self.project_top >= 0 && self.project_is_open) {
            project_title.addClass('fixed')
          } else {
            project_title.removeClass('fixed')
          }
          // project 打开时才考虑 + 48
          if (window.scrollY - self.type_top + 48 >= 0 && self.type_is_open && self.project_is_open) {
            type_title.addClass('fixed')
          } else if (window.scrollY - self.type_top >= 0 && self.type_is_open && !self.project_is_open) {
            type_title.addClass('fixed')
          } else {
            type_title.removeClass('fixed')
          }
          // 距离 60 px 时,顶上面那个
          let ding_top = window.scrollY - self.type_top + 108
          if (ding_top > 0) {
            project_title.css({'top': '-' + ding_top + 'px'})
          } else {
            project_title.css({'top': '0px'})
          }
        })

不够顺滑

用 js 来改 css top, 看着很简单, 但是用起来才知道有多惨

就像用磁铁推着另外一块磁铁走, 会颤抖不说, 拉猛了, 中间的距离还会缩短, 甚至重叠在一起.

还是要用 absolute 的方式, ding_top > 0 时, 把上一个浮动改为 absolute 的, 掉到 box 底部, 于是就能用下面的元素非常顺滑的推上去了

当然, 因为 absolute 对应父元素的结构和 css 都有修改

        // 距离 108 px 时,顶上面那个
        let ding_top = window.scrollY - self.type_top + 108
        console.log(ding_top)
        if (ding_top > 0) {
          if (self.project_is_open) {
            project_title.addClass('title-absolute-bz')
            project_title.removeClass('fixed')
          }
        } else {
          project_title.removeClass('title-absolute-bz')
        }

iOS 下 scroll 是废物

对scroll事件做了很大限制:

手指划动屏幕 -> 滚动 -> 手指抬起 -> 惯性滚动 -> 停止滚动

也就是你手指放去滑动时, 并没有触发 scroll, 放开后才触发.

导致手指在屏幕上时, 该浮动的元素会被带出屏幕顶部, 放开后又才回来, 达不到想要的效果了

平滑吸顶效果变成了滚过临界位置直到停止滚动时, 吸顶元素又跳到目标位置

absolute 的元素在手指离开时也不会掉下来, 看起来变成顶着上面的其他元素走了, 还能再惨一点么?

无数的人都已吐过这个特性了

在 scroll 的过程中是不会有任何的 js 可以被触发执行的

单独要一篇来说这个深不见底的坑 iOS 流畅滚动废了 scroll 的问题 iOS 流畅滚动废了 scroll 的问题.md

浮动以后占位的问题

本来不难, 但是因为了用 semantic 的 accordion, 无法在里面添加一个空的 div 来占位

最后用了 absolute

其他功能带来的复杂度

目前看上去也不是很难的样子, 但是还有其他的一些功能要考虑

前面说过这是一个手风琴组件, 页面打开后, 要获取数据, 填充, 展开, 展开还有动画, 导致有延时.

scroll 计算时需要取到需浮动元素的 top, 显然是不能在一开始就取的. 因为上面的元素还没有展开, 好在 accrodion 有个 onOpen 回调, 在里面取就行了.

等等, 根据业务需求, 初始的时候, accrodion 可以是展开, 也可以是关闭的, 放 onOpen 里未必能取到.

另外在浏览时, 用户可以把 accrodion 点击缩起来, 这时页面高度就变了, 还是用之前取到的 top 来计算, 妥妥的会出错.

还有就是 accrodion 关闭时对应元素就不能浮动了, 在计算 top 和顶起时都要考虑这个问题.

总结

知乎上有人问: 为什么开发软件这么难?

我其实有点想把这个发上去了, 就这么一个模拟 iOS 通讯录上顶的小需求, 对客户来说可能是一点点感受上的完美优化.

但是做出来还是要花不少时间和功夫的, 再加上其他的功能的影响, 耦合交互.

为了实现这一点完美, 就得做那么多工作, 考虑那么多的交互和影响. 其他还有那么多功能要开发呢, 你说开发软件难不难?

完成