uniapp项目-切换tab滚动到对应模块,滚动到某个模块选中对应tab

1,499 阅读2分钟

项目背景

在我最近参与的一个 uniapp 小程序项目中,我们需要实现一个类似于锚点定位的功能,即在首页中有多个 tab ,每个 tab 对应着不同的模块。

  • 当用户点击某个分类的 tab 时,需要滚动到对应的模块
  • 当页面滚动到某个模块时,对应分类的 tab 选中

思路

为了实现这个功能,我们可以分为以下几个步骤:

  1. 实时监听滚动并选中对应 tab
  2. 点击 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 时,我们根据 tabprop ,然后获取到要滚动到的距离

由于 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 的功能。

希望对大家有帮助!

谢谢各位大佬!

e7d3c648d1a247008deff66a2d8cea4f~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp