效果图
思路
- 失败尝试:顶部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;
}
}
- 页面初始化,获取所有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();
});
};
- 点击tab的函数
赋值当前选中项的距顶部的top
const handleTabClick = (index) => {
if (activeIndex.value === index) return;
scrollRightTop.value = tabDomList[index];
leftMenuStatus(index);
};
const leftMenuStatus = (index) => {
activeIndex.value = index;
};
- 分类滚动的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
};
总结
- 以上也可以将其改造成左右菜单联动滚动,思路也是差不多。
- 代码是全部已经贴出来了,可自行拼装一下。