小程序实现贴近原生端的 TabLayout + ViewPager 切换效果

目录

前言

1.需求分析

2.具体实现

2.1效果展示

2.1.1常见样式

 2.1.2 Tab 宽度等分

2.1.3 Page 内容不一致

2.1.4 Index 覆盖 Tab

2.2实现步骤

2.2.1布局分析

2.2.2功能实现

3.使用步骤

3.1基本使用

3.2属性说明 

4.注意事项

5.最后


前言

最近被分配到做项目小程序端的任务,做到原生端常见的 TabLayout + ViewPager 实现的 Tab 切换页面时,发现小程序未提供类似可以直接使用的 TabLayout 组件。

网上搜寻发现小程序端需要实现 Tab 切换效果,多是通过 scroll-view 和 swiper 联动实现,并且 指示器 没有过渡效果,多是闪现跳到下一个 Tab 。因此自行研究实现接近原生端的 TabLayout 组件。

1.需求分析

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

小程序 小米Lite Tab切换效果

原生端 TabLayout 常见的功能就是我们的目标效果,所以 目标效果如下

  • 指示器(Index)具有切换过渡效果;
  • 指示器(Index)需可自定义,常见的有:可固定宽度、可与 Tab 内容等宽、可覆盖在 Tab 上;
  • Tab 可自定义、可支持自动适应父控件宽度等分 Tab 宽度;
  • 当 Tab 总宽度超出父控件宽度时,Tab 行支持滚动且切换 Page 时保证当前 Tab 可见;
  • 支持 Page 切换监听

通过了解小程序组件及技术支持,选定通过 scroll-view swiperswiper-itemmovable-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.3效果演示 -- Page 内容不一致

 包含特点:

  • 根据 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>
    </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

booleanfalse

是否禁止左右滑动, true 禁止 false 允许

isTabSpaceEqual

boolean

false

是否根据组件宽度等分 Tab 宽度

isSetIndexPositionAbsolute

booleanfalse

是否设置 Index 悬浮在 Tab 上

pageChangeeventhandle 页面切换监听事件

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

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

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值