vue实现滑动和滚动
小静仔 人气:0面板滑动效果,父组件是resultPanel,子组件是resultOption,仿照了iview中,Select组件的写法。
<template> <div v-if="visiable"> <div class="transparent" :class="{active:resultPanelStatus==='top'}"></div> <div class="mapbox-result" ref="resultPanel" style="z-index: 101;" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" :style="slideEffect" > <div class="mapbox-result-content"> <a class="mapbox-result-close" v-if="closable" @click="close"></a> <div class="mapbox-result-header"> <slot name="header"> <div class="mapbox-result-header-title">共找到【{{header}}】相关{{total}}结果</div> </slot> </div> <div class="mapbox-result-body" ref="resultBody" > <result-option ref="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> </template> <script> import resultOption from './resultOption'; 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, // 开始触摸屏幕的点 endY: 0, // 离开屏幕的点 moveY: 0, // 滑动时的距离 disY: 0, // 移动距离 slideEffect: '' //滑动效果 } }, mounted() { // this.$refs.resultBody.style.height = `${this.defaultHeight - 60}px`; // this.$refs.resultBody.style.overflowY = 'hidden'; }, computed: { total() { return this.data.length; }, defaultHeight() { return this.data.length > 3 ? 240 : this.data.length * 60 + 60 //当结果大于3时,默认只显示三个 }, visiable() { this.resultPanelStatus = 'normal'; this.slideEffect = `transform: translateY(-${this.defaultHeight}px); transition: all .5s`; return this.$store.state.resultPanel.show; } }, methods: { /** * 手指接触屏幕 */ onTouchStart(ev) { ev = ev || event; // ev.preventDefault(); if (ev.touches.length === 1) { this.startY = ev.touches[0].clientY; } }, /** * 手指滑动 */ onTouchMove(ev) { ev = ev || event; console.log("ev.target: ", ev.target); // ev.preventDefault(); if (ev.touches.length === 1) { let resultPanel = this.$refs.resultPanel.offsetHeight; this.moveY = ev.touches[0].clientY; this.disY = this.moveY - this.startY; if (this.disY < 0 && -this.defaultHeight + this.disY > -resultPanel && this.resultPanelStatus === 'normal') { //向上滑动 this.slideEffect = `transform: translateY(${-this.defaultHeight + this.disY}px); transition: all 0s;`; //内容随着面板上滑出现的动画 this.$refs.resultBody.style.transition = 'all .5s'; this.$refs.resultBody.style.height = `${this.$refs.resultPanel.offsetHeight - 60}px`; } else if (this.resultPanelStatus === 'top' && this.disY < 0) { this.scroll(); } else if (this.disY > 0 && this.resultPanelStatus === 'top') { //向下滑动 /*当手指向下滑动时,如果滑动的起始点不在非内容区以及scrollTop不为0,则为滚动,否则面板随着手指滑动并隐藏滚动条,以防止下滑过程中,能够滚动数据*/ if (this.$refs.resultBody.scrollTop > 0 && ev.target !== document.getElementsByClassName("mapbox-result-header")[0]) { this.scroll(); } else { this.slideEffect = `transform: translateY(${-resultPanel + this.disY}px); transition: all 0s`; this.$refs.resultBody.style.overflowY = 'hidden'; } //当处于normal状态,手指向下滑,则下滑 } else if (this.disY > 0 && this.resultPanelStatus === 'normal') { this.slideEffect = `transform: translateY(${-this.defaultHeight + this.disY}px); transition: all 0s`; } } }, /** * 离开屏幕 */ onTouchEnd(ev) { ev = ev || event; // ev.preventDefault(); if (ev.changedTouches.length === 1) { this.endY = ev.changedTouches[0].clientY; this.disY = this.endY - this.startY; if (this.disY > 0 && this.resultPanelStatus === 'top') { //向下滑动 /*当手指向下滑动时,如果滑动的起始点不在非内容区以及scrollTop不为0,则为滚动,否则面板滑动到默认位置*/ if (this.$refs.resultBody.scrollTop > 0 && ev.target !== document.getElementsByClassName("mapbox-result-header")[0]) { this.scroll(); } else { this.normal(); } //手指离开的时候,出现滚动条,已解决第一次滑动内容的时候,滚动条才会出现而内容没有滑动的问题 } else if (this.disY < 0 && this.resultPanelStatus === 'normal') { //向上滑动 this.top(); this.move(); } else if (this.disY < 0 && this.resultPanelStatus === 'top') { this.scroll(); } else if (this.disY > 0 && this.resultPanelStatus === 'normal') { this.normal(); //处于normal状态下滑,手指离开屏幕,回归normal状态 } } }, //当到默认高度时,设置状态为正常状态,并且隐藏滚动条,将scrollTop置0,以避免内前面的内容被隐藏 normal() { // this.$refs.resultBody.style.overflowY = 'hidden'; this.slideEffect = `transform: translateY(${-this.defaultHeight}px); transition: all .5s;`; this.resultPanelStatus = 'normal'; this.$refs.resultBody.scrollTop = 0; }, top() { this.slideEffect = 'transform: translateY(-100%); transition: all .5s;'; this.resultPanelStatus = 'top'; }, move() { // this.$refs.resultBody.style.height = `${-this.disY + this.defaultHeight}px`; this.$refs.resultBody.style.overflowY = 'auto'; }, scroll() { this.$refs.resultBody.style.overflowY = 'auto'; }, close(ev) { // click事件会和touchestart事件冲突 //当面板处于最高状态被关闭时,恢复到正常高度状态,以避免下次打开仍处于最高处 this.normal(); // this.$refs.resultBody.scrollTop = 0; // this.$refs.resultBody.style.overflowY = 'hidden'; 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.$emit("on-click-item", _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> //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-family: PingFangSC-Regular; font-size: 12px; color: #4A4A4A; bottom: 0; width: 94.4vw; margin: 0 2.8vw; outline: 0; overflow: auto; 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; line-height: 1; text-align: center; } .mapbox-result-header-title { white-space: nowrap; } .mapbox-result-close { position: absolute; width: 16px; height: 16px; background: url('../../assets/close-black@2x.png'); background-size: 100% 100%; background-repeat: no-repeat; right: 5.6vw; top: 22px } .mapbox-result-body { height: auto; } </style>
<template> <div class="mapbox-result-option"> <div class="mapbox-result-option-content"> <!--<button class="mapbox-btn mapbox-btn-primary mapbox-result-option-btn mapbox-btn-right" @click="handleClick"> <i class="mapbox-result-option-icon"></i> </button>--> <a class="mapbox-result-option-nav" @click="handleClick"></a> <div class="mapbox-result-option-item" @click="resultItemClick"> <div class="mapbox-result-option-item-main"> <p class="mapbox-result-option-title"> <span class="mapbox-result-option-order">{{index}}</span> {{name}} </p> <p class="mapbox-result-option-note"> {{floorName}},距离当前位置{{meter}}米 </p> </div> </div> </div> </div> </template> <script> export default { name: 'result-option', props: { value: { type: Boolean, default: true }, index: { type: Number }, name: { type: String }, meter: { type: Number }, floorName: { type: String } }, data() { return { } }, methods: { handleClick() { this.$emit("on-click-gohere"); }, resultItemClick() { this.$emit("on-click-item"); } } } </script> <style type="text/less" scoped> .mapbox-result-option { height: 60px; width: calc(100% - 8.3vw); display: block; border-bottom: 1px solid #dbd6d6; box-sizing: border-box; margin: 0 auto; overflow: hidden; } .mapbox-result-option-content { padding: 0; margin: 0; font-family: PingFangSC-Regular; font-size: 12px; color: #4A4A4A; position: relative; display: inline-block; width: 100%; } .mapbox-btn { display: inline-block; margin-bottom: 0; font-weight: 400; text-align: center; vertical-align: middle; touch-action: manipulation; background-image: none; border: 1px solid transparent; white-space: nowrap; line-height: 1.5; } .mapbox-result-option-btn { position: relative; border-radius: 50%; height: 30px; width: 8.3vw; padding: 0; outline: none; margin: 15px 4.2vw 15px 0; z-index: 1; /*避免文字挡住了按钮*/ } .mapbox-btn-primary { color: #fff; background-color: #2A70FE; border-color: #2A70FE; } .mapbox-btn-right { float: right; margin-right: 4.2vw; } .mapbox-result-option-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-size: 100% 100%; width: 2.9vw; height: 18px; background: url("../../../static/image/icon_nav3.png") no-repeat; } .mapbox-result-option-nav { background: url("../../assets/btn_route_planning_normal.png"); width: 30px; height: 30px; background-size: 100% 100%; background-repeat: no-repeat; float: right; display: block; position: absolute; right: 0; top: 15px; z-index: 1; } .mapbox-result-option-item { display: block; position: relative; margin: 10px auto; } .mapbox-result-option-item-main { display: block; vertical-align: middle; font-size: 16px; color: #4A4A4A; } .mapbox-result-option-title { font: 15px/21px PingFangSC-Regular; position: relative; } .mapbox-result-option-order { font: 15px/21px PingFangSC-Medium; position: relative; margin-left: 1.9vw; margin-right: 4.6vw; } .mapbox-result-option-note { font: 12px/16px PingFangSC-Regular; color: #9B9B9B; white-space: normal; position: relative; margin-left: 12.5vw; margin-top: 3px; } </style>
ev = ev || event,这个写法是兼容各个浏览器,在Firefox浏览器中,事件绑定的函数获取事件本身,是通过函数中传入的,而IE等浏览器中,则可以通过window.event或者event的方式来获取函数本身。
touchstart和click事件冲突解决: 去掉touchstart,touchmove和touchend事件中的e.preventDefault(); 它会阻止后面事件的触发;但去掉preventDefault事件会有问题,在微信网页中打开这个网页,向下滑动时会触发微信的下拉事件,但是在App中应用这组件就不会有这个问题。有一个解决微信网页中,手指向下滑动触发了微信的下拉刷新事件的方法,就是使用setTimeout。
setTimeout(() => {e.preventDefault(); }, 200);
这样子可以在click事件发生后,再阻止之后的默认事件的触发。
滚动事件:滚动事件是在touchmove和touchend中触发的,面板的上滑事件和滚动事件不同时进行。
上滑时,判断面板状态,如果处于top状态,则触发scroll事件,手指离开面板时,仍是scroll事件;如果是处于normal状态,则是上滑面板,手指离开面板时,设置面板为top状态,并设置内容的滚动条可见;初始面板上滑到顶部时,第二次上滑面板则会触滚动条,内容可滚动;
下滑时,判断是否处于top状态,如果处于top状态,当内容区的scrollTop大于0,且手指初始位置位于内容区,那么就触发滚动,否则触发面板下滑;当处于normal状态时,下滑的话,可以采用不触发任何事件,或者可以下滑,但手指离开屏幕时,回归到默认位置,这里使用了后者的做法。
加载全部内容