uniapp实现tab分类导航以及滚动联动效果


效果图

tab切换效果

思路

  • 失败尝试:顶部tab使用scrollview的scroll-left来实现自动横向滚动,内容区域使用scrollview的scroll-into-view,这个属性虽然可以快速实现点击以后锚点定位的效果,但是内容区域滚动导致tab选中效果的相互影响。
  • 正确思路:顶部的tab依然使用scrollview的scroll-left来实现自动横向滚动,内容区域scroll-top以及scroll函数里面返回当前滚动条距离顶部来判断在哪个分类区间。

Tab部分

html

<template>
  <view class="menu_page">
	<scroll-view
      class="tabbar_scroll"
      :style="{
        height: tabHeight + 'px',
      }"
      scroll-x
      scroll-with-animation
      :scroll-left="scrollLeft"
    >
      <view class="tab_wrap">
        <view
          class="tab_item"
          :style="{ width: tabWidth + 'px', height: tabHeight + 'px' }"
          :class="{ active: activeIndex === index }"
          @click="handleTabClick(index)"
          v-for="(item, index) in tabs"
          >{{ item }}
        </view>
        <view class="tab_line" :style="{ left: lineLeft }"></view>
      </view>
    </scroll-view>
  </view>
</template>

js

import { ref, computed, getCurrentInstance, onMounted } from "vue";

const tabs = ["关注", "推荐", "视频", "图文", "短剧", "直播", "游戏"];

const { windowWidth } = uni.getSystemInfoSync();

const activeIndex = ref(0); // 当前激活的选项卡

const tabWidth = uni.upx2px(150); // 选项卡宽度

const tabHeight = uni.upx2px(64); // 选项卡高度

const linewidth = uni.upx2px(40); // 下划线宽度

let tabDomList = []; // 选项卡节点信息

const scrollRightTop = ref(0); // 右侧内容滚动距离

const isTabClick = ref(false); // 是否点击选项卡

这里tab的高度和宽度因为需要参与后面的计算,所以通过uni的内置方法将rpx转成px。
这里的tab注意是用的display: inline-block来横向排列。

scrollLeft

tab点击以后判断当前激活项是否超过屏幕一半,超过则需要自动将激活菜单居中显示,自动向左向右滚动的效果。

const scrollLeft = computed(() => {
  const tabLeft = tabWidth * activeIndex.value + tabWidth / 2;
  return tabLeft - windowWidth / 2;
});

下划线

下划线选中效果,使用的定位left,需要计算距离起始位置的宽度。应该还是比较好理解,当前选中项中间减去下划线本身的宽度。

const lineLeft = computed(() => {
  // 减掉下划线一半的宽度
  return tabWidth * activeIndex.value + tabWidth / 2 - linewidth / 2 + "px";
});

css

.menu_page {
  height: 100%;
}

.tabbar_scroll {
  position: fixed;
  left: 0;
  top: var(--windowTop);
  width: 100%;
  background-color: #fff;
  z-index: 3;
  box-shadow: inset 0 -1px 0 0 #f0f0f0;
}

.tab_wrap {
  position: relative;
  white-space: nowrap;

  .tab_item {
    display: inline-block;
    text-align: center;

    &.active {
      font-weight: 600;
      color: #1a1a1a;
    }
  }

  .tab_line {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 40rpx;
    height: 6rpx;
    border-radius: 4rpx;
    background-color: red;
    transition: left 0.3s;
  }
}

Tab内容区域

tab使用固定定位在最上方,内容区域padding填充这部分高度。

<scroll-view
      class="menu_scroll"
      :style="{ paddingTop: tabHeight + 'px' }"
      scroll-with-animation
      scroll-y
      :scroll-top="scrollRightTop"
      @scroll="handleScroll"
    >
      <view
        class="menu_item"
        v-for="(item, index) in tabs"
        :key="item"
        ><view class="menu_title">{{ item }}</view>
        <view
          >Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorem
          odio natus ipsam voluptatibus unde tenetur, praesentium dolores
          tempore aperiam? Voluptas, blanditiis suscipit. Autem cumque eaque,
          facilis adipisci fugiat inventore iste.
        </view>
      </view>
    </scroll-view>
.menu_scroll {
  height: 100%;
  box-sizing: border-box;

  .menu_item {
    padding: 20rpx;
  }
  .menu_title{
    font-weight: 600;
    padding: 10rpx 0;
  }
}
  1. 页面初始化,获取所有tab分类item距离顶部的高度。
    注意这里需要将第一个元素的top减掉,第一个的top包含了padding
onMounted(() => {
  getMenuItemTop();
});

const getMenuItemTop = () => {
  return new Promise((resolve) => {
    const vm = getCurrentInstance();
    const query = uni.createSelectorQuery().in(vm);
    query
      .selectAll(".menu_item")
      .boundingClientRect((data) => {
        if (Array.isArray(data) && data.length > 0) {
          const data0 = data[0];
          tabDomList = data.map((v) => v.top - data0.top);
          resolve();
        }
      })
      .exec();
  });
};
  1. 点击tab的函数
    赋值当前选中项的距顶部的top
 const handleTabClick = (index) => {
  if (activeIndex.value === index) return;
  scrollRightTop.value = tabDomList[index];
  leftMenuStatus(index);
};

const leftMenuStatus = (index) => {
  activeIndex.value = index;
};
  1. 分类滚动的handleScroll
    偏移量根据情况也可不用加

const sleep = (ms=30) => {
  return new Promise((resolve) => {
    const timer = setTimeout(() => {
      clearTimeout(timer);
      resolve();
    }, ms);
  });
};

const handleScroll = async ({ detail: { scrollTop } }) => {
  if (!tabDomList.length) {
    await getMenuItemTop();
  }
  await sleep(10);
  const scrollHeight = scrollTop + 3; // 加偏移量
  for (let i = 0; i < tabDomList.length; i++) {
    const height = tabDomList[i],
      nextHeight = tabDomList[i + 1];
    if (!nextHeight || (scrollHeight >= height && scrollHeight < nextHeight)) {
      leftMenuStatus(i);
      return;
    }
  }
};

存在的问题

当前分类tab的内容较少的时候,从第一个点到最后一个,tab会先激活最后一个,然后触发handleScroll函数将正确scrollTop传入过来,这时候的index将会出现突然闪到前面的某一个tab上。
原因已经找到了,那么解决的办法只需要对tab点击的时候,触发handleScroll阻止index的改变就行了。

const handleTabClick = (index) => {
  if (activeIndex.value === index) return;
  isTabClick.value = true;
  scrollRightTop.value = tabDomList[index];
  leftMenuStatus(index);
};

const handleScroll = async ({ detail: { scrollTop } }) => {
  if(isTabClick.value) {
    return isTabClick.value = false;
  }
  // code
  // code
  // code
};

总结

  • 以上也可以将其改造成左右菜单联动滚动,思路也是差不多。
  • 代码是全部已经贴出来了,可自行拼装一下。
  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值