探探滑动组件

tantan-slider

使用Vue有一段时间了,对于组件的编写不是很熟悉,找一个感兴趣的功能组件,模仿(抄袭)过来作为练习。因为原代码用的是webpack1,所以有一些修改,在还原的过程中,也会按照自己的理解整理思路。

原文地址在http://web.jobbole.com/94134/

摘要

文件目录结构

.
├── dist                         
├── src                         
│   ├── components              
│   │   └── stack.vue 
│   ├── img          
│   │   └── //...
│   └── main.js  
│   └── App.vue
│   └── exportModule.js
├── webpack.config.js
.

原作者并没有使用Vue-cli的webpack配置,所以我也写了一个配置文件,权当复习webpack的配置了吧(雾)

在原配置文件基础上,给webpack添加了这些功能:

  • 使用purify-css清除未使用的CSS
  • html文件发布
  • postcss自动处理CSS3属性前缀

具体的配置在文件中。

在打包的过程中还发现一个问题:
vue-runtime-warning]

运行时构建不包含模块编译器,所以不支持template选项,只能使用render选项。单文件组件中所写的模板只能在vue模块下的vue.js才可以构建。

注意这一行:

1
2
3
4
5
6
7
8

// webpack.config.js
resolve: {
//...
alias: {
'vue': 'vue/dist/vue.js'
}
}

开始

该组件的功能:

  1. 堆叠图片结构
  2. 首页图的滑动
  3. 条件判断成功后滑出,失败回弹
  4. 下一张图片叠加到顶部

功能实现

1. 叠加效果

①. 在父级层设定prespectiveprespective-orgin,实现子层的透视,子层各元素设定translate3D值形成效果。
②. 在子组件上遍历从父组件中传来的图片props属性,得到图片数据,使用:style绑定样式

首先,单文件组件内的参数决定了在可视范围内,能够看见几张图。剩下的图片使用zIndex暂时隐藏,可见的这几张图片样式与参数的关系是判断的先决条件。

parent-components]

在子组件中使用:
child-components]

现在大概是这个样子:
static]

2. 滑动效果

让首页图片能够有滑动的效果,在图片上添加一些事件,使用translate将其移动。图片滑出原序列中。步骤如下:

  • touchs事件的绑定
  • 监听并储存手势位置变化的数值
  • 改变首图的translate值,形成滑动的效果

在滑动的过程中,为了避免出现不同图片同时滑动的混乱,在temporaryData中加入了tracking变量作为判断。同样,在图片的translate过程中也要加入这样的判断,变量为animation。默认值均为false

slide]

仍然使用:style绑定样式,完成图片的移动行为

slide-behavior]

现在效果是这样的:
transform]

3. 判断成功则滑出,否则回弹

在以上代码的基础上,多加一个判断,让判断成功的滑出原序列,失败的回弹到序列中。判断的依据比较简单:一段滑动过程结束时,x轴偏移量大于200px,则图片滑出。

judgement]

效果是这样的:
transform]

4.划出后下一张图片浮出,堆叠到顶部

经过以上一步之后,DOM会重新更新,按照之前的做法,只需要把绑定样式的函数中,即transformtransformIndex中的currentPage加1即可,但是这么做会再次触发DOM更新,打断动画的执行。所以仅仅是修改currentPage是不够的。

原有的第一张图片移出原序列,完成更新DOM(touchend事件中currentPage+1)后到下一张图片浮出,堆叠至顶部动作(tranform函数功能)中间,增加对于函数排序的判断,使移出的照片能顺利执行动画。而新的首图也要形成动画效果,顺利过渡为首图。总结起来就是:

  • currentPage + 1
  • 增加transform函数排序条件
  • 将新的首图顺利过渡至顶部

Vue的响应式原理中,当事件循环结束时,nextTick会在异步队列中排在首位。所以,在这里可以写入新首图的视觉效果初始化。

stack]

写到这里发现一个问题:
当点击移动图片时,如果点击的位置非常靠近图片的边缘,移动时会形成图片拖动的效果。

就像这样:
solution]

最初我才想是因为边界条件的判断导致了这个问题,最后发现其实很简单,这样的问题只会在鼠标或手势动作脱离图片时出现,所以只需要添加相应事件即可。

solve]

issue:

  1. 只使用x轴偏移量作为判断,图片在x、y轴上进行线性移动,动画效果非常生硬,探探上作出了添加角度偏移的效果优化。

优化提升

为了方便阅读,以下部分代码直出,不使用图片形式展示。

[issue1]:在原版本中,图片是否脱离原序列的判断为:滑动结束时,滑动的图片位移大于200px

1
2
3
4
5
if(Math.abs(this.temporaryData.posWidth) >= 200) {
// 图片形成位移动画
} else {
// 图片回弹到原序列中
}

修改为:图片滑出的面积占图片的比例是否大于0.4

1
2
3
4
5
6
7

// 判断划出面积是否大于0.4
if (this.offsetRatio >= 0.4) {
// 图片形成位移动画
} else {
// 图片回弹到原序列中
}

判断的依据绑定在计算属性中:

1
2
3
4
5
6
7
8
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
}

同时,在滑动过程中增加偏移角度的计算。偏移角度 = 偏移方向正负值 划出宽度比例 15 * 划出高度比例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
touchmove(e) {

// 记录位移值
if(this.temporaryData.tracking && !this.temporaryData.animation) {
if(e.type === 'touchmove') {
//...
} else {
//...
}

/** compute slide numerical
...*/

let
// 计算偏移角度
let rotateDirection = this.rotateDirection()
let angleRatio = this.angleRatio()
this.temporaryData.rotate = rotateDirection * this.offsetWidthRatio * 15 * angleRatio
}
}

所以在计算属性中有这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
computed: {

// 划出面积比例
offsetRatio() {
//...
},

// 划出宽度比例
offsetWidthRatio() {
let width = this.$el.offsetWidth
let offsetWidth = width - Math.abs(this.temporaryData.posWidth)
let ratio = 1 - offsetWidth / width || 0
return ratio
},

// 划出高度比例
angleRatio() {
let height = this.$el.offsetHeight
let offsetY = this.temporaryData.offsetY
let ratio = -1 * (2 * offsetY / height - 1)
return ratio || 0
}
}

添加了偏移角度的计算,所以在绑定样式的位移函数中也会做出相应的修改,这部分会在代码中注释说明。为了功能更完整,会在图片下添加按钮,达到相应的滑动功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// App.vue
<div id="app">
<div>
<div class="stack-wrapper">
<stack ref="stack" :pages="someList"/>
</div>
<div class="controls">
<button @click="prev" class="button">
<i class="prev"></i>
<span class="text-hidden">prev</span>
</button>
<button @click="next" class="button">
<i class="next"></i>
<span class="text-hidden">next</span>
</button>
</div>
</div>
</div>

<script>
export default{
// ...
methods: {
prev () {
this.$refs.stack.$emit('prev')
},
next () {
this.$refs.stack.$emit('next')
}
}
}
</script>

主文件中执行事件,在单文件组件中绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// stack.vue
// ...
mounted () {
// 绑定事件
this.$on('next', () => {
this.next()
})
this.$on('prev', () => {
this.prev()
})
},
methods: {
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()
}
}

总结

组件写到这里就结束了。演示在这里:TheEnd]

其实里面的方法并不复杂,关键点之一在于理清楚DOM更新后的事件执行顺序,在这点上最好要深入到Event Loop