项目背景
在我最近参与的一个 uniapp
小程序项目中,我们需要实现一个类似于锚点定位的功能,即在首页中有多个 tab
,每个 tab
对应着不同的模块。
- 当用户点击某个分类的
tab
时,需要滚动到对应的模块 - 当页面滚动到某个模块时,对应分类的
tab
选中
思路
为了实现这个功能,我们可以分为以下几个步骤:
- 实时监听滚动并选中对应
tab
- 点击
tab
跳转至当前模块
实现
1. 实时监听滚动并选中对应 tab
在滚动时实时通过 boundingClientRect
获取模块相对于窗口的值
// uiapp提供的页面级别的方法
onPageScroll(res) {
//记录当前页面的滚动距离
this.scrollTop = res.scrollTop;
//如果当前的页面滚动是由切换tab触发,那么return出去,防止互相冲突
if (this.isScrollByTab) return;
// 获取各个 tab 的位置信息
const query = uni.createSelectorQuery().in(this);
query.selectAll('.item')
.boundingClientRect((rects) => {
//实时获取多个模块距离的距离,使用findLastIndex找到当前的tab
const index = findLastIndex(rects, (rect) => rect.top < 80);
if (index > -1) {
const activeRect = rects[index];
//这里模块的id和tab的prop一致
this.activeTab = activeRect.id;
}
}).exec();
},
通过滚动时实时获取,避免了获取 top
值不准确的问题,之后判断距离当前窗口的距离,小于临界值则选中对应的tab
为什么实时获取,而不是预先获取呢?
在onReady
钩子函数中获取多个模块的距离当前视口的 top
值,在初始情况(页面没有进行滚动过)下,该模块的 top
值等于该模块的 scrollTop
值
onReady() {
console.log('onReady')
//记录页面每个模块的位置信息
this.$nextTick(() => {
// 获取各个 tab 的位置信息
const query = uni.createSelectorQuery().in(this);
query.selectAll('.item')
.boundingClientRect((rects) => {
this.tabPositions = rects.map((rect) => rect.top);
}).exec();
})
},
但是这里有一个致命的问题,就是通过 boundingClientRect
获取的值不一定准确,因为页面需要请求接口获取数据的,很有可能在 onReady
方法中去获取的时候,页面还没有渲染完成,那么此时获取的 top
值一定是错误的。查看更多
2. 点击 tab
跳转至当前模块
当用户点击某个 tab
时,我们根据 tab
的 prop
,然后获取到要滚动到的距离
由于 boundingClientRect
获取的 top
值是相对于视口的,所以实际上页面需要滚动的距离为相对于视口的距离加上当前页面的滚动距离,即 rect.top + scrollTop
//切换tab触发滚动
switchTab(tabProp) {
this.isScrollByTab = true;
// 切换 tab
this.activeTab = tabProp;
//获取各个 tab 的位置信息
const query = uni.createSelectorQuery().in(this);
queryselect(`#${tabProp}`)
.boundingClientRect((rect) => {
// 滚动到当前模块
this.$nextTick(() => {
uni.pageScrollTo({
//页面需要滚动的距离为相对于视口的距离加上当前页面的滚动距离
scrollTop: rect.top + this.scrollTop - 48,
duration: 300,
});
//这里在滚动完成之后将isScrollByTab重置为false,故意在大于300ms之后
const timer = setTimeout(() => {
this.isScrollByTab = false;
clearTimeout(timer);
}, 500);
});
}).exec();
},
全部代码
<template>
<view class="container">
<view class="tabs">
<view :class="['tab', activeTab === tab.prop ? 'active' : '']" v-for="tab in tabList" :key="tab.prop" @tap="switchTab(tab.prop)">{{ tab.title }}</view>
</view>
<view class="list">
<view class="item" v-for="(tab, index) in tabList" :id="tab.prop" :key="'detail' + tab.prop">模块{{ index + 1 }},对应的是{{ tab.title }}</view>
</view>
</view>
</template>
<script>
//这里使用到了lodash中的findLastIndex
import { findLastIndex } from 'lodash-es';
export default {
data() {
return {
//生成tabs的假数据
tabList: [
{ title: 'tab1', prop: 'first' },
{ title: 'tab2', prop: 'second' },
{ title: 'tab3', prop: 'third' },
{ title: 'tab4', prop: 'four' },
{ title: 'tab5', prop: 'five' },
{ title: 'tab6', prop: 'six' },
],
//记录滚动的距离,用在切换tab页面滚动逻辑中
scrollTop: 0,
//当前选中的tab
activeTab: 'first',
//当前的页面滚动是否由切换tab触发
isScrollByTab: false,
};
},
//uniapp提供的页面级别的方法
onPageScroll(res) {
//记录当前页面的滚动距离
this.scrollTop = res.scrollTop;
//如果当前的页面滚动是由切换tab触发,那么return出去,防止互相冲突
if (this.isScrollByTab) return;
// 获取各个 tab 的位置信息
const query = uni.createSelectorQuery().in(this);
query.selectAll('.item')
.boundingClientRect((rects) => {
//实时获取多个模块距离的距离,使用findLastIndex找到当前的tab
const index = findLastIndex(rects, (rect) => rect.top < 80);
if (index > -1) {
const activeRect = rects[index];
//这里模块的id和tab的prop一致
this.activeTab = activeRect.id;
}
}).exec();
},
methods: {
//切换tab触发滚动
switchTab(tabProp) {
this.isScrollByTab = true;
// 切换 tab
this.activeTab = tabProp;
//获取各个 tab 的位置信息
const query = uni.createSelectorQuery().in(this);
queryselect(`#${tabProp}`)
.boundingClientRect((rect) => {
// 滚动到当前模块
this.$nextTick(() => {
uni.pageScrollTo({
//页面需要滚动的距离为相对于视口的距离加上当前页面的滚动距离
scrollTop: rect.top + this.scrollTop - 80,
duration: 300,
});
//这里在滚动完成之后将isScrollByTab重置为false,故意在大于300ms之后
const timer = setTimeout(() => {
this.isScrollByTab = false;
clearTimeout(timer);
}, 500);
});
}).exec();
},
},
};
</script>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
.tabs {
display: flex;
align-items: center;
width: 100%;
height: 100rpx;
min-height: 100rpx;
position: fixed;
left: 0;
top: 0;
background: pink;
.tab {
flex: 1;
color: #fff;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.active {
color: skyblue;
}
}
}
.list {
margin-top: 100rpx;
.item {
border: 1px solid #dedede;
width: 100%;
height: 300rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
总结
这篇文章介绍了如何在一个 uniapp
小程序项目中实现点击 tab
跳转到对应模块,并滚动到某个模块选中对应 tab
的功能。
希望对大家有帮助!
谢谢各位大佬!