视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
vue实现滑动堆叠组件
2020-11-27 19:55:55 责编:小采
文档


这次给大家带来vue实现滑动堆叠组件,vue实现滑动堆叠组件的注意事项有哪些,下面就是实战案例,一起来看一下。

前言

嗨,说起探探想必各位程序汪都不陌生(毕竟妹子很多),能在上面丝滑的翻牌子,探探的的堆叠滑动组件起到了关键的作用,下面就来看看如何用vue写一个探探的堆叠组件

一. 功能分析

简单归纳下里面包含的基本功能点:

  • 图片的堆叠

  • 图片第一张的滑动

  • 条件成功后的滑出,条件失败后的回弹

  • 滑出后下一张图片堆叠到顶部

  • 体验优化

  • 根据触摸点的不同,滑动时首图有不同角度偏移

  • 偏移面积判定是否成功滑出

  • 二. 具体实现

    有了归纳好的功能点,我们实现组件的思路会更清晰

    1. 堆叠效果

    堆叠图片效果在网上有大量的实例,实现的方法大同小异,主要通过在父层设定perspective 及perspective-origin ,来实现子层的透视,子层设定好translate3d Z轴数值即可模拟出堆叠效果,具体代码如下

    // 图片堆叠dom
     <!--opacity: 0 隐藏我们不想看到的stack-item层级-->
     <!--z-index: -1 调整stack-item层级"-->
    <ul class="stack">
     <li class="stack-item" style="transform: translate3d(0px, 0px, 0px);opacity: 1;z-index: 10;"><img src="1.png" alt="01"></li>
     <li class="stack-item" style="transform: translate3d(0px, 0px, -60px);opacity: 1;z-index: 1"><img src="2.png" alt="02"></li>
     <li class="stack-item" style="transform: translate3d(0px, 0px, -120px);opacity: 1;z-index: 1"><img src="3.png" alt="03"></li>
     <li class="stack-item" style="transform: translate3d(0px, 0px, -180px);opacity: 0;z-index: -1"><img src="4.png" alt="04"></li>
     <li class="stack-item" style="transform: translate3d(0px, 0px, -180px);opacity: 0;z-index: -1"><img src="5.png" alt="05"></li>
    </ul>
    <style>
    .stack {
     width: 100%;
     height: 100%;
     position: relative;
     perspective: 1000px; //子元素视距
     perspective-origin: 50% 150%; //子元素透视位置
     -webkit-perspective: 1000px;
     -webkit-perspective-origin: 50% 150%;
     margin: 0;
     padding: 0;
     }
     .stack-item{
     background: #fff;
     height: 100%;
     width: 100%;
     border-radius: 4px;
     text-align: center;
     overflow: hidden;
     }
     .stack-item img {
     width: 100%;
     display: block;
     pointer-events: none;
     }
    </style>

    上面只是一组静态代码,我们希望得到的是vue组件,所以需要先建立一个组件模板stack.vue,在模板中我们可以使用v-for,遍历出stack节点,使用:style 来修改各个item的style,代码如下

    <template>
     <ul class="stack">
     <li class="stack-item" v-for="(item, index) in pages" :style="[transform(index)]">
     <img :src="item.src">
     </li>
     </ul>
    </template>
    <script>
    export default {
     props: {
     // pages数据包含基础的图片数据
     pages: {
     type: Array,
     default: []
     }
     },
     data () {
     return {
     // basicdata数据包含组件基本数据
     basicdata: {
     currentPage: 0 // 默认首图的序列
     },
     // temporaryData数据包含组件临时数据
     temporaryData: {
     opacity: 1, // 记录opacity
     zIndex: 10, // 记录zIndex
     visible: 3 // 记录默认显示堆叠数visible
     }
     }
     },
     methods: {
     // 遍历样式
     transform (index) {
     if (index >= this.basicdata.currentPage) {
     let style = {}
     let visible = this.temporaryData.visible
     let perIndex = index - this.basicdata.currentPage
     // visible可见数量前滑块的样式
     if (index <= this.basicdata.currentPage + visible - 1) {
     style['opacity'] = '1'
     style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')'
     style['zIndex'] = visible - index + this.basicdata.currentPage
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     } else {
     style['zIndex'] = '-1'
     style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
     }
     return style
     }
     }
     }
    }
    </script>

    关键点

    :style 可以绑定对象的同时,也可以绑定数组和函数,这在遍历的时候很有用
    最基本的dom结构已经构建完毕,下一步是让首张图片“动”起来

    2. 图片滑动

    图片滑动效果,在很多场景中都有出现,其原理无非是监听touchs事件,得到位移,再通过translate3D改变目标位移,因此我们要实现的步骤如下

  • 对stack进行touchs事件的绑定

  • 监听并储存手势位置变化的数值

  • 改变首图css属性中translate3D的x,y值

  • #### 具体实现

    在vue框架中,不建议直接操作节点,而是通过指令v-on对元素进行绑定,因此我们将绑定都写在v-for遍历里,通过index进行判断其是否是首图,再使用:style修改首页的样式,具体代码如下:

    <template>
     <ul class="stack">
     <li class="stack-item" v-for="(item, index) in pages"
     :style="[transformIndex(index),transform(index)]"
     @touchstart.stop.capture="touchstart"
     @touchmove.stop.capture="touchmove"
     @touchend.stop.capture="touchend"
     @mousedown.stop.capture="touchstart"
     @mouseup.stop.capture="touchend"
     @mousemove.stop.capture="touchmove">
     <img :src="item.src">
     </li>
     </ul>
    </template>
    <script>
    export default {
     props: {
     // pages数据包含基础的图片数据
     pages: {
     type: Array,
     default: []
     }
     },
     data () {
     return {
     // basicdata数据包含组件基本数据
     basicdata: {
     start: {}, // 记录起始位置
     end: {}, // 记录终点位置
     currentPage: 0 // 默认首图的序列
     },
     // temporaryData数据包含组件临时数据
     temporaryData: {
     poswidth: '', // 记录位移
     posheight: '', // 记录位移
     tracking: false // 是否在滑动,防止多次操作,影响体验
     }
     }
     },
     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
     }
     // 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.tracking = true
     },
     touchmove (e) {
     // 记录滑动位置
     if (this.temporaryData.tracking && !this.temporaryData.animation) {
     if (e.type === 'touchmove') {
     this.basicdata.end.x = e.targetTouches[0].clientX
     this.basicdata.end.y = e.targetTouches[0].clientY
     } else {
     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
     }
     },
     touchend (e) {
     this.temporaryData.tracking = false
     // 滑动结束,触发判断
     },
     // 非首页样式切换
     transform (index) {
     if (index > this.basicdata.currentPage) {
     let style = {}
     let visible = 3
     let perIndex = index - this.basicdata.currentPage
     // visible可见数量前滑块的样式
     if (index <= this.basicdata.currentPage + visible - 1) {
     style['opacity'] = '1'
     style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')'
     style['zIndex'] = visible - index + this.basicdata.currentPage
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     } else {
     style['zIndex'] = '-1'
     style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
     }
     return style
     }
     },
     // 首页样式切换
     transformIndex (index) {
     // 处理3D效果
     if (index === this.basicdata.currentPage) {
     let style = {}
     style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px)'
     style['opacity'] = 1
     style['zIndex'] = 10
     return style
     }
     }
     }
    }
    </script>

    3. 条件成功后的滑出,条件失败后的回弹

    条件的触发判断是在touchend/mouseup后进行,在这里我们先用简单的条件进行判定,同时给予首图弹出及回弹的效果,代码如下

    <template>
     <ul class="stack">
     <li class="stack-item" v-for="(item, index) in pages"
     :style="[transformIndex(index),transform(index)]"
     @touchmove.stop.capture="touchmove"
     @touchstart.stop.capture="touchstart"
     @touchend.stop.capture="touchend"
     @mousedown.stop.capture="touchstart"
     @mouseup.stop.capture="touchend"
     @mousemove.stop.capture="touchmove">
     <img :src="item.src">
     </li>
     </ul>
    </template>
    <script>
    export default {
     props: {
     // pages数据包含基础的图片数据
     pages: {
     type: Array,
     default: []
     }
     },
     data () {
     return {
     // basicdata数据包含组件基本数据
     basicdata: {
     start: {}, // 记录起始位置
     end: {}, // 记录终点位置
     currentPage: 0 // 默认首图的序列
     },
     // temporaryData数据包含组件临时数据
     temporaryData: {
     poswidth: '', // 记录位移
     posheight: '', // 记录位移
     tracking: false, // 是否在滑动,防止多次操作,影响体验
     animation: false, // 首图是否启用动画效果,默认为否
     opacity: 1 // 记录首图透明度
     }
     }
     },
     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
     }
     // 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.tracking = true
     this.temporaryData.animation = false
     },
     touchmove (e) {
     // 记录滑动位置
     if (this.temporaryData.tracking && !this.temporaryData.animation) {
     if (e.type === 'touchmove') {
     this.basicdata.end.x = e.targetTouches[0].clientX
     this.basicdata.end.y = e.targetTouches[0].clientY
     } else {
     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
     }
     },
     touchend (e) {
     this.temporaryData.tracking = false
     this.temporaryData.animation = true
     // 滑动结束,触发判断
     // 简单判断滑动宽度超出100像素时触发滑出
     if (Math.abs(this.temporaryData.poswidth) >= 100) {
     // 最终位移简单设定为x轴200像素的偏移
     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
     // 不满足条件则滑入
     } else {
     this.temporaryData.poswidth = 0
     this.temporaryData.posheight = 0
     }
     },
     // 非首页样式切换
     transform (index) {
     if (index > this.basicdata.currentPage) {
     let style = {}
     let visible = 3
     let perIndex = index - this.basicdata.currentPage
     // visible可见数量前滑块的样式
     if (index <= this.basicdata.currentPage + visible - 1) {
     style['opacity'] = '1'
     style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')'
     style['zIndex'] = visible - index + this.basicdata.currentPage
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     } else {
     style['zIndex'] = '-1'
     style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
     }
     return style
     }
     },
     // 首页样式切换
     transformIndex (index) {
     // 处理3D效果
     if (index === this.basicdata.currentPage) {
     let style = {}
     style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px)'
     style['opacity'] = this.temporaryData.opacity
     style['zIndex'] = 10
     if (this.temporaryData.animation) {
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     }
     return style
     }
     }
     }
    }
    </script>

    4. 滑出后下一张图片堆叠到顶部

    重新堆叠是组件最后一个功能,同时也是最重要和复杂的功能。在我们的代码里,stack-item的排序依赖绑定:style的transformIndex和transform函数,函数里判定的条件是currentPage,那是不是改变currentPage,让其+1,即可完成重新堆叠呢?

    答案没有那么简单,因为我们滑出是动画效果,会进行300ms的时间,而currentPage变化引起的重排,会立即变化,打断动画的进行。因此我们需要先修改transform函数的排序条件,后改变currentPage。

    #### 具体实现

  • 修改transform函数排序条件

    让currentPage+

  • 添加onTransitionEnd事件,在滑出结束后,重新放置stack列表中

  • 代码如下:

    <template>
     <ul class="stack">
     <li class="stack-item" v-for="(item, index) in pages"
     :style="[transformIndex(index),transform(index)]"
     @touchmove.stop.capture="touchmove"
     @touchstart.stop.capture="touchstart"
     @touchend.stop.capture="touchend"
     @mousedown.stop.capture="touchstart"
     @mouseup.stop.capture="touchend"
     @mousemove.stop.capture="touchmove"
     @webkit-transition-end="onTransitionEnd"
     @transitionend="onTransitionEnd"
     >
     <img :src="item.src">
     </li>
     </ul>
    </template>
    <script>
    export default {
     props: {
     // pages数据包含基础的图片数据
     pages: {
     type: Array,
     default: []
     }
     },
     data () {
     return {
     // basicdata数据包含组件基本数据
     basicdata: {
     start: {}, // 记录起始位置
     end: {}, // 记录终点位置
     currentPage: 0 // 默认首图的序列
     },
     // temporaryData数据包含组件临时数据
     temporaryData: {
     poswidth: '', // 记录位移
     posheight: '', // 记录位移
     lastPosWidth: '', // 记录上次最终位移
     lastPosHeight: '', // 记录上次最终位移
     tracking: false, // 是否在滑动,防止多次操作,影响体验
     animation: false, // 首图是否启用动画效果,默认为否
     opacity: 1, // 记录首图透明度
     swipe: false // onTransition判定条件
     }
     }
     },
     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
     }
     // 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.tracking = true
     this.temporaryData.animation = false
     },
     touchmove (e) {
     // 记录滑动位置
     if (this.temporaryData.tracking && !this.temporaryData.animation) {
     if (e.type === 'touchmove') {
     this.basicdata.end.x = e.targetTouches[0].clientX
     this.basicdata.end.y = e.targetTouches[0].clientY
     } else {
     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
     }
     },
     touchend (e) {
     this.temporaryData.tracking = false
     this.temporaryData.animation = true
     // 滑动结束,触发判断
     // 简单判断滑动宽度超出100像素时触发滑出
     if (Math.abs(this.temporaryData.poswidth) >= 100) {
     // 最终位移简单设定为x轴200像素的偏移
     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.temporaryData.lastPosWidth = this.temporaryData.poswidth
     this.temporaryData.lastPosHeight = this.temporaryData.posheight
     // currentPage+1 引发排序变化
     this.basicdata.currentPage += 1
     // currentPage切换,整体dom进行变化,把第一层滑动置零
     this.$nextTick(() => {
     this.temporaryData.poswidth = 0
     this.temporaryData.posheight = 0
     this.temporaryData.opacity = 1
     })
     // 不满足条件则滑入
     } else {
     this.temporaryData.poswidth = 0
     this.temporaryData.posheight = 0
     this.temporaryData.swipe = false
     }
     },
     onTransitionEnd (index) {
     // dom发生变化后,正在执行的动画滑动序列已经变为上一层
     if (this.temporaryData.swipe && index === this.basicdata.currentPage - 1) {
     this.temporaryData.animation = true
     this.temporaryData.lastPosWidth = 0
     this.temporaryData.lastPosHeight = 0
     this.temporaryData.swipe = false
     }
     },
     // 非首页样式切换
     transform (index) {
     if (index > this.basicdata.currentPage) {
     let style = {}
     let visible = 3
     let perIndex = index - this.basicdata.currentPage
     // visible可见数量前滑块的样式
     if (index <= this.basicdata.currentPage + visible - 1) {
     style['opacity'] = '1'
     style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')'
     style['zIndex'] = visible - index + this.basicdata.currentPage
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     } else {
     style['zIndex'] = '-1'
     style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
     }
     return style
     // 已滑动模块释放后
     } else if (index === this.basicdata.currentPage - 1) {
     let style = {}
     // 继续执行动画
     style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData.lastPosHeight + 'px' + ',0px)'
     style['opacity'] = '0'
     style['zIndex'] = '-1'
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     return style
     }
     },
     // 首页样式切换
     transformIndex (index) {
     // 处理3D效果
     if (index === this.basicdata.currentPage) {
     let style = {}
     style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px)'
     style['opacity'] = this.temporaryData.opacity
     style['zIndex'] = 10
     if (this.temporaryData.animation) {
     style['transitionTimingFunction'] = 'ease'
     style['transitionDuration'] = 300 + 'ms'
     }
     return style
     }
     }
     }
    }
    </script>

    ok~ 完成了上面的四步,堆叠组件的基本功能就已经实现,快来看看效果吧

    相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!

    推荐阅读:

    Native怎么使用fetch实现图片上传功能

    vue.js移动数组位置并实时更新视图

    下载本文
    显示全文
    专题