版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shelbyandfxj/article/details/83026088
参考自:源生js惯性滚动与回弹效果
参考了上面的博客实现了惯性滚动和回弹效果,并且实现了点击列表内容时,当不完全在可视区域,上移或下移至可视区域。
在这里,需要注意的是一个this指向的问题(在mleave和ease方法中);
在匿名函数中,this指向的是windows,为了让this始终指向当前对象而不是全局对象,这里将this赋值给一个变量再在匿名函数中使用该变量;
而当在函数作为某个对象的方法调动时,此时的this指向的是调用该函数的对象。
<template>
<div v-if="visiable">
<div class="transparent" :class="{active:resultPanelStatus==='top'}"></div>
<div class="mapbox-result"
ref="resultPanel"
style="z-index: 101;"
:style="slideEffect"
>
<div class="mapbox-result-content">
<a class="mapbox-result-close" v-if="closable" @click="close"></a>
<div class="mapbox-result-header">
<!--<div class="touch_line"></div>-->
<slot name="header">
<div class="mapbox-result-header-title">
共找到【<div class="mapbox-result-header-poi">{{header}}</div>】相关{{total}}个结果
</div>
</slot>
</div>
<div
class="mapbox-result-scroll-hidden"
>
<div
class="mapbox-result-wrap"
ref="resultWrap"
>
<div
class="mapbox-result-body"
id="resultBody"
ref="resultBody"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
:style="resultBodyStyle"
>
<result-option
v-for="(item, index) in data"
:index="index+1"
:name="item.name"
:meter="item.meter?item.meter:0"
:floor-name="item.floorName"
:key="index"
v-show="visiable"
@on-click-gohere="handleNavigate(index)"
@on-click-item="focusResultOnMap(index)"
></result-option>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import resultOption from './resultOption';
let offset = 50; // 最大溢出值
let cur = 0; // 列表滑动位置
let isDown = false;
let vy = 0; // 滑动的力度
let fl = 150; // 弹力,值越大,到底或到顶后,可以继续拉的越远
let isInTransition = false; // 是否在滚动中
export default {
name: 'result-panel',
components: {resultOption},
props: {
header: {
type: String
},
// value: {
// type: Boolean,
// default: true
// },
closable: {
type: Boolean,
default: true
},
data: {
type: Array,
default: []
}
},
data() {
return {
// visiable: true,
resultPanelStatus: 'normal', //'normal'、'top'
cloneData: this.deepCopy(this.data),
startY: 0, // 开始触摸屏幕的点,会变动,用于滑动计算
sY: 0, // 开始触摸屏幕点,不会变动,用于判断是否是点击还是滑动
endY: 0, // 离开屏幕的点
moveY: 0, // 滑动时的距离
disY: 0, // 移动距离
slideEffect: '', // 滑动效果
timer: null,
resultBodyStyle: '',
top: false,
startTime: '', // 初始点击时的时间戳
oh: 0, // 列表的高度
ch: 0 // 容器的高度
}
},
mounted() {
// this.$refs.resultWrap.style.height = `${this.defaultHeight - 50}px`;
},
computed: {
total() {
return this.data.length;
},
defaultHeight() {
return this.data.length > 3 ? 254 : this.data.length * 60 + 50
},
visiable() {
this.resultPanelStatus = 'normal';
this.slideEffect = `transform: translateY(-${this.defaultHeight}px); transition: all .5s`;
return this.$store.state.resultPanel.show;
}
},
methods: {
/**
* 根据手指来滚动,会触发click延时300ms的问题,导致关闭结果列表面板时,立即点击另一个poi结果列表,导致此时的scroll滑动到上一次的位置,且滑动时也会移到上一次滑动停止的位置
*/
touchStart (ev) {
ev = ev || event;
if (isInTransition || ev.targetTouches.length > 1) return;
// ev.preventDefault();
this.startY = ev.targetTouches[0].clientY; // 点击的初始位置
this.sY = ev.targetTouches[0].clientY; // 点击的初始位置, 点击时使用
clearInterval(this.timer); // 清除定时器
vy = 0;
this.disY = ev.targetTouches[0].clientY - cur; // 计算点击位置与列表当前位置的差值,列表位置初始值为0
this.startTime = ev.timeStamp;
/**
* overflow:hidden 导致scrollHeight和clientHeight 相等 解决:把容器高度写死, 结果列表大于3,则为204,否则内容高度即为容器高度
*/
this.oh = this.$refs.resultWrap.scrollHeight; // 内容的高度
this.ch = this.total > 3 ? 204 : this.$refs.resultWrap.clientHeight; // 容器的高度
// console.log("this.$refs.resultWrap.style: ", this.$refs.resultWrap.style);
isDown = true;
},
touchMove (ev) {
ev = ev || event;
if (ev.targetTouches.length > 1) return;
if (Math.abs(ev.targetTouches[0].clientY - this.sY) < 5) return;
if (isDown) {
if (ev.timeStamp - this.startTime > 40) { // 如果是慢速滑动,则不会产生力度,内容是根据手指一动的
this.startTime = ev.timeStamp; // 慢速滑动不会产生力度,所以需要实时更新时间戳
cur = ev.targetTouches[0].clientY - this.disY; // 内容位置应为手指当前位置减去手指点击时与内容位置的差值
if (cur > 0) { // 如果内容位置大于0, 即手指向下滑动并到顶时
cur *= fl / (fl + cur); // 弹力模拟公式: 位置 *= 弹力 / (弹力 + 位置)
} else if (cur < this.ch - this.oh) { // 如果内容位置小于容器高度减内容高度(因为需要负数,所以反过来减),即向上滑动到最底部时
// 当列表滑动到最底部时,curPos的值其实是等于容器高度减列表高度的,假设窗口高度为10,列表为30,此时curPos为-20,但这里判断是小于,所以当curPos < -20时才会触发
cur += this.oh - this.ch; // 列表位置加等于列表高度减容器高度(这是与上面不同,这里是正减,得到一个正数),这里curPos为负数,加上一个正数,这里curPos为负数,加上一个正数,延用上面的假设,此时 cur = -21 + (30-10=20) = -1 ,所以这里算的是溢出数
cur = cur * fl / (fl - cur) - this.oh + this.ch; // 然后给溢出数带入弹力,延用上面的假设,这里为 cur = -1 * 150 /(150 - -1 = 151)~= -0.99 再减去 30 等于 -30.99 再加上容器高度 -30.99+10=-20.99 ,这也是公式,要死记。。
}
this.setPos(cur);
}
vy = ev.targetTouches[0].clientY - this.startY; // 记录本次移动后,与前一次手指位置的滑动的距离,快速滑动时才有效,慢速滑动时差值为 1 或 0,vy可以理解为滑动的力度
this.startY = ev.targetTouches[0].clientY; // 更新前一次位置为现在的位置,以备下一次比较
}
// let maxHeight = this.total < 3 ? 0 : (this.$refs.resultBody.offsetHeight - this.defaultHeight);
},
touchEnd (ev) {
ev = ev || event;
if (ev.changedTouches.length > 1) return;
if (Math.abs(ev.changedTouches[0].clientY - this.sY) < 5) return;
this.mleave(ev);
},
setPos(y) { // 列表y轴位置,移动列表
this.resultBodyStyle = `transform: translateY(${y}px) translateZ(0);`
},
ease (target) {
isInTransition = true;
let that = this;
this.timer = setInterval(function () { // 回弹算法为 当前位置 减 目标位置 取2个百分点 递减
cur -= (cur - target) * 0.2;
if (Math.abs(cur - target) < 1) { // 减到当前位置与目标位置相差小于1之后直接归位
cur = target;
clearInterval(that.timer);
isInTransition = false;
}
that.setPos(cur);
}, 20);
},
mleave(ev) {
if (isDown) {
isDown = false;
let friction = ((vy >> 31) * 2 + 1) * 0.5, // 根据力度套用公式计算出惯性大小
that = this,
// _oh = this.$refs.resultWrap.scrollHeight - this.$refs.resultWrap.clientHeight;
_oh = this.$refs.resultWrap.scrollHeight - (this.total > 3 ? 204 : this.$refs.resultWrap.clientHeight);
this.timer = setInterval(function () {
vy -= friction; // 力度按惯性大小递减
cur += vy; // 转换为额外的滑动距离
that.setPos(cur); // 滑动列表
if (-cur - _oh > offset) { // 如果列表底部超出
clearInterval(that.timer);
that.ease(-_oh); // 回弹
return;
}
if (cur > offset) { // 如果列表顶部超出
clearInterval(that.timer);
that.ease(0); // 回弹
return;
}
if (Math.abs(vy) < 1) { // 如果力度减小到小于1了,再做超出回弹
clearInterval(that.timer);
if (cur > 0) {
that.ease(0);
return;
}
if (-cur > _oh) {
that.ease(-_oh);
return;
}
}
}, 20);
}
},
normal() {
this.slideEffect = `transform: translateY(${-this.defaultHeight}px); transition: all .5s;`;
this.resultPanelStatus = 'normal';
},
/**
* 点击地图上的poi点时,结果列表中相应的点置于可视区域
*/
scrollToClickOne (_index) {
let dis = (_index === 0 ? 0 : (_index) * 60); // 选中的索引行乘以60,即为当索引行移动到第一行所需的上移高度
let scrollHeight = this.$refs.resultWrap.scrollHeight; // 列表内容高度
let dVal = (scrollHeight - 204); // 最多能够上移的高度
if (0 < Math.abs(cur) && Math.abs(cur) < dVal) {
if (dis < Math.abs(cur)) { // 下移
cur += Math.abs(cur) - dis;
} else if (Math.abs(cur) + 144 < dis && dis < Math.abs(cur) + 204) { // 上移
cur -= dis + 60 - 204 - Math.abs(cur);
} else if (dis >= Math.abs(cur) + 204) { // 上移 目标完全在可视范围下面
cur -= dis + 60 - Math.abs(cur) - 204;
}// cur = -dVal;
} else if (Math.abs(cur) === dVal) { // 当滑至底部时
if (_index < (this.total - 3)) {
cur = -_index * 60;
}
} else if (cur === 0) { // 当滑至顶部时
if (dis >= 144) {
cur += -(dis + 60 - 204 - Math.abs(cur));
}
}
this.setPos(cur);
},
clickItem (_index) {
let len = this.$refs.resultBody.children.length;
for (let i = 0; i < len; i++) {
if (i === _index) {
this.$refs.resultBody.children[i].style.background = "#F0F0F0";
} else {
this.$refs.resultBody.children[i].style.background = "white";
}
}
},
close(ev) { // click事件会和touchestart事件冲突
this.normal();
this.resultBodyStyle = 'transform: translateY(0) translateZ(0);';
cur = 0;
this.$store.state.resultPanel.show = false;
this.$emit('on-cancel');
},
handleNavigate(_index) {
// this.$emit("on-item-click", JSON.parse(JSON.stringify(this.cloneData[_index])), _index); //这个是获取行的元素,和索引
this.$emit("on-click-gohere", _index); // 这个是获取索引
},
/**
* 点击结果列表,地图上的图标置于可视区域
*/
focusResultOnMap(_index) {
this.clickItem(_index);
this.scrollToClickOne(_index);
this.$emit("on-click-item", _index); // 这个是获取索引
},
/**
* 点击地图上的图标,结果列表中相应的结果置于可视区域
*/
focusResultOnMapReverse(_index) {
this.scrollToClickOne(_index);
this.clickItem(_index);
},
// deepCopy
deepCopy(data) {
const t = this.typeOf(data);
let o;
if (t === 'array') {
o = [];
} else if (t === 'object') {
o = {};
} else {
return data;
}
if (t === 'array') {
for (let i = 0; i < data.length; i++) {
o.push(this.deepCopy(data[i]));
}
} else if (t === 'object') {
for (let i in data) {
o[i] = this.deepCopy(data[i]);
}
}
return o;
},
typeOf(obj) {
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object'
};
return map[toString.call(obj)];
}
}
}
</script>
<style type="text/less" scoped>
.transparent {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity .3s;
z-index: -1000000000;
}
.transparent.active {
opacity: 1;
z-index: 0;
}
.mapbox-result {
height: calc(100% - 2.8vw);
background: #fff;
position: absolute;
font-size: 12px;
color: #4A4A4A;
bottom: 0;
width: 94.4vw;
margin: 0 2.8vw;
outline: 0;
box-sizing: border-box;
top: 100%;
overflow: hidden;
border-radius: 5px 5px 0 0;
box-shadow: 0 0 12px 0px rgba(153, 153, 153, 0.25);
}
.mapbox-result-content {
position: relative;
background-color: #fff;
border: 0;
}
.mapbox-result-header {
/*padding: 24px 10vw;*/
text-align: center;
width: calc(100% - 5.6vw - 5.6vw - 32px);
margin: 0 auto;
overflow: hidden;
}
.mapbox-result-header-title {
white-space: nowrap;
/*line-height: 48px;*/
line-height: 50px;
display: inline-flex;
margin: 0;
}
.mapbox-result-header-poi {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 150px;
}
.mapbox-result-close {
position: absolute;
width: 16px;
height: 16px;
background-image: url('../../assets/[email protected]');
background-size: 100% 100%;
background-repeat: no-repeat;
right: 5.6vw;
top: 17px;
}
.mapbox-result-scroll-hidden {
overflow: hidden;
width: 100%;
}
.mapbox-result-wrap {
/*-webkit-overflow-scrolling: touch;*/
/*overflow-scrolling: touch;*/
position: relative;
overflow-y: hidden; /* 设置overflow-y为hidden,以避免原生的scroll影响根据手势滑动计算滚动距离 */
height: 204px;
background: transparent;
width: calc(100% + 17px);
/*解决安卓滑动页面时出现空白*/
-webkit-backface-visibility: hidden;
-webkit-transform: translate3d(0,0,0);
}
.mapbox-result-wrap::-webkit-scrollbar {
display: none;
}
.mapbox-result-body {
/*position: absolute;*/
/*transform: translateY(0);*/
/*transition: transform .5s;*/
width: 100%;
}
</style>