前言
最近被分配到做项目小程序端的任务,做到原生端常见的 TabLayout + ViewPager 实现的 Tab 切换页面时,发现小程序未提供类似可以直接使用的 TabLayout 组件。
网上搜寻发现小程序端需要实现 Tab 切换效果,多是通过 scroll-view 和 swiper 联动实现,并且 指示器 没有过渡效果,多是闪现跳到下一个 Tab 。因此自行研究实现接近原生端的 tab-layout 组件。
1.需求分析
下图是小程序 小米Lite 的 Tab 切换效果:切换 Page 时,Tab 下方的指示器(红色横条)是没有滚动效果而是直接闪现到下一个 Page 的,这也是市面上小程序常见的 Tab 切换效果。

原生端 TabLayout 常见的功能就是我们的目标效果,所以 目标效果 如下:
- 指示器(Index)具有切换过渡效果;
- 指示器(Index)需可自定义,常见的有:可固定宽度、可与 Tab 内容等宽、可覆盖在 Tab 上;
- Tab 可自定义、可支持自动适应父控件宽度等分 Tab 宽度;
- 当 Tab 总宽度超出父控件宽度时,Tab 行支持滚动且切换 Page 时保证当前 Tab 可见;
- 支持 Page 切换监听;
通过了解小程序组件及技术支持,选定通过 scroll-view 、swiper 、swiper-item 、movable-area 、movable-view 配合 插槽 和 抽象节点 来实现自定义组件 tab-layout 。
2.具体实现
2.1效果展示
2.1.1常见样式

包含特点:
- Page 懒加载;
- Page 切换时 Tab 自动跟随滚动;
- Page 内 scroll-view 的滚动处理;
2.1.2 Tab 宽度等分

包含特点:
- Tab 宽度等分组件宽度;
- 跳转指定 Position = 2 ;
- 监听页面切换;
2.1.3 Page 内容不一致

包含特点:
- 根据 Position 变化调整 Page 布局;
- Index 宽度与 Tab 宽度保持一致;
- Tab 总宽度未填满 TabLayout 时居中显示;
2.1.4 Index 覆盖 Tab

包含特点:
- Index 悬浮覆盖在 Tab 上;
- Tab 与 Index 之间插入固定 View
- Page 禁止左右滑动;
2.2实现步骤
2.2.1布局分析
- 使用可横向滑动的 scroll-view 作为 Tab 和 指示器(Index)的容器
- Tab 的具体内容与样式,由抽象节点 item-tab 决定
- Index 滑动区域由 movable-area 实现,长度应与 Tab 栏总宽度一致,高度由自定义属性 indexAreaHeight 赋值决定
- Index 的展示区域由 movable-view 实现,并提供插槽 slot name="index" 让用户可自定义 Index 样式(用户也可采用自定义属性 indexStyle 来直接设置 movable-view 的 style 从而实现 Index 的样式).
- 各 Tab 对应的 Page,由被 swiper 包裹的 swiper-item 充当容器,通过抽象节点 item-page 决定内容
具体如下图所示 :
<!--components/tab-layout/tab-layou.wxml-->
<view class="tablayout" id="tab-layout">
<!-- tablayout -->
<scroll-view class="sv-tab-layout" scroll-x="true" scroll-with-animation="true" scroll-left="{{tabLayoutScrollLeft}}">
<view class="{{isTabCenter?'tab-group-center':'tab-group'}}"
style="flex-flow:{{isSetIndexPositionAbsolute?'column':'column-reverse'}}">
<!-- index 活动区域(应与 tab-list 内容总宽度等宽)-->
<movable-area style="width:{{indexAreaWidth}}px;height:{{indexAreaHeight}}px;{{indexAreaStyle}}">
<!-- index 显示区域(应与 item-tab 等宽)-->
<movable-view class="tab-index" id="tab-index"
style="width:{{tabWidth}}px;height:{{indexAreaHeight}}px;{{indexStyle}}" x="{{tabIndexScrollX}}"
direction="horizontal" disabled="true">
<!-- index -->
<slot name="index"></slot>
</movable-view>
</movable-area>
<!-- tab 显示区域 -->
<view class="tab-list" id="tab-list" style="position:{{isSetIndexPositionAbsolute?'absolute':'unset'}};">
<view class="item-tab" id="tab{{index}}" wx:for="{{tabList}}" wx:key="index" bindtap="tapTab"
data-index="{{index}}" style="width:{{isTabSpaceEqual?tabWidth:auto}}px">
<!-- tab -->
<item-tab item="{{item}}" position="{{index}}" currentIndex="{{currentIndex}}"></item-tab>
</view>
</view>
<slot name="subTab"></slot>
</view>
</scroll-view>
<slot name="subContent"></slot>
<!-- swiper -->
<swiper current='{{targetIndex}}' bindchange="onChangePage">
<swiper-item wx:for="{{tabList}}" wx:key="index" catchtouchmove="{{isStopTouchMove?'stopTouchMove':''}}"
style="height:621px">
<item-page item="{{item}}" position="{{index}}" currentIndex="{{targetIndex}}" bind:updata="onPageUpdata">
</item-page>
</swiper-item>
</swiper>
</view>
2.2.2功能实现
- 在组件生命周期来到 attached 方法时:
获取所有 Tab 的宽度并记录,用设置 Index 的显示区域长度及活动区域长度
通过自定属性 targetIndex 判断是否需要进行页面切换跳转指定 page- 通过监听 Tab 的点击事件,促使 swiper 切换页面;
- 通过 swiper 组件的 bindchange 方法,监听页面切换事件;
- 在页面切换的时候:
计算并移动 Index(movable-view)到指定 Tab 位置
计算 scroll-view 应该横向滚动的距离(为使得选中 Tab 和 Index 能始终保持可见)
3.使用步骤
3.1基本使用
1.复制 tab-layout 组件到项目中(点击跳转至源码 TabLayout 目录)

2.自定义 Tab 与 Page 组件,并声明 item、position 和 currentIndex 三个自定义属性
3.在页面的配置文件中引用 tab-layout 、自定义的 Tab 和 Page 组件
{
"usingComponents": {
"tab-layout":"/components/tab-layout/tab-layout",
"item-page":"./item-page/item-page",
"item-tab":"./item-tab/item-tab"
}
}
4.在布局文件中使用 TabLayout 组件,并通过抽象节点 generic:item-tab 和 generic:item-page 分别与自定义的 Tab 和 Page 绑定
5.使用自定义属性 indexAreaHeight 为 Index 及其活动区域设置高度
6.使用自定义属性 tabList 设置数据源,根据数据源将自动生成对应数量的 Tab 和 Page
<tab-layout tabList="{{tabList}}" indexAreaHeight="5" generic:item-tab="item-tab" generic:item-page="item-page">
<view slot="index">
<view class="index"></view>
</view>
</tab-layout>
7.按需选择可采用插槽 slot = "index" 或自定义属性 indexStyle 设置 Index 的样式
- 采用插槽 slot = "index" 方式设置 Index 样式
布局文件 index.wxml 中:
<tab-layout tabList="{{tabList}}" indexAreaHeight="5" generic:item-tab="item-tab" generic:item-page="item-page">
<view slot="index">
<view class="index"></view>
</view>
</tab-layout>
样式文件 index.wxss 中
.index{
width: 60rpx;
height: 5rpx;
background: linear-gradient(to right, #00CFFF, #00A1FF);
border-radius: 2rpx;
}
- 采用自定义属性 indexStyle 方式设置 Index 样式
布局文件 index.wxml 中
<tab-layout tabList="{{tabList}}" indexAreaHeight="32" generic:item-tab="item-tab" generic:item-page="item-page"
indexStyle="background:#4D999999;border-radius: 10rpx;">
</tab-layout>
3.2属性说明
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
tabList | Array | [] | 是 | 数据源,根据数据源将自动生成对应 Tab 和 Page 数 |
isTabCenter | boolean | false | 否 | Tab 是否居中显示,Tab 总宽度未填满 TabLayout 时,可通过该属性讲 Tab 居中显示 |
targetIndex | number | 0 | 否 | 选中 Tab 下标,可通过该属性跳转指定 Tab 与 Page |
indexAreaHeight | number | 10 | 是 | Index 及其活动区域高度,单位为 px |
indexStyle | string | 否 | 可通过该属性设置 index 的 style | |
indexAreaStyle | string | 否 | 可通过该属性设置 index 活动区域的 style | |
isStopTouchMove | boolean | false | 否 | 是否禁止左右滑动, true 禁止 false 允许 |
isTabSpaceEqual | boolean | false | 否 | 是否根据组件宽度等分 Tab 宽度 |
pageChange | eventhandle | 否 | 页面切换监听事件 | |
index | slot(插槽) | 否 | 自定义 Index view 插槽 | |
subContent | slot(插槽) | 否 | 位于 Tab 与 Page 之间的插槽,不在 Tab/Page 内,即不会随着 Tab/Page 变动或切换 | |
item-tab | generic(抽象节点) | 是 | 插入自定义 Tab View 的抽象节点,决定 Tab 样式与内容 | |
item-page | generic(抽象节点) | 是 | 插入自定义 Page View 的抽象节点,决定 Page 样式与内容 |
PS:还有一个很重要的方法 onPageUpdata ,用于抽象节点 item-tab 和 item-page 通知父节点 tab-layout 刷新数据,在子控件中通过 this.triggerEvent("updata") 触发
4.注意事项
可能出现问题:
1. 当 item-page 中存在竖直滚动的 scroll-view 时出现滑动冲突该如何解决?
在 item-page 组件 attached 方法中按需为 scroll-view 设置固定高度或占满屏幕剩余位置(点击跳转查看参考写法)
2.当 item-page 或 item-tab 中调用 this.setData ( ) 之后,发现自定义属性 item 获取值为 null ?
出现该种情况,应在 this.setData ( ) 之后,执行 this.triggerEvent("updata") 触发 tab-layout 的 onPageUpdata 方法重新得到 item 值
3.如何实现 " 懒加载 " ,即当 Tab 首次被选中时,才进行对应 Page 的数据加载?
为自定义组件 item-page 设置一个懒加载标志位暂定为 isLoadData ,通过订阅自定义属性 currentIndex ,在 currentIndex 属性变化或组件进行到 attached 生命周期时,通过判断 isLoadData 和 currentIndex 是否与 position 相等来进行数据加载并调整标志位(点击跳转查看参考写法)
5.最后
在小程序越来越普及的现状下,如何使得小程序能给用户带来更完善的显示效果和使用体验,是每一个开发者都应该力尽其责的事。鉴于本人当前对小程序和网页端的熟悉程度,该组件或许还存在很多瑕疵,如有更好的见解或建议,欢迎留言。
- 小程序代码片段:https://developers.weixin.qq.com/s/KnxMSmmx7Yqh
- 源码及 Demo 地址:https://github.com/ziwenL/TabLayout
网友评论