u-sticky.vue 6.84 KB
   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>