开箱即用 小程序列表吸顶组件

2,013 阅读4分钟

前言

去年这笔记就开了头,结果摸鱼摸的太快乐忘了这茬...言归正传。最初想到用onPageScroll这个API来实现,结果效果并不理想,单个吸顶效果尚可,但列表吸顶会有明显卡顿,用户体验较差。抓了抓狗头,开启骚操作,到社区、搜狗(why不是度娘和股沟,毕竟小程序是tencent家的,当然用搜狗了)一顿狂搜,果然站在大佬们的肩膀上看的更远,以下代码是uniapp版本。

列表(伪)吸顶实现

原理是监听目标节点与参照节点的相交情况对吸顶模块做显示隐藏,所以叫(伪)吸顶吧。

文章实现了列表吸顶与列表折叠效果。

核心API

wx.createSelectorQuery()

创建一个SelectorQuery 选择器,用来查询节点信息

SelectorQuery.select(string selector)

用于获取当前页面第一个匹配selector的节点

SelectorQuery.selectAll(string selector)

用于获取当前页面所有selector节点

wx.createIntersectionObserver(Object component, Object options)

创建并返回一个 IntersectionObserver 对象实例,它和web端 IntersectionObserver 这个API相对应

IntersectionObserver.relativeTo(string selector, Object margins)

指定一个选择器作为参照节点

IntersectionObserver.observe(string targetSelector, function callback)

用于监听指定目标节点相交状态变化情况

获取目标节点

通过createSelectorQuery()创建选择器,利用selectAll获取页面全部目标元素,注意自定义组件用this.createSelectorQuery()来创建,否则获取不到。

targetElmEvent(targetElm) {
    const me = this
    return new Promise((resolve, reject) => {
        // 创建选择器 获取监听数组
        me.createSelectorQuery().selectAll(targetElm).boundingClientRect(function(res) {
	    // 此处会出现找不到元素的情况,此处做了简单处理
            // id要与元素节点上的id一致。对id做了处理,貌似数字id会有问题
            if(res.length < 1) {
                const tip = JSON.parse(JSON.stringify(me.list))
                tip.map(v=> v.id=`tip${v.id}`)
                me.targetList = tip
            }else {
                me.targetList = res
            }
            resolve(res)
        }).exec()
    })
}

设置监听器

通过wx.createIntersectionObserver()来创建一个监听器,传入当前自定义组件实例this,设置用于同时监测多个节点的属性observeAll,注意节点过多会消耗性能。通过relativeTo设置参照物,使用observe监听目标节点与参照物的相交状态变化。

observerEvent(targetElm, relativeElm) {
    const me = this;
    this.targetElmEvent(targetElm).then(res => {
        // 创建观察者 设置observeAll用于同时监测多个节点
        const observer = wx.createIntersectionObserver(this, {observeAll: true})
        // 设置参照物并监听指定目标
        observer.relativeTo(relativeElm).observe(targetElm, res => {
            // intersectionRatio为相交比例 boundingClientRect为目标边界
            const {id, intersectionRatio, boundingClientRect} = res
            const {top, bottom, height} = boundingClientRect
            // 此处通过 intersectionRatio 来判断是否超过相交边界
            if (intersectionRatio > 0) {
                // 赋值吸顶数据
                me.fixedData = me.list.filter(v=>id.includes(v.id))[0]
	            me.fixedShow = true
            }else {
                // 对目标首尾项的上边界做特殊处理 清空吸顶数据
                if((id === me.targetList[0].id && top >= 0) || (id === me.targetList[me.targetList.length-1].id && top <= 0)) {
                    me.fixedShow = false
                    me.fixedData = {}
                }
            }
        })
    })
}

附加小功能 - 折叠

通过对每条数据设置show属性来控制折叠效果。

stackToggleEvent(item) {
    const me = this
    let count = 0
    me.list.map((v, i)=> {
        if(v.id===item.id) {
            v.show=!v.show
        }
        if(!v.show) {
            count+=1
        }
        if(count === me.list.length) {
            me.fixedData = {}
            me.fixedShow = false
        }
    })
}

以下是全部代码

<template>
    <div>
        <!-- 参照物节点 -->
        <div class="relative"></div>
        <!-- 伪吸顶模块 -->
        <div class="stack-top st-fixed" @click="stackToggleEvent(fixedData)" v-if="fixedShow">{{fixedData.title}}</div>
        <!-- 数据列表 -->
        <ul v-if="list.length > 0">
        <!-- 监听目标 -->
            <li class="targetTag" :id="`tip${item.id}`" v-for="item of list" :key="item.id"> 
                <div class="stack-top" @click="stackToggleEvent(item)">{{item.title}}</div>
                <div class="stack-ct" v-if="item.show">
                    <ul :class="'foldTag'+item.id">
                        <li class="sc-k" v-for="child in item.children" :key="child.id">
                            <div>{{child.name}}</div>
                        </li>
                    </ul>
                </div>
            </li>
        </ul>
    <!-- 测试超出范围 -->
        <div class="overflow"></div>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                fixedShow: false, // 是否显示吸顶模块
                targetList: [], // 监听目标
                fixedData: {}, // 吸顶模块数据
                list: [] // 数据源
            }
        },
        onLoad() {
      // 测试数据
      // show属性用于折叠功能
            this.list = [
                {
                    id: 1,
                    title: '第一部分',
                    show: false,
                    children: [
                        {
                            id: 11,
                            name: '1 - 1'
                        },
                        {
                            id: 12,
                            name: '1 - 2'
                        },
                        {
                            id: 13,
                            name: '1 - 3'
                        },
                        {
                            id: 14,
                            name: '1 - 4'
                        },
                        {
                            id: 15,
                            name: '1 - 5'
                        },
                    ]
                },
                {
                    id: 2,
                    title: '第二部分',
                    show: false,
                    children: [
                        {
                            id: 21,
                            name: '2 - 1'
                        },
                        {
                            id: 22,
                            name: '2 - 2'
                        },
                        {
                            id: 23,
                            name: '2 - 3'
                        },
                        {
                            id: 24,
                            name: '2 - 4'
                        },
                        {
                            id: 25,
                            name: '2 - 5'
                        },
                        {
                            id: 26,
                            name: '2 - 6'
                        },
                    ]
                },
                {
                    id: 3,
                    title: '第三部分',
                    show: false,
                    children: [
                        {
                            id: 31,
                            name: '3 - 1'
                        },
                        {
                            id: 32,
                            name: '3 - 2'
                        },
                        {
                            id: 33,
                            name: '3 - 3'
                        },
                        {
                            id: 34,
                            name: '3 - 4'
                        },
                        {
                            id: 35,
                            name: '3 - 5'
                        },
                        {
                            id: 36,
                            name: '3 - 6'
                        },
                        {
                            id: 37,
                            name: '3 - 7'
                        },
                        {
                            id: 38,
                            name: '3 - 8'
                        },
                        {
                            id: 39,
                            name: '3 - 9'
                        },
                        {
                            id: 391,
                            name: '3 - 9 - 1'
                        },
                        {
                            id: 392,
                            name: '3 - 9 - 2'
                        },
                    ]
                }
            ]
        },
        mounted(){
            const me = this
            // 启动吸顶功能,传入目标节点与参照节点
            me.observerEvent('.targetTag', '.relative')
        },
        methods: {
            // 实现折叠功能并处理展开隐藏时吸顶变化
            stackToggleEvent(item) {
                const me = this
                let count = 0
                me.list.map((v, i)=> {
                    if(v.id===item.id) {
                        v.show=!v.show
                    }
                    if(!v.show) {
                        count+=1
                    }
                    if(count === me.list.length) {
                        me.fixedData = {}
                        me.fixedShow = false
                    }
                })
            },
            // 获取监听目标 返回promise
            targetElmEvent(targetElm) {
                const me = this
                return new Promise((resolve, reject) => {
                    // 创建选择器 获取监听数组
                    me.createSelectorQuery().selectAll(targetElm).boundingClientRect(function(res) {
                        // 此处会出现找不到元素的情况,此处做了简单处理
                        // id要与元素节点上的id一致。对id做了处理,貌似数字id会有问题
                        if(res.length < 1) {
                            const tip = JSON.parse(JSON.stringify(me.list))
                            tip.map(v=> v.id=`tip${v.id}`)
                            me.targetList = tip
                        }else {
                            me.targetList = res
                        }
                        resolve(res)
                    }).exec()
                })
            },
            // 监听目标 判断边界 处理核心逻辑
            observerEvent(targetElm, relativeElm) {
                const me = this;
                this.targetElmEvent(targetElm).then(res => {
                    // 创建观察者 设置observeAll用于同时监测多个节点,注意节点过多会消耗性能
                    const observer = wx.createIntersectionObserver(this, {observeAll: true})
                    // 设置参照物并监听指定目标
                    observer.relativeTo(relativeElm).observe(targetElm, res => {
                        // intersectionRatio为相交比例 boundingClientRect为目标边界
                        const {id, intersectionRatio, boundingClientRect} = res
                        const {top, bottom, height} = boundingClientRect
                        // 此处通过 intersectionRatio 来判断是否超过相交边界
                        if (intersectionRatio > 0) {
                            // 赋值吸顶数据
                            me.fixedData = me.list.filter(v=>id.includes(v.id))[0]
                            me.fixedShow = true
                        }else {
                            // 对目标首尾的上边界做特殊处理 清空吸顶数据
                            if((id === me.targetList[0].id && top >= 0) || (id === me.targetList[me.targetList.length-1].id && top <= 0)) {
                                me.fixedShow = false
                                me.fixedData = {}
                            }
                        }
                    })
                })
            }
        }
    }
</script>
<style lang="scss" scoped>
    .relative {
        position: fixed;
        top: -2rpx;
        left: 0;
        height: 2rpx;
        width: 100vw;
        opacity: 0;
    }
    
    .st-fixed {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        z-index: 10;
        box-sizing: border-box;
        background-color: #ff7a52 !important;
    }
    
    .stack-top {
        @include _flex(space-between);
        padding: 20rpx 30rpx;
        background-color: #6f9dff;
        
        .deg180c {
            transform: rotate(180deg);
        }
    }
    
    .stack-ct {
        padding: 0 30rpx;
        text-align: right;
        
        .sc-k {
            position: relative;
            display: inline-block;
            width: 100%;
            padding: 30rpx;
            margin-bottom: 60rpx;
            box-sizing: border-box;
            box-shadow:0 12rpx 36rpx 0 rgba(31,66,209,0.1);
            border-radius:8rpx;
            border:2rpx solid rgba(235,238,255,1);
            
            &:first-child {
                margin-top: 10rpx;
            }
            
            &:last-child {
                .line {
                    opacity: 0;
                }
            }
        }
        
        .sk-top {
            @include _flex(flex-start, flex-start, flex-start);
            
            .st-time {
                font-size:24rpx;
                color:rgba(2,0,18,1);
            }
        }
    }
  .overflow {
        height: 200vh; 
        width: 100vw;
        background-color: red;
    }
</style>

结语

若有不对的地方,请轻点指教。