#
whycq
2023-10-13 ed410791a62bb5dc11d8be22c42084c88aca78a9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<template>
    <view
        class="u-sticky"
        :id="elId"
        :style="[style]"
    >
        <view
            :style="[stickyContent]"
            class="u-sticky__content"
        >
            <slot />
        </view>
    </view>
</template>
 
<script>
    import props from './props.js';;
    /**
     * sticky 吸顶
     * @description 该组件与CSS中position: sticky属性实现的效果一致,当组件达到预设的到顶部距离时, 就会固定在指定位置,组件位置大于预设的顶部距离时,会重新按照正常的布局排列。
     * @tutorial https://www.uviewui.com/components/sticky.html
     * @property {String | Number}    offsetTop        吸顶时与顶部的距离,单位px(默认 0 )
     * @property {String | Number}    customNavHeight    自定义导航栏的高度 (h5 默认44  其他默认 0 )
     * @property {Boolean}            disabled        是否开启吸顶功能 (默认 false )
     * @property {String}            bgColor            组件背景颜色(默认 '#ffffff' )
     * @property {String | Number}    zIndex            吸顶时的z-index值
     * @property {String | Number}    index            自定义标识,用于区分是哪一个组件
     * @property {Object}            customStyle        组件的样式,对象形式
     * @event {Function} fixed        组件吸顶时触发
     * @event {Function} unfixed    组件取消吸顶时触发
     * @example <u-sticky offsetTop="200"><view>塞下秋来风景异,衡阳雁去无留意</view></u-sticky>
     */
    export default {
        name: 'u-sticky',
        mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
        data() {
            return {
                cssSticky: false, // 是否使用css的sticky实现
                stickyTop: 0, // 吸顶的top值,因为可能受自定义导航栏影响,最终的吸顶值非offsetTop值
                elId: uni.$u.guid(),
                left: 0, // js模式时,吸顶的内容因为处于postition: fixed模式,为了和原来保持一致的样式,需要记录并重新设置它的left,height,width属性
                width: 'auto',
                height: 'auto',
                fixed: false, // js模式时,是否处于吸顶模式
            }
        },
        computed: {
            style() {
                const style = {}
                if(!this.disabled) {
                    if (this.cssSticky) {
                        style.position = 'sticky'
                        style.zIndex = this.uZindex
                        style.top = uni.$u.addUnit(this.stickyTop)
                    } else {
                        style.height = this.fixed ? this.height + 'px' : 'auto'
                    }
                } else {
                    // 无需吸顶时,设置会默认的relative(nvue)和非nvue的static静态模式即可
                    // #ifdef APP-NVUE
                    style.position = 'relative'
                    // #endif
                    // #ifndef APP-NVUE
                    style.position = 'static'
                    // #endif
                }
                style.backgroundColor = this.bgColor
                return uni.$u.deepMerge(uni.$u.addStyle(this.customStyle), style)
            },
            // 吸顶内容的样式
            stickyContent() {
                const style = {}
                if (!this.cssSticky) {
                    style.position = this.fixed ? 'fixed' : 'static'
                    style.top = this.stickyTop + 'px'
                    style.left = this.left + 'px'
                    style.width = this.width == 'auto' ? 'auto' : this.width + 'px'
                    style.zIndex = this.uZindex
                }
                return style
            },
            uZindex() {
                return this.zIndex ? this.zIndex : uni.$u.zIndex.sticky
            }
        },
        mounted() {
            this.init()
        },
        methods: {
            init() {
                this.getStickyTop()
                // 判断使用的模式
                this.checkSupportCssSticky()
                // 如果不支持css sticky,则使用js方案,此方案性能比不上css方案
                if (!this.cssSticky) {
                    !this.disabled && this.initObserveContent()
                }
            },
            initObserveContent() {
                // 获取吸顶内容的高度,用于在js吸顶模式时,给父元素一个填充高度,防止"塌陷"
                this.$uGetRect('#' + this.elId).then((res) => {
                    this.height = res.height
                    this.left = res.left
                    this.width = res.width
                    this.$nextTick(() => {
                        this.observeContent()
                    })
                })
            },
            observeContent() {
                // 先断掉之前的观察
                this.disconnectObserver('contentObserver')
                const contentObserver = uni.createIntersectionObserver({
                    // 检测的区间范围
                    thresholds: [0.95, 0.98, 1]
                })
                // 到屏幕顶部的高度时触发
                contentObserver.relativeToViewport({
                    top: -this.stickyTop
                })
                // 绑定观察的元素
                contentObserver.observe(`#${this.elId}`, res => {
                    this.setFixed(res.boundingClientRect.top)
                })
                this.contentObserver = contentObserver
            },
            setFixed(top) {
                // 判断是否出于吸顶条件范围
                const fixed = top <= this.stickyTop
                this.fixed = fixed
            },
            disconnectObserver(observerName) {
                // 断掉观察,释放资源
                const observer = this[observerName]
                observer && observer.disconnect()
            },
            getStickyTop() {
                this.stickyTop = uni.$u.getPx(this.offsetTop) + uni.$u.getPx(this.customNavHeight)
            },
            async checkSupportCssSticky() {
                // #ifdef H5
                // H5,一般都是现代浏览器,是支持css sticky的,这里使用创建元素嗅探的形式判断
                if (this.checkCssStickyForH5()) {
                    this.cssSticky = true
                }
                // #endif
 
                // 如果安卓版本高于8.0,依然认为是支持css sticky的(因为安卓7在某些机型,可能不支持sticky)
                if (uni.$u.os() === 'android' && Number(uni.$u.sys().system) > 8) {
                    this.cssSticky = true
                }
 
                // APP-Vue和微信平台,通过computedStyle判断是否支持css sticky
                // #ifdef APP-VUE || MP-WEIXIN
                this.cssSticky = await this.checkComputedStyle()
                // #endif
 
                // ios上,从ios6开始,都是支持css sticky的
                if (uni.$u.os() === 'ios') {
                    this.cssSticky = true
                }
 
                // nvue,是支持css sticky的
                // #ifdef APP-NVUE
                this.cssSticky = true
                // #endif
            },
            // 在APP和微信小程序上,通过uni.createSelectorQuery可以判断是否支持css sticky
            checkComputedStyle() {
                // 方法内进行判断,避免在其他平台生成无用代码
                // #ifdef APP-VUE || MP-WEIXIN
                return new Promise(resolve => {
                    uni.createSelectorQuery().in(this).select('.u-sticky').fields({
                        computedStyle: ["position"]
                    }).exec(e => {
                        resolve('sticky' === e[0].position)
                    })
                })
                // #endif
            },
            // H5通过创建元素的形式嗅探是否支持css sticky
            // 判断浏览器是否支持sticky属性
            checkCssStickyForH5() {
                // 方法内进行判断,避免在其他平台生成无用代码
                // #ifdef H5
                const vendorList = ['', '-webkit-', '-ms-', '-moz-', '-o-'],
                    vendorListLength = vendorList.length,
                    stickyElement = document.createElement('div')
                for (let i = 0; i < vendorListLength; i++) {
                    stickyElement.style.position = vendorList[i] + 'sticky'
                    if (stickyElement.style.position !== '') {
                        return true
                    }
                }
                return false;
                // #endif
            }
        },
        beforeDestroy() {
            this.disconnectObserver('contentObserver')
        }
    }
</script>
 
<style lang="scss" scoped>
    .u-sticky {
        /* #ifdef APP-VUE || MP-WEIXIN */
        // 此处默认写sticky属性,是为了给微信和APP通过uni.createSelectorQuery查询是否支持css sticky使用
        position: sticky;
        /* #endif */
    }
</style>