公司最近要开发新的移动端项目,项目中有很多页面都用到了tab分组。
想起在 Android 中看到的 TabLayout + ViewPager 的组合,决定动手做一个类似的 Vue 组件。
github地址
组件设计
组件的功能基本参照 Android 端,首先把组件的设计要点列出来
- 顶部是一个可以横向的 tab-nav 容器;容器内需要填充 nav-item;使用者可以自定义 nav-Item 的具体内容,然后通过 template 与 slot 分发到这个容器中。
- tab-nav 容器有两种模式:
- nav-item 会自动扩展,平分容器宽度(每一个 nav-item 的宽度是一样的)。
- nav-item 不会自动扩展,每一个 nav-item 的宽度由使用者决定。
- tab-nav 容器内有导航条,nav-indicator (导航条) 可以在 tab-nav 会根据当前 nav-item 的 激活状态变化来滑动;可以定义 nav-indicator 的宽度缩放比,缩放比相对所在的 nav-item 的宽度;可以定义 nav-indicator 的位置(顶部或者底部);可以设置 nav-indicator 的位置偏移值;
- 除去顶部的 tab-nav,剩下的就是内容 tab-content,tab-content 的容器水平排列,宽度为浏览器可视区域的宽度。
- tab-content 的具体DOM结构同样通过 template 与 slot 分发到组件中。
- tab-content的父级可以水平滑动,滑动使用 translate3d。
- 在第一屏手指向右滑动时,禁止滑动,手指向左时可滑动,先向右再向左可以触发滑动。
- 在最后一屏手指向左滑动时,禁止滑动,手指向右滑动时可滑动,先向左再向右可以触发滑动。
- 除去上述两种情况,其他均可任意滑动。
- 触发滑动到下一页(前一页)的条件默认是手指滑动距离为屏幕宽度1/3以上。
- tab-content 滑动时,nav-indicator 需要跟着一起滑动
- nav-indicator 滑动到目标位置后,如果当前 nav-item 不在浏览器可视区域的正中间,tab-nav 容器会尽可能的滑动使当前 nav-item 到达可视区域的正中间,如果允许的滑动距离不够的话,使用最大的可滑动距离。
在实际写代码前,尽可能的列出需求可以防止开发中突然发现新需求而不得不破坏当前结构的情况。(说的就是我o(╥﹏╥)o)
下面开始分析代码如何写。
组件代码分析
- 首先分析模板及内容的 slot
1 | <!-- swipe-tab-container.vue --> |
1 | // swipe-tab-container.vue |
1 | // 具体样式可以查看代码 |
无论是 nav-item 还是 tab-content 都使用 slot 分发,能够最大限度的允许使用者自定义内容。其他则属于组件自身的行为。
现在已经可以分发 DOM 节点了,但是 tab-nav 容器的宽度在第二种模式是有问题的。
内部的div的宽度如果自然伸展,最大不会比它的容器大,所以,在第二种模式下是需要计算子元素的宽度并最终得出容器的最小宽度应该是多少,这里面会有一点点的偏差。
但是保险起见,在两种模式下都进行计算,并判断那个比较数值更大,然后使用数值大的那一个。
此外,因为 nav-indicator 和 tab-content 容器滑动时是要计算滑动的距离的,为了避免在滑动时的大量计算导致滑动掉帧,在初始化前将需要的值计算好。
初始化时还需要添加好手指滑动的事件,这里我使用了 hammerjs
作为手势库。
1 | // swipe-tab-container.vue |
在初始化滑动实例时,我只添加了 水平滑动的监听,垂直滑动交由浏览器自身处理。
并且监听了滑动取消的事件,因为如果在滑动期间发生了意外导致滑动被迫中断时可以迅速根据当前的条件判断页面应该滑动到那一页。
1 | export default { |
swipe-tab-container 的重要代码就差不多完成了。
还剩下 swipe-tab-nav,就是 nav-item 的组件。
swipe-tab-nav 默认显示 对象的 label 属性,当然也可以通过 slot 把自定义的 DOM 传入到组件中。
1 | <!-- swipe-tab-nav.vue --> |
1 | // swipe-tab-nav.vue |
1 | .swipe-tab-nav-item { |
遇到的一些问题
- 不同浏览器的原生滚动和hammer的兼容问题以及 touch-action 属性
前面在样式中提到过,在safari中或者Android的微信与hammerjs有着奇奇怪怪的行为,滑动经常无故被中断,要不就是浏览器的原生滑动被中断。
解决办法就是如果内部有列表使用了浏览器的原生滚动overflow: auto,那么该元素需要 touch-action: pan-y | pan ,并且hammer实例不能监听 垂直方向上的pan事件。
- DOM节点的width,height 以及 left,top 的精度问题
这其实是一个比较老生常谈的问题,DOM属性上的 width,height, left 和 top 一定是一个整数,所以使用这个属性时会缺失精度。建议使用 getBoundingClientRect。