本文可能有点啰嗦了...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>
复制代码