vue實現(xiàn)移動端touch拖拽排序
本文實例為大家分享了vue實現(xiàn)移動端touch拖拽排序的具體代碼,供大家參考,具體內(nèi)容如下
功能介紹:
在移動端開發(fā)中,希望實現(xiàn)類似支付寶應(yīng)用管理頁面的可拖拽排序交互。
大致需求:
1、卡片按照一定順序排序,超出橫向范圍換行顯示;
2、手指長按卡片,可進行拖拽控制,卡片追隨手指移動;
3、卡片移動到相應(yīng)位置,該位置上的卡片向后或向前更換位置,當(dāng)前位置空出;
4、松開手指,卡片可回到原位置或新位置進行展示;
整體思路:
1、卡片實行flex彈性布局,通過數(shù)組的遍歷可自動顯示在相應(yīng)位置;
2、手指長按可使用定時器來判斷,若手指松開,則關(guān)閉定時器,等待下次操作再啟用;
3、跟隨手指移動的卡片可使用absolute定位控制,同時根據(jù)手指位置判斷當(dāng)前所在位置;
4、位置發(fā)生改變時,控制數(shù)組添加或刪除相應(yīng)元素,從而實現(xiàn)換位效果;
簡單效果展示:
具體實現(xiàn):
一、display:flex+v-for布局:
使用彈性布局實現(xiàn)
<!-- 外層ul控制卡片范圍 --> <ul> ? ? <li class="libox" v-for="(item, ind) in list" :key="ind"> ? ? ? ? <div> ? ? ? ? <!-- div顯示數(shù)組內(nèi)容 --> ? ? ? ? ? {{item.name}} ? ? ? ? </div> ? ? </li> </ul>
data() { ? ? return { ? ? ? ? list: [ ? ? ? ? ? ? { name: '1' }, // 卡片內(nèi)容 ? ? ? ? ? ? { name: '2' }, ? ? ? ? ? ? { name: '3' } ? ? ? ? ] ? ? } },
ul { ? ? width: 100%; ? ? height: 100%; ? ? display: flex; // 彈性布局 ? ? flex-wrap: wrap; ? ? overflow: hidden; // 超出部分隱藏,目的阻止橫向滾動 ? ? .libox { ? ? ? width: 25%; // 這里以4列為例 ? ? ? height: 70px; ? ? ?>div { ? ? ? ? background-color:#eee; ? ? ? ? width: calc(100% - 10px); ? ? ? ? height: 36px; ? ? ? ? border-radius: 18px; ? ? ? } ? ? } }
二、touch事件綁定:
應(yīng)用到touchstart,touchmove,touchend事件,使用定時器實現(xiàn)長按效果:
<div? ? ? ? @touchstart="touchstart($event, item)" ? ? ? @touchmove="touchMove($event, item)" ? ? ? @touchend="touchEnd($event, item)" > ? ? ? {{item.name}} </div>
data() { ? ? return { ? ? ? ? timeOutEvent: 0 ? ? }; }, methods: { ? ? // 手指觸摸事件 ? ? touchstart(ev, item) { ? ? ? ? // 定時器控制長按時間,超過500毫秒開始進行拖拽 ? ? ? ? this.timeOutEvent = setTimeout(() => { ? ? ? ? ? ? this.longClick = 1; ? ? ? ? }, 500); ? ? }, ? ? // 手指在屏幕上移動 ? ? touchMove(ev) { ? ? ? ? // 未達到500毫秒就移動則不觸發(fā)長按,清空定時器 ? ? ? ? clearTimeout(this.timeOutEvent); ? ? }, ? ? // 手指離開屏幕 ? ? touchEnd() { ? ? ? ? clearTimeout(this.timeOutEvent); ? ? } }
三、卡片移動:
在ul中增加一個獨立的不在循環(huán)中的li標(biāo)簽,改為absolute定位,通過動態(tài)修改li標(biāo)簽top、left屬性實現(xiàn)跟隨手指移動效果。
<ul> ? ? <li v-show="selectItem.name" class="selectBox" ref="selectBox"> ? ? ? ? {{selectItem.name}} ? ? </li> </ul>
ul { ? ? position: relative; ? ? // 此li標(biāo)簽的樣式與循環(huán)li標(biāo)簽內(nèi)的div樣式保持一致 ? ? // 背景色加深,代表被手指選中 ? ? .selectBox { ? ? ? ? position: absolute; ? ? ? ? width: calc(25% - 10px); ? ? ? ? height: 36px; ? ? ? ? border-radius: 18px; ? ? ? ? background-color:#6981c8; ? ? ? ? color:white; ? ? } }
當(dāng)卡片被選中,將卡片內(nèi)容賦值給全局變量,判斷卡片顯示隱藏(v-show判斷,隱藏但占位),實現(xiàn)選中元素位置空出效果:
手指位置通過touchmove獲?。?/p>
<div? ? ? @touchstart="touchstart($event, item)" ? ? @touchmove="touchMove($event, item)" ? ? @touchend="touchEnd($event, item)" ? ? @click="listClickHandler(item)" ? ? v-show="item.name !== selectItem.name" > ? ? {{item.name}} </div>
touchstart(ev, item) { ? ? this.timeOutEvent = setTimeout(() => { ? ? ? ? this.longClick = 1; ? ? ? ? this.selectItem = item; // 將卡片內(nèi)容賦值給全局變量 ? ? ? ? const selectDom = ev.target; // li元素 ? ? ? ? // 元素初始位置 ? ? ? ? this.oldNodePos = { ? ? ? ? ? ? x: selectDom.offsetLeft, ? ? ? ? ? ? y: selectDom.offsetTop ? ? ? ? }; ? ? ? ? // 鼠標(biāo)原始位置 ? ? ? ? this.oldMousePos = { ? ? ? ? ? ? x: ev.touches[0].pageX, ? ? ? ? ? ? y: ev.touches[0].pageY ? ? ? ? }; ? ? ? ? const lefts = this.oldMousePos.x - this.oldNodePos.x; // x軸偏移量 ? ? ? ? const tops = this.oldMousePos.y - this.oldNodePos.y; // y軸偏移量 ? ? ? ? const { pageX, pageY } = ev.touches[0]; // 手指位置 ? ? ? ? this.$refs.selectBox.style.left = `${pageX - lefts}px`; ? ? ? ? this.$refs.selectBox.style.top = `${pageY - tops}px`; ? ? }, 500); }, touchMove(ev) { ? ? clearTimeout(this.timeOutEvent); ? ? // this.longClick === 1判斷是否長按 ? ? if (this.longClick === 1) { ? ? ? ? const selectDom = ev.target.parentNode; // li元素 ? ? ? ? const lefts = this.oldMousePos.x - this.oldNodePos.x; // x軸偏移量 ? ? ? ? const tops = this.oldMousePos.y - this.oldNodePos.y; // y軸偏移量 ? ? ? ? const { pageX, pageY } = ev.touches[0]; // 手指位置 ? ? ? ? this.$refs.selectBox.style.left = `${pageX - lefts}px`; ? ? ? ? this.$refs.selectBox.style.top = `${pageY - tops}px`; ? ? } }
四、獲取手指所在位置:
cardIndex(selDom, moveleft, movetop) { ? ? const liWid = selDom.clientWidth; // li寬度 ? ? const liHei = selDom.clientHeight; // li高度 ? ? const newWidNum = Math.ceil((moveleft / liWid)); // 手指所在列 ? ? const newHeiNum = Math.ceil((movetop / liHei)); // 手指所在行 ? ? const newPosNum = (newHeiNum - 1) * 4 + newWidNum; // 手指所在位置 ? ? // 判斷是否是新位置并且沒有超出列表數(shù)量范圍 ? ? if (this.oldIndex !== newPosNum &&? ? ? ? ? newPosNum <= this.list.length) { ? ? ? ? // 將新的位置賦值給全局變量oldIndex ? ? ? ? this.oldIndex = newPosNum; ? ? } }
五、操作數(shù)組(刪除或插入元素):
監(jiān)聽oldIndex的值,若發(fā)生改變則執(zhí)行操作數(shù)組函數(shù)
watch: { ? ? oldIndex(newVal) { ? ? ? ? const oldIndex = this.list.indexOf(this.selectItem); ? ? ? ? this.list.splice(oldIndex, 1); ? ? ? ? this.list.splice(newVal - 1, 0, this.selectItem); ? ? } },
六、手指離開屏幕:
手指離開屏幕,清空選中的元素selectItem,跟隨手指移動的卡片(li.selectBox)自動隱藏,在循環(huán)中隱藏的卡片(li)則會顯示,實現(xiàn)換位效果。
touchEnd() { ? ? clearTimeout(this.timeOutEvent); ? ? this.selectItem = {}; }
七、備注:
上面的代碼是基于div容器內(nèi)只有文字沒有其他dom元素實現(xiàn),后發(fā)現(xiàn)若div中存在dom元素例如svg,則【$event】選中的值會變成其子元素,且拖拽排序出現(xiàn)問題,希望知道原因的小伙伴可以評論或私信告訴我一下,非常感謝。
粗暴的解決方式:
div容器增加after蒙版,可設(shè)置為透明色:
div? ? position: relative; ? &::after { ? ? content: ''; ? ? width: 100%; ? ? height: 100%; ? ? background: rgba(255, 177, 177, 0.3); // 背景色 ? ? position: absolute; ? ? top: 0; ? ? left: 0; ? } }
八、完整代碼:
<template> ? <div> ? ? <ul> ? ? ? <li? ? ? ? ? class="libox"? ? ? ? ? v-for="(item, index) in list" ? ? ? ? :key="index" ? ? ? ? :id="'card' + (index + 1)" ? ? ? > ? ? ? ? <div ? ? ? ? ? @touchstart="touchstart($event, item)" ? ? ? ? ? @touchmove="touchMove($event, item)" ? ? ? ? ? @touchend="touchEnd($event, item)" ? ? ? ? ? v-show="item.name !== selectItem.name" ? ? ? ? > ? ? ? ? ? {{item.name}} ? ? ? ? ? <svg class="icon svg-icon" aria-hidden="true"> ? ? ? ? ? ? <use :xlink:href="item.icon" rel="external nofollow" ></use> ? ? ? ? ? </svg> ? ? ? ? </div> ? ? ? </li> ? ? ? <li v-show="selectItem.name" class="selectBox" ref="selectBox"> ? ? ? ? {{selectItem.name}} ? ? ? ? <svg class="icon svg-icon" aria-hidden="true"> ? ? ? ? ? <use :xlink:href="selectItem.icon" rel="external nofollow" ></use> ? ? ? ? </svg> ? ? ? </li> ? ? </ul> ? </div> </template> <script> export default { ? data() { ? ? return { ? ? ? // 列表數(shù)據(jù) ? ? ? list: [ ? ? ? ? { name: '1', selected: true, icon: '#icon-mianxingbenzivg' }, ? ? ? ? { name: '2', selected: true, icon: '#icon-mianxingchizi' }, ? ? ? ? { name: '3', selected: true, icon: '#icon-mianxingdiannao' }, ? ? ? ? { name: '4', selected: true, icon: '#icon-mianxingdayinji' }, ? ? ? ? { name: '5', selected: true, icon: '#icon-mianxingdingshuqi' }, ? ? ? ? { name: '6', selected: true, icon: '#icon-mianxingheiban' }, ? ? ? ? { name: '7', selected: true, icon: '#icon-mianxinggangbi' }, ? ? ? ? { name: '8', selected: true, icon: '#icon-mianxingboshimao' }, ? ? ? ? { name: '9', selected: true, icon: '#icon-mianxingjisuanqi' }, ? ? ? ? { name: '10', selected: true, icon: '#icon-mianxinghuaxue' }, ? ? ? ? { name: '11', selected: true, icon: '#icon-mianxingqianbi' }, ? ? ? ? { name: '12', selected: true, icon: '#icon-mianxingshubao' }, ? ? ? ? { name: '13', selected: true, icon: '#icon-mianxingshuicaibi' }, ? ? ? ? { name: '14', selected: true, icon: '#icon-mianxingtushu' }, ? ? ? ], ? ? ? // 選中元素內(nèi)容 ? ? ? selectItem: {}, ? ? ? timeOutEvent: 0, ? ? ? oldNodePos: { ? ? ? ? x: 0, ? ? ? ? y: 0, ? ? ? }, ? ? ? oldMousePos: { ? ? ? ? x: 0, ? ? ? ? y: 0 ? ? ? }, ? ? ? oldIndex: 0, ? ? ? // 長按標(biāo)識 ? ? ? longClick: 0 ? ? }; ? }, ? watch: { ? ? oldIndex(newVal) { ? ? ? const oldIndex = this.list.findIndex(r=> r.name === this.selectItem.name); ? ? ? this.list.splice(oldIndex, 1); ? ? ? this.list.splice(newVal, 0, this.selectItem); ? ? } ? }, ? methods: { ? ? touchstart(ev, item) { ? ? ? this.longClick = 0; ? ? ? const that = this; ? ? ? const selectDom = ev.currentTarget; // div元素 ? ? ? this.timeOutEvent = setTimeout(() => { ? ? ? ? that.longClick = 1; ? ? ? ? that.selectItem = item; ? ? ? ? // 元素初始位置 ? ? ? ? that.oldNodePos = { ? ? ? ? ? x: selectDom.offsetLeft, ? ? ? ? ? y: selectDom.offsetTop ? ? ? ? }; ? ? ? ? // 鼠標(biāo)原始位置 ? ? ? ? that.oldMousePos = { ? ? ? ? ? x: ev.touches[0].pageX, ? ? ? ? ? y: ev.touches[0].pageY ? ? ? ? }; ? ? ? ? const lefts = that.oldMousePos.x - that.oldNodePos.x; // x軸偏移量 ? ? ? ? const tops = that.oldMousePos.y - that.oldNodePos.y; // y軸偏移量 ? ? ? ? const { pageX, pageY } = ev.touches[0]; // 手指位置 ? ? ? ? that.$refs.selectBox.style.left = `${pageX - lefts}px`; ? ? ? ? that.$refs.selectBox.style.top = `${pageY - tops}px`; ? ? ? }, 500); ? ? }, ? ? touchMove(ev) { ? ? ? clearTimeout(this.timeOutEvent); ? ? ? const selectDom = ev.currentTarget.parentNode; // li元素 ? ? ? if (this.longClick === 1) { ? ? ? ? const lefts = this.oldMousePos.x - this.oldNodePos.x; // x軸偏移量 ? ? ? ? const tops = this.oldMousePos.y - this.oldNodePos.y; // y軸偏移量 ? ? ? ? const { pageX, pageY } = ev.touches[0]; // 手指位置 ? ? ? ? this.$refs.selectBox.style.left = `${pageX - lefts}px`; ? ? ? ? this.$refs.selectBox.style.top = `${pageY - tops}px`; ? ? ? ? this.cardIndex(selectDom, pageX, pageY); ? ? ? } ? ? }, ? ? touchEnd() { ? ? ? clearTimeout(this.timeOutEvent); ? ? ? this.selectItem = {}; ? ? }, ? ? /** ? ? ?* 計算當(dāng)前移動卡片位于卡片的哪一行哪一列 ? ? ?*/ ? ? cardIndex(selDom, moveleft, movetop) { ? ? ? const liWid = selDom.clientWidth; ? ? ? const liHei = selDom.clientHeight; ? ? ? const newWidthNum = Math.ceil((moveleft / liWid)); // 哪一列 ? ? ? const newHeightNum = Math.ceil((movetop / liHei)); // 哪一行 ? ? ? const newPositionNum = (newHeightNum - 1) * 4 + newWidthNum; ? ? ? if (this.oldIndex !== newPositionNum - 1) { ? ? ? ? ? if (newPositionNum <= this.list.length) { ? ? ? ? ? ? this.oldIndex = newPositionNum - 1; ? ? ? ? ? } else { ? ? ? ? ? ? this.oldIndex = this.list.length - 1; ? ? ? ? ? } ? ? ? } ? ? } ? } } </script> <style lang="scss" scoped> ? @mixin myFlexCenter{ ? ? display: flex; ? ? justify-content: center; ? ? align-items: center; ? } ? ul { ? ? width: 100%; ? ? height: 100%; ? ? display: flex; ? ? flex-wrap: wrap; ? ? position: relative; ? ? overflow: hidden; ? ? .libox { ? ? ? width: 25%; ? ? ? height: 100px; ? ? ? border-right: 1px dashed #cccccc; ? ? ? border-bottom: 1px dashed #cccccc; ? ? ? box-sizing: border-box; ? ? ? @include myFlexCenter; ? ? ? >div { ? ? ? ? width: calc(100% - 10px); ? ? ? ? height: 75px; ? ? ? ? border-radius: 18px; ? ? ? ? @include myFlexCenter; ? ? ? ? position: relative; ? ? ? ? &::after { ? ? ? ? ? ? content: ''; ? ? ? ? ? ? width: 100%; ? ? ? ? ? ? height: 100%; ? ? ? ? ? ? background: rgba(255, 177, 177, 0.3); ? ? ? ? ? ? position: absolute; ? ? ? ? ? ? top: 0; ? ? ? ? ? ? left: 0; ? ? ? ? } ? ? ? ? >svg { ? ? ? ? ? width: 75px; ? ? ? ? ? height: 75px; ? ? ? ? } ? ? ? } ? ? } ? ? .selectBox{ ? ? ? position: absolute; ? ? ? width: calc(25% - 10px); ? ? ? height: 75px; ? ? ? border-radius: 18px; ? ? ? >svg { ? ? ? ? width: 75px; ? ? ? ? height: 75px; ? ? ? } ? ? ? background-color: rgba(0, 0, 0, 0.1); ? ? ? color:white; ? ? ? @include myFlexCenter; ? ? ? -moz-user-select:none; /*火狐*/ ? ? ? -webkit-user-select:none; /*webkit瀏覽器*/ ? ? ? -ms-user-select:none; /*IE10*/ ? ? ? -khtml-user-select:none; /*早期瀏覽器*/ ? ? ? user-select:none; ? ? } ? } </style>
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue2中使用SSE(服務(wù)器發(fā)送事件)原因分析
SSE是圍繞只讀Comet交互推出的API或者模式,SSE 支持短輪詢、長輪詢和HTTP 流,而且能在斷開連接時自動確定何時重新連接,本文重點給大家介紹2023-10-10深入了解vue-router原理并實現(xiàn)一個小demo
這篇文章主要為大家詳細(xì)介紹了vue-router原理并實現(xiàn)一個小demo,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03使用vue-cli3+typescript的項目模板創(chuàng)建工程的教程
這篇文章主要介紹了使用vue-cli3+typescript的項目模板創(chuàng)建工程,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02vue子組件使用自定義事件向父組件傳遞數(shù)據(jù)
這篇文章主要介紹了vue子組件使用自定義事件向父組件傳遞數(shù)據(jù)的方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-05-05Vue-router結(jié)合transition實現(xiàn)app前進后退動畫切換效果的實例
下面小編就為大家?guī)硪黄猇ue-router結(jié)合transition實現(xiàn)app前進后退動畫切換效果的實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10