微信小程序实现 TabLayout 并带有过渡效果

前言

  最近被分配到做项目小程序端的任务,做到原生端常见的 TabLayout + ViewPager 实现的 Tab 切换页面时,发现小程序未提供类似可以直接使用的 TabLayout 组件。
  网上搜寻发现小程序端需要实现 Tab 切换效果,多是通过 scroll-viewswiper 联动实现,并且 指示器 没有过渡效果,多是闪现跳到下一个 Tab 。因此自行研究实现接近原生端的 tab-layout 组件。

1.需求分析

  下图是小程序 小米Lite 的 Tab 切换效果:切换 Page 时,Tab 下方的指示器(红色横条)是没有滚动效果而是直接闪现到下一个 Page 的,这也是市面上小程序常见的 Tab 切换效果。

小程序 小米Lite 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常见样式

2.1.1效果演示 -- 常见样式

包含特点

  • Page 懒加载;
  • Page 切换时 Tab 自动跟随滚动;
  • Page 内 scroll-view 的滚动处理;

2.1.2 Tab 宽度等分

2.1.2效果演示 -- Tab 宽度等分

包含特点

  • Tab 宽度等分组件宽度;
  • 跳转指定 Position = 2 ;
  • 监听页面切换;

2.1.3 Page 内容不一致

2.1.2效果演示 -- Tab 宽度等分

包含特点

  • 根据 Position 变化调整 Page 布局;
  • Index 宽度与 Tab 宽度保持一致;
  • Tab 总宽度未填满 TabLayout 时居中显示;

2.1.4 Index 覆盖 Tab

2.1.4效果演示 -- Index 覆盖 Tab

包含特点

  • Index 悬浮覆盖在 Tab 上;
  • Tab 与 Index 之间插入固定 View
  • Page 禁止左右滑动;

2.2实现步骤

2.2.1布局分析

  1. 使用可横向滑动的 scroll-view 作为 Tab 和 指示器(Index)的容器
  2. Tab 的具体内容与样式,由抽象节点 item-tab 决定
  3. Index 滑动区域由 movable-area 实现,长度应与 Tab 栏总宽度一致,高度由自定义属性 indexAreaHeight 赋值决定
  4. Index 的展示区域由 movable-view 实现,并提供插槽 slot name="index" 让用户可自定义 Index 样式(用户也可采用自定义属性 indexStyle 来直接设置 movable-view 的 style 从而实现 Index 的样式).
  5. 各 Tab 对应的 Page,由被 swiper 包裹的 swiper-item 充当容器,通过抽象节点 item-page 决定内容

具体如下图所示 :

2.2自定义组件布局分析
自定义组件 TabLayout 的布局代码(点击跳转查看源码)

<!--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功能实现

  1. 在组件生命周期来到 attached 方法时:
     获取所有 Tab 的宽度并记录,用设置 Index 的显示区域长度及活动区域长度
     通过自定属性 targetIndex 判断是否需要进行页面切换跳转指定 page
  2. 通过监听 Tab 的点击事件,促使 swiper 切换页面;
  3. 通过 swiper 组件的 bindchange 方法,监听页面切换事件;
  4. 在页面切换的时候:
     计算并移动 Index(movable-view)到指定 Tab 位置
     计算 scroll-view 应该横向滚动的距离(为使得选中 Tab 和 Index 能始终保持可见)

主要实现(点击跳转查看源码)

3.使用步骤

3.1基本使用

  1.复制 tab-layout 组件到项目中(点击跳转至源码 TabLayout 目录)

3.1.1使用步骤 -- 复制组件源码

  2.自定义 Tab 与 Page 组件,并声明 item、position 和 currentIndex 三个自定义属性
3.1.2使用步骤 -- 自定义组件 Tab 和 Page

  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.最后

  在小程序越来越普及的现状下,如何使得小程序能给用户带来更完善的显示效果和使用体验,是每一个开发者都应该力尽其责的事。鉴于本人当前对小程序和网页端的熟悉程度,该组件或许还存在很多瑕疵,如有更好的见解或建议,欢迎留言。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容