本文可能有點囉嗦了...css
爲了充分發揮vue
的特性,咱們不該該經過ref
來直接操做dom
,而是應該經過修改數據項從而讓vue
自動更新dom
。所以,咱們這樣編寫template
。html
<template>
<div class="ys-float-btn" :style="{'left':left+'px','top':top+'px'}">
<slot name="icon"></slot>
<p>{{text}}</p>
</div>
</template>
複製代碼
固然.ys-float-btn
確定是position:fixed
的,其餘的樣式很簡單,你們自由發揮。vue
首次進入頁面時,按鈕應該處於一個初始位置。咱們在created鉤子中進行初始化。html5
created(){
this.left = document.documentElement.clientWidth - 50;
this.top = document.documentElement.clientHeight*0.8;
},
複製代碼
爲了可以讓這個浮動按鈕可以在頁面滾動時隱藏,第一步要作的就是監聽頁面滾動事件。chrome
mounted(){
window.addEventListener('scroll', this.handleScrollStart);
},
methods:{
handleScrollStart(){
this.left = document.documentElement.clientWidth - 25;
}
}
複製代碼
嗯,別忘了取消註冊。瀏覽器
beforeDestroy(){
window.removeEventListener('scroll', this.handleScrollStart);
},
複製代碼
這樣就可以讓組件在頁面滾動時往右再移動25像素的距離。 but!我尚未寫動畫誒...less
嗯,我固然不會使用js寫動畫了,咱們在css
類.ys-float-btn
中加上transition: all 0.3s;
過渡動畫就搞定了。dom
監聽到scroll
事件只是第一步,那麼何時scroll事件纔會中止呢?瀏覽器並無爲咱們準備這樣一個事件,咱們須要手動去實現它。思路其實也很簡單,當一個時間週期內頁面的scrollTop
不變就說明頁面滾動中止了。 因此咱們須要在data
函數裏返回一個timer
對象,用來存儲咱們的定時器。像這樣:函數
data(){
return{
timer:null,
currentTop:0
}
}
複製代碼
改造一下handleScrollStart
方法。 觸發scroll
的時候清掉當前的計時器(若是存在),並從新計時測試
handleScrollStart(){
this.timer&&clearTimeout(this.timer);
this.timer = setTimeout(()=>{
this.handleScrollEnd();
},300);
this.currentTop = document.documentElement.scrollTop || document.body.scrollTop;
this.left = document.documentElement.clientWidth - 25;
},
複製代碼
如今增長了一個回調handleScrollEnd
方法
handleScrollEnd(){
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if(scrollTop === this.currentTop){
this.left = document.documentElement.clientWidth - 50;
clearTimeout(this.timer);
}
}
複製代碼
若是如今的滾動高度等於以前的滾動高度,說明頁面沒有繼續滾動了。將left
調整爲初始位置。
爲了實現組件的拖拽功能,我最早想到的就是html5
爲咱們提供的drag
方法。所以像這樣,爲咱們的template
增長這樣的代碼。
<div class="ys-float-btn" :style="{'width':itemWidth+'px','height':itemHeight+'px','left':left+'px','top':top+'px'}" :draggable ='true' @dragstart="onDragStart" @dragover.prevent = "onDragOver" @dragenter="onDragEnter" @dragend="onDragEnd">
<slot name="icon"></slot>
<p>{{text}}</p>
</div>
複製代碼
結果在測試的時候就是沒有效果,設置的四個監聽方法一個都沒有執行。迷茫了很久,後來在本身找bug
期間無心將chrome
取消了移動端模式,而後發現拖拽監聽方法執行了。
這真是,無力吐槽。 記筆記了:移動端沒法使用drag來進行組件的拖拽操做。
那麼移動端如何實現拖拽效果呢?瞭解到移動端有touch
事件。touch
與click
事件觸發的前後順序以下所示:
touchstart => touchmove => touchend => click。
這裏咱們須要爲組件註冊監聽以上touch
事件,怎麼拿到具體的dom
呢? vue
爲咱們提供了ref
屬性。
template
最外層的
div
加上
ref
<div class="ys-float-btn" :style="{'left':left+'px','top':top+'px'}"
ref="div">
<slot name="icon"></slot>
<p>{{text}}</p>
</div>
複製代碼
爲了確保組件已經成功掛載,咱們在nextTick
中進行事件註冊。如今mounted鉤子方法長這樣:
mounted(){
window.addEventListener('scroll', this.handleScrollStart);
this.$nextTick(()=>{
const div = this.$refs.div;
div.addEventListener("touchstart",()=>{
});
div.addEventListener("touchmove",(e)=>{
});
div.addEventListener("touchend",()=>{
});
});
},
複製代碼
在對組件進行拖拽的過程當中,應當不須要組件的過分動畫的,因此咱們在touchstart中取消過分動畫。
div.addEventListener("touchstart",()=>{
div.style.transition = 'none';
});
複製代碼
在拖拽的過程當中,組件應該跟隨手指的移動而移動。
div.addEventListener("touchmove",(e)=>{
if (e.targetTouches.length === 1) {//一根手指
let touch = event.targetTouches[0];
this.left = touch.clientX;
this.top = touch.clientY;
}
});
複製代碼
可能有同窗看了上面的代碼以後已經看出來所疏漏的地方了,上述代碼彷佛可以讓組件跟隨手指移動了,可是還差了點。由於並非組件中心跟隨手指在移動。咱們微調一下:
div.addEventListener("touchmove",(e)=>{
if (e.targetTouches.length === 1) {
let touch = event.targetTouches[0];
this.left = touch.clientX - 25;//組件的寬度是50
this.top = touch.clientY - 25;
}
});
複製代碼
拖拽結束之後,判斷在頁面的稍左仍是稍右,從新調整組件的位置並從新設置過分動畫。
div.addEventListener("touchend",()=>{
div.style.transition = 'all 0.3s';
if(this.left>document.documentElement.clientWidth/2){
this.left = document.documentElement.clientWidth - 50;
}else{
this.left = 0;
}
});
複製代碼
寫到這裏是否是就完了呢? 咱們好像漏了點什麼。 對了,頁面滾動時沒有判斷組件在左邊仍是在右邊,當時統一當成右邊在處理了。 如今修改handleScrollStart和handleScrollEnd方法。
handleScrollStart(){
this.timer&&clearTimeout(this.timer);
this.timer = setTimeout(()=>{
this.handleScrollEnd();
},300);
this.currentTop = document.documentElement.scrollTop || document.body.scrollTop;
if(this.left>document.documentElement.clientWidth/2){
this.left = document.documentElement.clientWidth - 25;
}else{
this.left = -25;
}
},
handleScrollEnd(){
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if(scrollTop === this.currentTop){
if(this.left>document.documentElement.clientWidth/2){
this.left = document.documentElement.clientWidth - 50;
}else{
this.left = 0;
}
clearTimeout(this.timer);
}
}
複製代碼
剛剛噼裏啪啦一頓敲鍵盤終於把這個組件寫完啦,這樣是否是就完事大吉了呢?不,固然不。咱們爲何要寫組件呢?不就是爲了重用嗎,如今這個組件裏充斥着各類沒有標明意義的數字和重複代碼,是時候重構一下了。 開發組件一般是數據先行,如今咱們回過頭來看一下哪些數據須要預約義。
props:{
text:{
type:String,
default:"默認文字"
},
itemWidth:{
type:Number,
default:60
},
itemHeight:{
type:Number,
default:60
},
gapWidth:{
type:Number,
default:10
},
coefficientHeight:{
type:Number,
default:0.8
}
}
複製代碼
咱們須要組件的寬高和間隔(與頁面邊界的間隔),額對了,還有那個視口的寬度!咱們在前文中屢次使用document.documentElement.clientWidth
不知道大家有沒有看煩,我反正是寫煩了.... 組件內部用的數據咱們用data定義:
data(){
return{
timer:null,
currentTop:0,
clientWidth:0,
clientHeight:0,
left:0,
top:0,
}
}
複製代碼
所以,在組件建立的時候咱們須要爲這些數據作預處理! 如今created
長這樣:
created(){
this.clientWidth = document.documentElement.clientWidth;
this.clientHeight = document.documentElement.clientHeight;
this.left = this.clientWidth - this.itemWidth - this.gapWidth;
this.top = this.clientHeight*this.coefficientHeight;
},
複製代碼
... 就到這裏吧,後面的都差很少了....
<template>
<div class="ys-float-btn" :style="{'width':itemWidth+'px','height':itemHeight+'px','left':left+'px','top':top+'px'}"
ref="div"
@click ="onBtnClicked">
<slot name="icon"></slot>
<p>{{text}}</p>
</div>
</template>
<script>
export default {
name: "FloatImgBtn",
props:{
text:{
type:String,
default:"默認文字"
},
itemWidth:{
type:Number,
default:60
},
itemHeight:{
type:Number,
default:60
},
gapWidth:{
type:Number,
default:10
},
coefficientHeight:{
type:Number,
default:0.8
}
},
created(){
this.clientWidth = document.documentElement.clientWidth;
this.clientHeight = document.documentElement.clientHeight;
this.left = this.clientWidth - this.itemWidth - this.gapWidth;
this.top = this.clientHeight*this.coefficientHeight;
},
mounted(){
window.addEventListener('scroll', this.handleScrollStart);
this.$nextTick(()=>{
const div = this.$refs.div;
div.addEventListener("touchstart",()=>{
div.style.transition = 'none';
});
div.addEventListener("touchmove",(e)=>{
if (e.targetTouches.length === 1) {
let touch = event.targetTouches[0];
this.left = touch.clientX - this.itemWidth/2;
this.top = touch.clientY - this.itemHeight/2;
}
});
div.addEventListener("touchend",()=>{
div.style.transition = 'all 0.3s';
if(this.left>this.clientWidth/2){
this.left = this.clientWidth - this.itemWidth - this.gapWidth;
}else{
this.left = this.gapWidth;
}
});
});
},
beforeDestroy(){
window.removeEventListener('scroll', this.handleScrollStart);
},
methods:{
onBtnClicked(){
this.$emit("onFloatBtnClicked");
},
handleScrollStart(){
this.timer&&clearTimeout(this.timer);
this.timer = setTimeout(()=>{
this.handleScrollEnd();
},300);
this.currentTop = document.documentElement.scrollTop || document.body.scrollTop;
if(this.left>this.clientWidth/2){
this.left = this.clientWidth - this.itemWidth/2;
}else{
this.left = -this.itemWidth/2;
}
},
handleScrollEnd(){
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if(scrollTop === this.currentTop){
if(this.left>this.clientWidth/2){
this.left = this.clientWidth - this.itemWidth - this.gapWidth;
}else{
this.left = this.gapWidth;
}
clearTimeout(this.timer);
}
}
},
data(){
return{
timer:null,
currentTop:0,
clientWidth:0,
clientHeight:0,
left:0,
top:0,
}
}
}
</script>
<style lang="less" scoped>
.ys-float-btn{
background:rgb(255,255,255);
box-shadow:0 2px 10px 0 rgba(0,0,0,0.1);
border-radius:50%;
color: #666666;
z-index: 20;
transition: all 0.3s;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: fixed;
bottom: 20vw;
img{
width: 50%;
height: 50%;
object-fit: contain;
margin-bottom: 3px;
}
p{
font-size:7px;
}
}
</style>
複製代碼