技术宅男对探探/陌陌并不陌生,一款专一于陌生人的社交App。里面的左右滑动翻牌子效果更是让人眼前一亮,彷佛有一种古时君王选妃子的感受。让人玩的爱不释手。
一睹风采
哈哈,效果还行吧。下面就简单的讲解下具体的实现方法。web
页面总体分为 顶部Navbar、卡片区域、底部Tabbar 三个部分。app
<!-- //翻一翻模板 --> <template> <div> <!-- >>顶部 --> <header-bar :back="false" bgcolor="linear-gradient(to right, #00e0a1, #00a1ff)" fixed> <div slot="title"><i class="iconfont icon-like c-red"></i> <em class="ff-gg">碰见TA</em></div> <div slot="right" class="ml-30" @click="showFilter = true"><i class="iconfont icon-filter"></i></div> </header-bar> <!-- >>主页面 --> <div class="nuxt__scrollview scrolling flex1" ref="scrollview" style="background: linear-gradient(to right, #00e0a1, #00a1ff);"> <div class="nt__flipcard"> <div class="nt__stack-wrapper"> <flipcard ref="stack" :pages="stackList" @click="handleStackClicked"></flipcard> </div> <div class="nt__stack-control flexbox"> <button class="btn-ctrl prev" @click="handleStackPrev"><i class="iconfont icon-unlike "></i></button> <button class="btn-ctrl next" @click="handleStackNext"><i class="iconfont icon-like "></i></button> </div> </div> </div> <!-- >>底部tabbar --> <tab-bar bgcolor="linear-gradient(to right, #00e0a1, #00a1ff)" color="#fff" /> </div> </template>
点击筛选,在侧边会出现弹窗。其中范围滑块、switch开关、Rate评分等组件则是使用Vant组件库。dom
侧边弹窗模板ide
<template> <!-- ... --> <!-- @@侧边栏弹框模板 --> <v-popup v-model="showFilter" position="left" xclose xposition="left" title="高级筛选与设置"> <div class="flipcard-filter"> <div class="item nuxt-cell"> <label class="lbl">范围</label> <div class="flex1"> <van-slider v-model="distanceRange" bar-height="2px" button-size="12px" active-color="#00e0a1" min="1" @input="handleDistanceRange" /> </div> <em class="val">{{distanceVal}}</em> </div> <div class="item nuxt-cell"> <label class="lbl flex1">自动增长范围</label> <em class="val"><van-switch v-model="autoExpand" size="20px" active-color="#00e0a1" /></em> </div> <div class="item nuxt-cell"> <label class="lbl flex1">性别</label> <em class="val">女生</em> </div> <div class="item nuxt-cell"> <label class="lbl">好评度</label> <div class="flex1"><van-rate v-model="starVal" color="#00e0a1" icon="like" void-icon="like-o" @change="handleStar" /></div> <em class="val">{{starVal}}星</em> </div> <div class="item nuxt-cell"> <label class="lbl flex1">优先在线用户</label> <em class="val"><van-switch v-model="firstOnline" size="20px" active-color="#00e0a1" /></em> </div> <div class="item nuxt-cell"> <label class="lbl flex1">优先新用户</label> <em class="val"><van-switch v-model="firstNewUser" size="20px" active-color="#00e0a1" /></em> </div> <div class="item nuxt-cell mt-20"> <div class="mt-30 nuxt__btn nuxt__btn-primary--gradient" style="height:38px;"><i class="iconfont icon-filter"></i> 更新</div> </div> </div> </v-popup> </template> <script> export default { // 用于配置应用默认的 meta 标签 head() { return { title: `${this.title} - 翻一翻`, meta: [ {name:'keywords',hid: 'keywords',content:`${this.title} | 翻一翻 | 翻动卡片`}, {name:'description',hid:'description',content:`${this.title} | 仿探探卡片翻动`} ] } }, middleware: 'auth', data () { return { title: 'Nuxt', showFilter: false, distanceRange: 1, distanceVal: '<1km', autoExpand: true, starVal: 5, firstOnline: false, firstNewUser: true, // ... } }, methods: { /* @@左侧筛选函数 */ // 范围选择 handleDistanceRange(val) { if(val == 1) { this.distanceVal = '<1km'; } else if (val == 100) { this.distanceVal = "100km+" }else { this.distanceVal = val+'km'; } }, // 好评度 handleStar(val) { this.starVal = val; }, // ... }, } </script>
卡片区单独封装了一个组件flipcard,只需传入pages数据就能够。<flipcard ref="stack" :pages="stackList"></flipcard>
在四周拖拽卡片会出现不一样的斜切视角。函数
pages数据格式布局
module.exports = [ { avatar: '/assets/img/avatar02.jpg', name: '放荡不羁爱自由', sex: 'female', age: 23, starsign: '天秤座', distance: '艺术/健身', photos: [...], sign: '交个朋友,非诚勿扰' }, ... ]
flipcard组件模板flex
<template> <ul class="stack"> <li class="stack-item" v-for="(item, index) in pages" :key="index" :style="[transformIndex(index),transform(index)]" @touchmove.stop.capture="touchmove" @touchstart.stop.capture="touchstart" @touchend.stop.capture="touchend($event, index)" @touchcancel.stop.capture="touchend($event, index)" @mousedown.stop.capture.prevent="touchstart" @mouseup.stop.capture.prevent="touchend($event, index)" @mousemove.stop.capture.prevent="touchmove" @mouseout.stop.capture.prevent="touchend($event, index)" @webkit-transition-end="onTransitionEnd(index)" @transitionend="onTransitionEnd(index)" > <img :src="item.avatar" /> <div class="stack-info"> <h2 class="name">{{item.name}}</h2> <p class="tags"> <span class="sex" :class="item.sex"><i class="iconfont" :class="'icon-'+item.sex"></i> {{item.age}}</span> <span class="xz">{{item.starsign}}</span> </p> <p class="distance">{{item.distance}}</p> </div> </li> </ul> </template>
/** * @Desc Vue仿探探|Tinder卡片滑动FlipCard * @Time andy by 2020-10-06 * @About Q:282310962 wx:xy190310 */ <script> export default { props: { pages: { type: Array, default: {} } }, data () { return { basicdata: { start: {}, end: {} }, temporaryData: { isStackClick: true, offsetY: '', poswidth: 0, posheight: 0, lastPosWidth: '', lastPosHeight: '', lastZindex: '', rotate: 0, lastRotate: 0, visible: 3, tracking: false, animation: false, currentPage: 0, opacity: 1, lastOpacity: 0, swipe: false, zIndex: 10 } } }, computed: { // 划出面积比例 offsetRatio () { let width = this.$el.offsetWidth let height = this.$el.offsetHeight let offsetWidth = width - Math.abs(this.temporaryData.poswidth) let offsetHeight = height - Math.abs(this.temporaryData.posheight) let ratio = 1 - (offsetWidth * offsetHeight) / (width * height) || 0 return ratio > 1 ? 1 : ratio }, // 划出宽度比例 offsetWidthRatio () { let width = this.$el.offsetWidth let offsetWidth = width - Math.abs(this.temporaryData.poswidth) let ratio = 1 - offsetWidth / width || 0 return ratio } }, methods: { touchstart (e) { if (this.temporaryData.tracking) { return } // 是否为touch if (e.type === 'touchstart') { if (e.touches.length > 1) { this.temporaryData.tracking = false return } else { // 记录起始位置 this.basicdata.start.t = new Date().getTime() this.basicdata.start.x = e.targetTouches[0].clientX this.basicdata.start.y = e.targetTouches[0].clientY this.basicdata.end.x = e.targetTouches[0].clientX this.basicdata.end.y = e.targetTouches[0].clientY // offsetY在touch事件中没有,只能本身计算 this.temporaryData.offsetY = e.targetTouches[0].pageY - this.$el.offsetParent.offsetTop } // pc操做 } else { this.basicdata.start.t = new Date().getTime() this.basicdata.start.x = e.clientX this.basicdata.start.y = e.clientY this.basicdata.end.x = e.clientX this.basicdata.end.y = e.clientY this.temporaryData.offsetY = e.offsetY } this.temporaryData.isStackClick = true this.temporaryData.tracking = true this.temporaryData.animation = false }, touchmove (e) { this.temporaryData.isStackClick = false // 记录滑动位置 if (this.temporaryData.tracking && !this.temporaryData.animation) { if (e.type === 'touchmove') { e.preventDefault() this.basicdata.end.x = e.targetTouches[0].clientX this.basicdata.end.y = e.targetTouches[0].clientY } else { e.preventDefault() this.basicdata.end.x = e.clientX this.basicdata.end.y = e.clientY } // 计算滑动值 this.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x this.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y let rotateDirection = this.rotateDirection() let angleRatio = this.angleRatio() this.temporaryData.rotate = rotateDirection * this.offsetWidthRatio * 15 * angleRatio } }, touchend (e, index) { if(this.temporaryData.isStackClick) { this.$emit('click', index) this.temporaryData.isStackClick = false } this.temporaryData.isStackClick = true this.temporaryData.tracking = false this.temporaryData.animation = true // 滑动结束,触发判断 // 判断划出面积是否大于0.4 if (this.offsetRatio >= 0.4) { // 计算划出后最终位置 let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth) this.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 : this.temporaryData.poswidth - 200 this.temporaryData.posheight = this.temporaryData.posheight >= 0 ? Math.abs(this.temporaryData.poswidth * ratio) : -Math.abs(this.temporaryData.poswidth * ratio) this.temporaryData.opacity = 0 this.temporaryData.swipe = true this.nextTick() // 不知足条件则滑入 } else { this.temporaryData.poswidth = 0 this.temporaryData.posheight = 0 this.temporaryData.swipe = false this.temporaryData.rotate = 0 } }, nextTick () { // 记录最终滑动距离 this.temporaryData.lastPosWidth = this.temporaryData.poswidth this.temporaryData.lastPosHeight = this.temporaryData.posheight this.temporaryData.lastRotate = this.temporaryData.rotate this.temporaryData.lastZindex = 20 // 循环currentPage this.temporaryData.currentPage = this.temporaryData.currentPage === this.pages.length - 1 ? 0 : this.temporaryData.currentPage + 1 // currentPage切换,总体dom进行变化,把第一层滑动置最低 this.$nextTick(() => { this.temporaryData.poswidth = 0 this.temporaryData.posheight = 0 this.temporaryData.opacity = 1 this.temporaryData.rotate = 0 }) }, onTransitionEnd (index) { let lastPage = this.temporaryData.currentPage === 0 ? this.pages.length - 1 : this.temporaryData.currentPage - 1 // dom发生变化正在执行的动画滑动序列已经变为上一层 if (this.temporaryData.swipe && index === lastPage) { this.temporaryData.animation = true this.temporaryData.lastPosWidth = 0 this.temporaryData.lastPosHeight = 0 this.temporaryData.lastOpacity = 0 this.temporaryData.lastRotate = 0 this.temporaryData.swipe = false this.temporaryData.lastZindex = -1 } }, prev () { this.temporaryData.tracking = false this.temporaryData.animation = true // 计算划出后最终位置 let width = this.$el.offsetWidth this.temporaryData.poswidth = -width this.temporaryData.posheight = 0 this.temporaryData.opacity = 0 this.temporaryData.rotate = '-3' this.temporaryData.swipe = true this.nextTick() }, next () { this.temporaryData.tracking = false this.temporaryData.animation = true // 计算划出后最终位置 let width = this.$el.offsetWidth this.temporaryData.poswidth = width this.temporaryData.posheight = 0 this.temporaryData.opacity = 0 this.temporaryData.rotate = '3' this.temporaryData.swipe = true this.nextTick() }, rotateDirection () { if (this.temporaryData.poswidth <= 0) { return -1 } else { return 1 } }, angleRatio () { let height = this.$el.offsetHeight let offsetY = this.temporaryData.offsetY let ratio = -1 * (2 * offsetY / height - 1) return ratio || 0 }, inStack (index, currentPage) { let stack = [] let visible = this.temporaryData.visible let length = this.pages.length for (let i = 0; i < visible; i++) { if (currentPage + i < length) { stack.push(currentPage + i) } else { stack.push(currentPage + i - length) } } return stack.indexOf(index) >= 0 }, // 非首页样式切换 transform (index) { let currentPage = this.temporaryData.currentPage let length = this.pages.length let lastPage = currentPage === 0 ? this.pages.length - 1 : currentPage - 1 let style = {} let visible = this.temporaryData.visible if (index === this.temporaryData.currentPage) { return } if (this.inStack(index, currentPage)) { let perIndex = index - currentPage > 0 ? index - currentPage : index - currentPage + length style['opacity'] = '1' style['transform'] = 'translate3D(0,0,' + -1 * 60 * (perIndex - this.offsetRatio) + 'px' + ')' style['zIndex'] = visible - perIndex if (!this.temporaryData.tracking) { style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' } } else if (index === lastPage) { style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData.lastPosHeight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.lastRotate + 'deg)' style['opacity'] = this.temporaryData.lastOpacity style['zIndex'] = this.temporaryData.lastZindex style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' } else { style['zIndex'] = '-1' style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')' } return style }, // 首页样式切换 transformIndex (index) { if (index === this.temporaryData.currentPage) { let style = {} style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.rotate + 'deg)' style['opacity'] = this.temporaryData.opacity style['zIndex'] = 10 if (this.temporaryData.animation) { style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = (this.temporaryData.animation ? 300 : 0) + 'ms' } return style } }, } } </script>
组件支持touch和mouse事件,在移动端和PC端都可滑动。动画
另外,点击卡片跳转到卡片详细页面。this
好了,基于Vue实现探探卡片效果就分享到这里。但愿能喜欢~~ ✍flexbox