1.最初的思路
當(dāng)前窗口由第一個(gè)變成第二個(gè):先把第二個(gè)放在第一個(gè)的后面南捂,然后第一個(gè)做左滑動(dòng)的動(dòng)畫,第二個(gè)也做向左滑動(dòng)的動(dòng)畫旧找,這樣第二個(gè)就出現(xiàn)在當(dāng)前窗口了溺健,然后把第一個(gè)去掉。
第二個(gè)到第三個(gè)也是一樣的钮蛛,二和三同時(shí)做左滑的動(dòng)畫鞭缭,三就出現(xiàn)在了當(dāng)前窗口,然后去掉二魏颓。
第三個(gè)到第一個(gè):把第一個(gè)放在第三個(gè)的后面岭辣,同時(shí)做左滑的動(dòng)畫,然后干掉第三個(gè)
1.1. 最初的代碼
- sliders.vue
<template>
<div class="lf-sliders">
<div class="lf-sliders-window" ref="window">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "LiFaSliders",
}
</script>
<style scoped>
</style>
- demo.vue
<template>
<div>
<lf-sliders>
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</lf-sliders>
</div>
</template>
<script>
import LfSliders from './slides.vue'
export default {
name: "demo",
components: {
LfSliders
},
data(){
return {
}
},
methods: {
},
created() {
}
}
</script>
<style scoped>
.box{
width: 100px;
height: 100px;
background: red;
border: 1px solid gray;
}
</style>
我們要讓它其中的一個(gè)顯示琼开,但是我們的組件里只有一個(gè)slot易结,我們?cè)趺茨苤滥膫€(gè)是第一個(gè)?
(1).通過(guò)vue的this.$children
來(lái)獲裙窈颉(但是他只能獲取子組件的搞动,而我們當(dāng)前組件沒(méi)有子組件)
(2). dom操作通過(guò)this.$refs.window.children
- slides.vue
mounted() {
console.log(this.$children)
console.log(this.$refs.window.children)
}
我們不想用dom操作,所以需要加一個(gè)子組件lf-sliders-item
1.2. 改進(jìn)后的代碼
- demo.vue
<template>
<div>
<lf-sliders>
<lf-sliders-item>
<div class="box">1</div>
</lf-sliders-item>
<lf-sliders-item>
<div class="box">2</div>
</lf-sliders-item>
<lf-sliders-item>
<div class="box">3</div>
</lf-sliders-item>
</lf-sliders>
</div>
</template>
- slliders-item.vue
<template>
<div class="lf-sliders-item">
<slot></slot>
</div>
</template>
我們需要給每一個(gè)item一個(gè)屬性visible來(lái)控制它是否顯示渣刷,正常情況下我們應(yīng)該在sliders里傳入一個(gè)visible鹦肿,然后在item中通過(guò)props接受這個(gè)visible,但是我們sliders里面用的是slot沒(méi)有l(wèi)f-sliders-item標(biāo)簽辅柴,所以我們沒(méi)法在item中通過(guò)props接收這個(gè)visible箩溃,只能在item里的data中傳入一個(gè)visible
- lf-sliders-item
<template>
<div class="lf-sliders-item" v-if="visible">
<slot></slot>
</div>
</template>
<script>
export default {
name: "sliders-item",
data(){
return {
visible: false
}
}
}
</script>
- sliders.vue
<template>
<div class="lf-sliders">
<div class="lf-sliders-window" ref="window">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "LiFaSliders",
mounted() {
let first = this.$children[0]
first.visible = true
}
}
</script>
1.3. 簡(jiǎn)單的實(shí)現(xiàn)從1到2的過(guò)程
注意:因?yàn)槲覀儼岩婚_(kāi)始進(jìn)入前的位置設(shè)為了100%,而如果我們不給item絕對(duì)定位的話碌嘀,由于第二張本來(lái)就在第一個(gè)后面涣旨,所以你再加上一開(kāi)始的100%中間就會(huì)有一張的空白,但是如果我們都絕對(duì)定位的話股冗,外層就會(huì)沒(méi)有高度霹陡,所以我們必須至少保證有一個(gè)不絕對(duì)定位,又因?yàn)檩啿ナ窍犬?dāng)前這張離開(kāi)止状,然后才后面的進(jìn)來(lái)烹棉,所以我們需要設(shè)置離開(kāi)的時(shí)候那一張絕對(duì)定位
- slides.vue
<template>
<div class="lf-slides">
<div class="lf-slides-window" ref="window">
<div class="lf-slides-wrapper">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LiFaslides",
mounted() {
let first = this.$children[0]
let second = this.$children[1]
first.visible = true
setTimeout(()=>{
first.visible = false
second.visible = true
},3000)
}
}
</script>
<style scoped lang="scss">
.lf-slides{
display: inline-block;
border: 1px solid black;
&-wrapper{
position: relative;
display: flex;
}
}
</style>
- slides-item.vue
<template>
<transition name="fade">
<div class="lf-slides-item" v-if="visible">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: "slides-item",
data() {
return {
visible: false
}
}
}
</script>
<style scoped lang="scss">
.fade-enter-active, .fade-leave-active {
transition: all .3s;
}
.fade-enter {
transform: translateX(100%);
}
//保證有一個(gè)不絕對(duì)定位,可以讓父級(jí)有寬高
.fade-leave-active{
position: absolute;
left: 0;
top: 0;
}
.fade-leave-to {
transform: translateX(-100%);
}
</style>
基本實(shí)現(xiàn)輪播
給sliders傳入一個(gè)selected和給item傳入一個(gè)name怯疤,selected的值就是item的name浆洗,對(duì)應(yīng)的就是哪個(gè)先顯示,這個(gè)selected還得通知到每個(gè)item組件集峦,因此每個(gè)item還需要聲明一個(gè)selected济舆,默認(rèn)為undefined,在sliders中通過(guò)mounted遍歷到每個(gè)item組件讓他們的selected等于sliders里的selected,如果沒(méi)傳就等于item第一個(gè)組件的name,之后我們只需要更新selected就可以被啼,但是因?yàn)閙ounted只能執(zhí)行一次,所以后期我們更新的selected不會(huì)通知到item組件莱革,所以我們需要在sliders中通過(guò)updated事件再次執(zhí)行mounted中的更新selected的方法
- demo.vue
<template>
<div>
<lf-sliders :selected="selected">
<lf-sliders-item name="1">
<div class="box">1</div>
</lf-sliders-item>
<lf-sliders-item name="2">
<div class="box">2</div>
</lf-sliders-item>
<lf-sliders-item name="3">
<div class="box">3</div>
</lf-sliders-item>
</lf-sliders>
</div>
</template>
<script>
import LfSliders from './slides.vue'
import LfSlidersItem from './sliders-item.vue'
export default {
name: "demo",
components: {
LfSliders,
LfSlidersItem
},
data(){
return {
selected: '1'
}
},
methods: {
},
created() {
let n = 1
setInterval(()=>{
n++
if(n === 4){
n = 1
}
this.selected = n.toString()
},3000)
}
}
</script>
<style scoped>
.box{
width: 100px;
height: 100px;
background: red;
border: 1px solid gray;
}
</style>
- slides.vue
<template>
<div class="lf-slides">
<div class="lf-slides-window" ref="window">
<div class="lf-slides-wrapper">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LiFaslides",
props: {
selected: {
type: String
}
},
mounted() {
this.updateChildren()
},
updated() {
this.updateChildren()
},
methods: {
updateChildren(){
let first = this.$children[0]
this.$children.forEach((vm)=>{
vm.selected = this.selected || first.$attrs.name
})
}
}
}
</script>
<style scoped lang="scss">
.lf-slides{
display: inline-block;
border: 1px solid black;
&-wrapper{
position: relative;
display: flex;
}
}
</style>
- slides-item.vue
<template>
<transition name="fade">
<div class="lf-slides-item" v-if="visible">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: "slides-item",
data() {
return {
selected: undefined
}
},
props: {
name: {
type: String,
required: true
}
},
computed: {
visible(){
return this.selected === this.name
}
}
}
</script>
<style scoped lang="scss">
.fade-enter-active, .fade-leave-active {
transition: all .3s;
}
.fade-enter {
transform: translateX(100%);
}
.fade-leave-active{
position: absolute;
}
.fade-leave-to {
transform: translateX(-100%);
}
</style>
向左滑動(dòng)實(shí)現(xiàn)
思路:給slides傳入一個(gè)autoPlay屬性果录,默認(rèn)為true,然后將上面的setTImeout放到slides組件中妨马,定義一個(gè)automaticPlay方法挺举,在這個(gè)方法里通過(guò)遍歷子組件拿到一個(gè)names數(shù)組,然后從names數(shù)組里找當(dāng)前的selected是第幾個(gè)烘跺,之后觸發(fā)父組件的update:selected事件湘纵,將names[index]傳給父組件,然后對(duì)index進(jìn)行++滤淳,之后通過(guò)setTimout反復(fù)的調(diào)用
- slides.vue
export default {
name: "LiFaslides",
props: {
selected: {
type: String
},
autoPlay: {
type: Boolean,
default: true
}
},
mounted() {
this.updateChildren()
this.automaticPlay()
},
updated() {
this.updateChildren()
},
methods: {
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.selected = selected
})
},
automaticPlay(){
let names = this.$children.map((vm)=>{
return vm.name
})
let selected = this.getSelected()
//拿到每一次的索引值梧喷,下次動(dòng)畫好在基礎(chǔ)上累加
let index = names.indexOf(selected)
let run = ()=>{
this.$emit('update:selected',names[index])
index++
if(index > names.length-1){
index = 0
}
setTimeout(()=>{
run()
},3000)
}
run()
},
getSelected(){
let first = this.$children[0]
return this.selected || first.$attrs.name
}
}
}
反向滾動(dòng)
只需要將index++改成--,但是如果直接用--names[index]
就會(huì)出問(wèn)題脖咐,所以需要一開(kāi)始的時(shí)候?qū)ndex進(jìn)行判斷铺敌,讓newindex=index-1,如果newindex小于
0就讓它等于最后一個(gè)也就是names.length-1如果大于等于names.length就讓它等于0屁擅,然后每次觸發(fā)update:selected更新的時(shí)候都觸發(fā)一個(gè)select偿凭,把當(dāng)前的index傳入,然后通過(guò)select的時(shí)候拿到當(dāng)前的現(xiàn)在選中的index給lastSelectedIndex屬性(這里還包括點(diǎn)擊某一個(gè)控制點(diǎn)派歌,點(diǎn)擊觸發(fā)select的時(shí)候先把當(dāng)前的selected給lastSelectedIndex弯囊,然后再更新selected為你點(diǎn)擊的那個(gè)點(diǎn)的索引和值),接著觸發(fā)update把最新的index傳進(jìn)去胶果,通過(guò)對(duì)比lastSelectedIndex和selectedIndex來(lái)確定是否給item添加一個(gè)reverse的類
- sides.vue
<ul class="dots">
<li v-for="n in childrenLength" :class="{active: selectedIndex === n-1}"
@click="select(n-1)"
>
{{n-1}}
</li>
</ul>
export default {
name: "LiFaslides",
props: {
selected: {
type: String
},
autoPlay: {
type: Boolean,
default: true
}
},
data(){
return {
childrenLength: 0,
lastSelectedIndex: undefined
}
},
mounted() {
this.childrenLength = this.$children.length
this.updateChildren()
this.automaticPlay()
},
updated() {
this.updateChildren()
},
computed: {
selectedIndex(){
return this.names.indexOf(this.selected) || 0
},
names(){
return this.$children.map(vm=>vm.name)
}
},
methods: {
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.selected = selected
vm.reverse = this.selectedIndex > this.lastSelectedIndex ? false : true
})
},
automaticPlay(){
let selected = this.getSelected()
//拿到每一次的索引值匾嘱,下次動(dòng)畫好在基礎(chǔ)上累加
let index = this.names.indexOf(selected)
let run = ()=>{
let newIndex = index -1
if(newIndex < 0){
newIndex = this.names.length - 1
}
if(newIndex === this.names.length){
newIndex = 0
}
this.select(newIndex)
setTimeout(()=>{
run()
},3000)
}
setTimeout(run, 3000)
},
getSelected(){
let first = this.$children[0]
return this.selected || first.$attrs.name
},
select(index){
//當(dāng)選中新的index的時(shí)候,就把舊的index賦給lastSelectedIndex
this.lastSelectedIndex = this.selectedIndex
//然后把新的index和選中值傳給selected
this.$emit('update:selected',this.names[index])
}
}
}
- sides-item.vue
<template>
<transition name="fade">
<div class="lf-slides-item" v-if="visible" :class="{reverse}">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: "slides-item",
data() {
return {
selected: undefined,
reverse: false
}
},
props: {
name: {
type: String,
required: true
}
},
computed: {
visible(){
return this.selected === this.name
}
}
}
</script>
<style scoped lang="scss">
.lf-slides-item{
width: 100%;
}
.fade-enter-active, .fade-leave-active {
transition: all 1s;
}
.fade-enter {
transform: translateX(100%);
}
.fade-enter.reverse{
transform: translateX(-100%);
}
.fade-leave-active{
position: absolute;
}
.fade-leave-to {
transform: translateX(-100%);
}
.fade-leave-to.reverse{
transform: translateX(100%);
}
</style>
解決動(dòng)畫混亂的bug
上次的代碼中早抠,會(huì)發(fā)現(xiàn)不管讓它正向還是反向他最后類里都會(huì)有一個(gè)reverse霎烙,所以就會(huì)造成正向的時(shí)候同時(shí)出現(xiàn)正向和反向的動(dòng)畫,之所以會(huì)一直有這個(gè)reverse的類存在是因?yàn)槲覀冊(cè)趗pdateChildren中雖然立即把reverse改了贝或,但是不代表它會(huì)立即生效在dom上面吼过,他有可能是放在了一個(gè)任務(wù)隊(duì)列里。解決辦法:在reverse生效后再去更改當(dāng)前顯示的selected咪奖,也就是延遲更改(在下一次的時(shí)候更改)通過(guò)nextTick就可以
- slides.vue
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.reverse = this.selectedIndex > this.lastSelectedIndex ? false : true
this.$nextTick(()=>{
vm.selected = selected
})
})
},
解決每次只滾動(dòng)顯示兩張圖的bug
在自動(dòng)播放的方法里通過(guò)console發(fā)現(xiàn)index每次都是初始值盗忱,比如初始值是1,那么每次都是1羊赵,所以圖片只顯示兩張趟佃,我們需要每次setTimout的時(shí)候index的值都要變化扇谣,可以在結(jié)束的時(shí)候讓index = newIndex
automaticPlay(){
let selected = this.getSelected()
//拿到初始的索引值
let index = this.names.indexOf(selected)
let run = ()=>{
let newIndex = index -1
if(newIndex < 0){
newIndex = this.names.length - 1
}
if(newIndex === this.names.length){
newIndex = 0
}
index = newIndex
this.select(newIndex)
setTimeout(()=>{
run()
},3000)
}
setTimeout(run, 3000)
},
實(shí)現(xiàn)鼠標(biāo)經(jīng)過(guò)輪播圖,輪播暫停闲昭,離開(kāi)繼續(xù)
思路:給外層加一個(gè)mouseenter和mouseleave監(jiān)聽(tīng)事件罐寨,然后加一個(gè)timerId屬性,讓setTimeout等于它序矩,鼠標(biāo)經(jīng)過(guò)清除setTImoue并把timeId置為null鸯绿,鼠標(biāo)離開(kāi)再次執(zhí)行自動(dòng)滾動(dòng)方法
- slides.vue
<div class="lf-slides-window" ref="window" @mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
methods: {
onMouseEnter(){
this.pause()
},
onMouseLeave(){
this.automaticPlay()
},
pause(){
window.clearTimeout(this.timerId)
this.timerId = null
},
autoMaticPlay(){
+//如果當(dāng)前正在輪播中就不再次執(zhí)行這個(gè)方法
+ if(this.timerId){
+ return
}
}
}
解決反向的時(shí)候從第一張到最后一張動(dòng)畫是正向動(dòng)畫的問(wèn)題
方法:只需要在上一個(gè)選中的index等于0并且當(dāng)前選中的是最后一個(gè)的索引(this.names.length-1)的情況下讓item的reverse屬性為true就可以
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.reverse = this.selectedIndex > this.lastSelectedIndex ? false : true
//如果上一張是第一張,當(dāng)前這張是最后一張(也就是反向動(dòng)畫的時(shí)候)就讓它依然是反向動(dòng)畫
+ if(this.lastSelectedIndex === 0 && this.selectedIndex === this.names.length-1){
+ vm.reverse = true
+ }
//如果上一張是最后一張簸淀,當(dāng)前這張是第一張(也就是正向動(dòng)畫的時(shí)候)就讓它依然是正向
+ if(this.lastSelectedIndex === this.names.length-1 && this.selectedIndex === 0){
+ vm.reverse = false
+ }
this.$nextTick(()=>{
vm.selected = selected
})
})
},
點(diǎn)擊的時(shí)候動(dòng)畫方向不對(duì)
針對(duì)上面的代碼自動(dòng)滾動(dòng)的時(shí)候動(dòng)畫都是正常的瓶蝴,但是當(dāng)我們點(diǎn)擊的下面的對(duì)應(yīng)點(diǎn)的時(shí)候,比如我正向的時(shí)候當(dāng)前顯示的是第一個(gè)租幕,我點(diǎn)第三個(gè)就會(huì)發(fā)現(xiàn)動(dòng)畫是反向的舷手,這就是因?yàn)槲覀兩厦孀龅呐袛啵晕覀円堰@個(gè)判斷加到自動(dòng)滾動(dòng)的方法中去
//如果是自動(dòng)滾動(dòng)的情況下
+ if(this.timerId){
//如果上一張是第一張劲绪,當(dāng)前這張是最后一張(也就是反向動(dòng)畫的時(shí)候)就讓它依然是反向動(dòng)畫
if(this.lastSelectedIndex === 0 && this.selectedIndex === this.names.length-1){
vm.reverse = true
}
//如果上一張是最后一張男窟,當(dāng)前這張是第一張(也就是正向動(dòng)畫的時(shí)候)就讓它依然是正向
if(this.lastSelectedIndex === this.names.length-1 && this.selectedIndex === 0){
vm.reverse = false
}
+ }
手機(jī)上觸摸滑動(dòng)
- 為什么獲取滑動(dòng)點(diǎn)的坐標(biāo)用的是e.touches[0]而不是e.touch
答:因?yàn)榇嬖诙帱c(diǎn)觸控e.touches獲取的是用戶手指的數(shù)量,也就是屏幕觸摸點(diǎn)的數(shù)量贾富,e.touches[0]獲取的就是第一個(gè)觸摸點(diǎn)(也就是第一個(gè)手指)歉眷,所以我們需要判斷如果觸摸點(diǎn)的個(gè)數(shù)大于1(有一個(gè)以上的手指觸摸)就直接return - 如何確定是往左滑動(dòng)還是往右滑動(dòng)?
只需通過(guò)touchstart時(shí)獲取e.touches[0]的clientX和touchEnd的時(shí)候changedTouches[0]的clientX祷安,將開(kāi)始點(diǎn)的clientX通過(guò)一個(gè)屬性存下來(lái)姥芥,然后比較兩者的大小,如果結(jié)束的時(shí)候的距離汇鞭,大于開(kāi)始的時(shí)候就是右滑了凉唐,否則就是左滑,然后分別對(duì)應(yīng)著讓當(dāng)前選中的index加1或減1
- slides.vue
<div class="lf-slides-window" ref="window" @touchstart="onTouchStart"
@touchmove="onTouchMove" @touchend="onTouchEnd"
>
methods: {
onTouchStart(e){
if(e.touches.length > 0){return}
this.touchStart = {clientX:e.touches[0].clientX,clientY:e.touches[0].clientY}
this.pause()
},
onTouchMove(e){
console.log(e)
},
onTouchEnd(e){
let {clientX,clientY} = e.changedTouches[0]
if(clientX > this.touchStart.clientX){
this.select(this.selectedIndex + 1)
}else{
this.select(this.selectedIndex - 1)
}
this.automaticPlay()
},
select(newIndex){
if(newIndex < 0){
newIndex = this.names.length - 1
}
if(newIndex >= this.names.length){
newIndex = 0
}
//讓newIndex等于條件內(nèi)的newIndex
this.newIndex = newIndex
//當(dāng)選中新的index的時(shí)候霍骄,就把舊的index賦給lastSelectedIndex
this.lastSelectedIndex = this.selectedIndex
//然后把新的index和選中值傳給selected
this.$emit('update:selected',this.names[newIndex])
},
automaticPlay(){
//如果當(dāng)前正在輪播中就不再次執(zhí)行這個(gè)方法
if(this.timerId){
return
}
let selected = this.getSelected()
//拿到初始的索引值
let index = this.names.indexOf(selected)
let run = ()=>{
this.newIndex = index +1
this.select(this.newIndex)
index = this.newIndex
this.timerId =setTimeout(()=>{
run()
},3000)
}
this.timerId = setTimeout(run, 3000)
},
}
上面的代碼雖然已經(jīng)實(shí)現(xiàn)了左右滑動(dòng)台囱,但是如果我沒(méi)有左右滑,我是上下滑了(上下翻頁(yè))读整,他依然會(huì)根據(jù)你最后clientX的偏移倆執(zhí)行左滑或右滑時(shí)的函數(shù)
比如根據(jù)上圖我們明顯能知道第二張是翻頁(yè)簿训,第三個(gè)是滑動(dòng),而第一個(gè)我們可以根據(jù)我們自己的設(shè)定米间,比如開(kāi)始和結(jié)束后的角度如果小于三十度就是在滑動(dòng)强品,否則就是翻頁(yè),而角度的確定我們可以根據(jù)三十度直角三角形特性屈糊,三十度角所對(duì)的直角邊是斜邊的一半的榛,所以針對(duì)下圖只要是斜邊除以垂直的直角邊大于2就說(shuō)明是小于三十度,再讓他執(zhí)行左滑右滑的方法
上圖2x那條邊的距離就是
垂直的直角邊的距離就是|y2-y1|
onTouchEnd(e){
let {clientX,clientY} = e.changedTouches[0]
+ let [x1,y1] = [this.touchStart.clientX,this.touchStart.clientY]
+ let [x2,y2] = [clientX,clientY]
+ let distance = Math.sqrt(Math.pow(x2 - x1,2) + Math.pow(y2 - y1,2))
+ let deltaY = Math.abs(y2-y1)
+ let rate = distance / deltaY
+ if(rate > 2){
if(clientX > this.touchStart && this.touchStart.clientX){
this.select(this.selectedIndex - 1)
}else{
this.select(this.selectedIndex + 1)
}
+ }
this.automaticPlay()
},