昨天写了一篇侧边菜单组件的文章,阅读人数挺多的,心里很欣喜(偷着乐,第一篇文章有这么多人看)!乘着这股劲,今天在继续写一篇咱们平时工做中更经常使用的滑动轮播组件的文章。javascript
老规矩,我们先看作成后的效果,而后我们再一步步的开始制做:css
在实际的工做中,我们轮播中的内容形式可能有不少种:图片、文本、视频、其余DOM结构等。因此我们的轮播组件必须能知足这几种应用状况。那么咱们能够把组件分两部分:html
咱们如今这定义子组件的名称为swiper-item;父组件名称为swiperjava
首先我们的子组件中负责渲染自定义的内容,则子组件中须要一个插槽slot。 web
swiper-item:微信
<template> <div class="r-swiper-item"> <slot></slot> </div> </template>
其次父组件中负责通用的功能,以及轮播的总体架构,其DOM结构以下。 架构
swiper:函数
<template> <div class="r-swiper"> <slot></slot> <slot name="indicator"> <div class="indicator"></div> </slot> </div> </template>
默认插槽在使用的时候渲染我们轮播的子项,一般为swiper-item;indicator插槽用来自定义指示器的样式,由于在实际使用过程当中指示器样式极可能是须要定制的。flex
移动端的视图大小有限,子项的大小通常是父组件的所有可视视图。
swiper-item:动画
<style lang="scss"> .r-swiper-item{ position: absolute; left:0; top:0; width: 100%; height: 100%; } </style>
下面的vw是一种移动端的适配方案(https://www.w3cplus.com/css/t...)。其余的适配方案还有淘宝的flexible,这个css你们根据本身的适配方案更改下,这里不作过多描述,你们感兴趣的自行百度。
swiper:
<style lang="scss"> .r-swiper{ position: relative; overflow: hidden; .indicator{ position: absolute; right: 3vw; bottom: 3vw; width: 10vw; height: 10vw; line-height: 10vw; border-radius: 5vw; text-align: center; background-color: rgba(0,0,0,.5); color: #fff; font-size: 14px; } } </style>
老规矩,写JS代码前我们先理清交互逻辑:
初始化的时候须要:
swiper-item:
export default { mounted () { this.$nextTick(() => { this.$parent.init() }) }, beforeDestroy () { this.$nextTick(() => { this.$parent.destroy() }) } }
swiper:
<template> <div ref="swiper" class="r-swiper" :style="{height: _height}" @touchstart="moveStart" @touchmove="moving" @touchend="moveEnd"> <slot></slot> <slot name="indicator"> <div class="indicator"></div> </slot> </div> </template> <script> let each = function (ary, callback) { for (let i = 0, l = ary.length; i < l; i++) { if (callback(ary[i], i) === false) break } } export default{ props: { // 设置父容器的高度,使用过程当中自定义 height: { type: [Number, String], default: 'auto' } }, data () { return { _width: 0, duration: 300, container: null, items: [], active: 0, start: { x: 0, y: 0 }, move: { x: 0, y: 0 } } }, computed: { // 根据传入参数类型设置正确的高度样式 _height () { if (typeof this.height === 'number') { return this.height + 'px' } else { return this.height } } }, methods: { init () { // 得到父容器节点 this.container = this.$refs.swiper // 得到全部的子节点 this.items = this.container.querySelectorAll('.r-swiper-item') this.updateItemWidth() this.setTransform() this.setTransition('none') }, // 获取父容器宽度,而且更新全部的子节点宽度,由于咱们默认全部子节点的宽高等于父节点的宽高 updateItemWidth () { this._width = this.container.offsetWidth || document.documentElement.offsetWidth }, // 根据当前活动子项的下标计算各个子项的X轴位置 // 计算公式(子项的下标 - 当前活动下标) * 子项宽度 + 偏移(手指移动距离); setTransform (offset) { offset = offset || 0 each(this.items, (item, i) => { let distance = (i - this.active) * this._width + offset let transform = `translate3d(${distance}px, 0, 0)` item.style.webkitTransform = transform item.style.transform = transform }) }, // 给每个子项添加transition过分动画 setTransition (duration) { duration = duration || this.duration duration = typeof duration === 'number' ? (duration + 'ms') : duration each(this.items, (item) => { item.style.webkitTransition = duration item.style.transition = duration }) }, moveStart (e) {}, moving (e) {}, moveEnd (e) {}, destroy () { this.removeEvent() } } } </script>
初始化完成后,我们接下来编写我们的moveStart、moving、moveEnd三个touch事件,在methods中完善这三个函数,并添加一个临界值sensitivity以及一个阻力系数,阻力系数有啥用,注意看下面代码的注释:
data () { return { sensitivity: 60, resistance: 0.3 } }, methods: { moveStart (e) { this.start.x = e.changedTouches[0].pageX this.start.y = e.changedTouches[0].pageY this.setTransition('none') }, moving (e) { e.preventDefault() e.stopPropagation() let distanceX = e.changedTouches[0].pageX - this.start.x let distanceY = e.changedTouches[0].pageY - this.start.y if (Math.abs(distanceX) > Math.abs(distanceY)) { this.isMoving = true this.move.x = this.start.x + distanceX this.move.y = this.start.y + distanceY // 当活动子项为第一项且手指向右滑动或者活动项为最后一项切向左滑动的时候,添加阻力,造成一个拉弹簧的效果 if ((this.active === 0 && distanceX > 0) || (this.active === (this.items.length - 1) && distanceX < 0)) { distanceX = distanceX * this.resistance } this.setTransform(distanceX) } }, moveEnd (e) { if (this.isMoving) { e.preventDefault() e.stopPropagation() let distance = this.move.x - this.start.x if (Math.abs(distance) > this.sensitivity) { if (distance < 0) { this.next() } else { this.prev() } } else { this.back() } this.reset() this.isMoving = false } }, // 切换下一屏 next () {}, // 切换下一屏 prev () {}, // 若是滑动达不到阈值,全部元素重置回以前状态 back () {}, // 重置动画中用到的一些变量 reset () {}, destroy () { this.setTransition('none') } }
接下来我们完善下next、prev、back、reset函数:
next () { let index = this.active + 1 // 运用动画切换到指定下标的子项 this.go(index) }, prev () { let index = this.active - 1 // 运用动画切换到指定下标的子项 this.go(index) }, reset () { this.start.x = 0 this.start.y = 0 this.move.x = 0 this.move.y = 0 }, back () { this.setTransition() this.setTransform() }, go (index) {}
go函数用来作轮播切换的效果。咱们在写代码的过程当中,能够先定义一个函数来作某个事情,而后再后面用代码来实现逻辑,这样的我们写代码过程当中的思路就会很清晰。接下来实现go函数:
// 运用动画切换到指定下标的子项 go (index) { this.active = index if (this.active < 0) { this.active = 0 } else if (this.active > this.items.length - 1) { this.active = this.items.length - 1 } this.$emit('change', this.active) this.setTransition() this.setTransform() }
到此为止,我们就已经完成了一个初步的滑动切换轮播图的功能了。可是不少时候,咱们的轮播是须要自动播放的,那么如何在如今的基础上增长自动轮播呢?请你们本身思考下,哈哈。下面咱们把当前代码整合下:
<template> <div ref="swiper" class="r-swiper" :style="{height: _height}" @touchstart="moveStart" @touchmove="moving" @touchend="moveEnd"> <slot></slot> <slot name="indicator"> <div class="indicator"></div> </slot> </div> </template> <script> let each = function (ary, callback) { for (let i = 0, l = ary.length; i < l; i++) { if (callback(ary[i], i) === false) break } } export default{ props: { height: { type: [Number, String], default: 'auto' } }, data () { return { isMoving: false, _width: 0, duration: 300, container: null, items: [], active: 0, sensitivity: 60, // 触发切换的阈值 resistance: 0.3, // 阻力系数 start: { x: 0, y: 0 }, move: { x: 0, y: 0 } } }, computed: { _height () { if (typeof this.height === 'number') { return this.height + 'px' } else { return this.height } } }, methods: { init () { this.container = this.$refs.swiper this.items = this.container.querySelectorAll('.r-swiper-item') this.updateItemWidth() this.setTransform() this.setTransition('none') }, updateItemWidth () { this._width = this.container.offsetWidth || document.documentElement.offsetWidth }, setTransform (offset) { offset = offset || 0 each(this.items, (item, i) => { let distance = (i - this.active) * this._width + offset let transform = `translate3d(${distance}px, 0, 0)` item.style.webkitTransform = transform item.style.transform = transform }) }, setTransition (duration) { duration = duration || this.duration duration = typeof duration === 'number' ? (duration + 'ms') : duration each(this.items, (item) => { item.style.webkitTransition = duration item.style.transition = duration }) }, moveStart (e) { this.start.x = e.changedTouches[0].pageX this.start.y = e.changedTouches[0].pageY this.setTransition('none') }, moving (e) { e.preventDefault() e.stopPropagation() let distanceX = e.changedTouches[0].pageX - this.start.x let distanceY = e.changedTouches[0].pageY - this.start.y if (Math.abs(distanceX) > Math.abs(distanceY)) { this.isMoving = true this.move.x = this.start.x + distanceX this.move.y = this.start.y + distanceY if ((this.active === 0 && distanceX > 0) || (this.active === (this.items.length - 1) && distanceX < 0)) { distanceX = distanceX * this.resistance } this.setTransform(distanceX) } }, moveEnd (e) { if (this.isMoving) { e.preventDefault() e.stopPropagation() let distance = this.move.x - this.start.x if (Math.abs(distance) > this.sensitivity) { if (distance < 0) { this.next() } else { this.prev() } } else { this.back() } this.reset() this.isMoving = false } }, next () { let index = this.active + 1 this.go(index) }, prev () { let index = this.active - 1 this.go(index) }, reset () { this.start.x = 0 this.start.y = 0 this.move.x = 0 this.move.y = 0 }, back () { this.setTransition() this.setTransform() }, destroy () { this.setTransition('none') this.clearTimer() }, go (index) { this.active = index if (this.active < 0) { this.active = this.isMoving ? 0 : this.items.length - 1 } else if (this.active > this.items.length - 1) { this.active = this.isMoving ? this.items.length - 1 : 0 } this.$emit('change', this.active) this.setTransition() this.setTransform() } } } </script> <style lang="scss"> @import "../style/color.scss"; @import "../style/fontSize.scss"; @import "../style/mixin.scss"; .r-swiper{ position: relative; overflow: hidden; .indicator{ position: absolute; right: 3vw; bottom: 3vw; width: 10vw; height: 10vw; line-height: 10vw; border-radius: 5vw; text-align: center; background-color: rgba(0,0,0,.5); color: #fff; font-size: 14px; } } </style>
今天写这篇文章的时候发现有两个兄弟给我微信转了钱,很谢谢这两个兄弟,感谢大家的支持。其实说实话,我花心思写这个主要目的不是为了钱,而是兴趣,不然我用这个时间用来作点私活什么的收入比这个多多了。只是看到你们的支持,心里颇有成就感,尽管不少时候只有1分钱,因此也但愿你们有钱的捧个钱场,没钱的捧我的场,哈哈。(未完待续...)