本文介绍一个简单的DrawerLayout(相似Android的DrawerLayout)布局组件的实现,基于 Vue.js。介绍的内容已经制做成 vue-drawer-layout 组件。
你们有兴趣先用手机扫一扫这个二维码,或者点我css
而后点击页面中左上角的头像打开drawer或者向右向左拖拽,就能够看到下面gif的效果,打开本身的手机QQ,是否是很像:)html
谷歌官方把这种布局叫作DrawerLayout(抽屉式导航栏)。那么咱们要如何实现呢,好了正片开始!vue
页面结构很简单,一个抽屉,一个主容器,内容能够利用slot支持外部自行定制。css3
<div class="drawer-layout"> <!--抽屉--> <div class="drawer-wrap"> <slot name="drawer"></slot> </div> <!--主容器--> <div class="content-wrap"> <!--遮罩--> <div class="drawer-mask"></div> <slot name="content"></slot> </div> </div>
抽屉一开始是隐藏在左侧屏幕外的,故设置left:-100%
使其整个都藏在外部git
首先,判断浏览器是否支持touchEvent
github
let isTouch = 'ontouchstart' in window; let mouseEvents = isTouch ? { down: 'touchstart', move: 'touchmove', up: 'touchend', over: 'touchstart', out: 'touchend' } : { down: 'mousedown', move: 'mousemove', up: 'mouseup', over: 'mouseover', out: 'mouseout' };
绑定touchdown
事件web
document.addEventListener(mouseEvents.down, initDrag, false);
先定义一些变量,手指按下的x坐标记为startX
,滑动中手指的位置x坐标记为nowX
,drawer的x坐标偏移量记为startPos
npm
let startX, nowX, startPos;
触发touchstart
时,记录起始位置并绑定touchmove
,注意:若是是mouseEvent
,经过e.clientX
来获取当前的x坐标,若是是touchEvent
,要经过e.changedTouches[0].clientX
来获取x坐标浏览器
const initDrag = function (e) { startX = e.clientX || e.changedTouches[0].clientX; //记录手指按下的位置 startPos = this.pos; //记录drawer的上次位置 document.addEventListener(mouseEvents.move, drag, false); document.addEventListener(mouseEvents.up, removeDrag, false); }.bind(this);
const drag = function (e) { nowX = e.clientX || e.changedTouches[0].clientX; //滑动中手指的位置x坐标 let pos = startPos + nowX - startX; pos = Math.min(width, pos); //不能超过滑动最大值 pos = Math.max(0, pos); //不能小于0 this.pos = pos; //设置滚动距离为拖动的距离 }.bind(this);
那么,手指滑动的距离就是nowX - startX
,当前drawer的位置为startPos + nowX - startX
,这样抽屉已经跟随手指向右移动了,而且不会超过咱们设置的拖动最大值。函数
接下来你会发现一个问题,当手指垂直滚动主内容时,向右滑动手指也会拖出抽屉,这时应该作一件事:区分垂直滑动和水平滑动
固然,办法有不少,这里先介绍一种利用三角函数来断定的方法
假设,上图中的每一个箭头是手指滑动的方向,绿色箭头表明能够拖出抽屉,红色箭头表明不能够拖出(注意,红色箭头也是有x坐标的偏移量的)。即当不能够拖出抽屉时,应触发默认事件,好比垂直方向的滚动等等。
当手指按下触发touchstart
时,记录初始位置P0;当滑动手指时,触发的第一次touchmove
时,记录位置P1,咱们将P0到P1的矢量记为S(原谅我这个灵魂画手)
这时候很容易看出,∠θ大于某个值时,好比30度,就多是垂直方向的滚动操做而不是拖动抽屉。因此,能够根据y/x>tan30°
获得判断条件:
if (isVerticle === undefined) isVerticle = Math.abs(nowY - startY) / Math.abs(nowX - startX) > (Math.sqrt(3) / 3);
当isVerticle
为true
时,不执行drawer的拖动
咱们使用css3的transition
属性使drawer具备过渡动画效果,这里写一个moving
类
.moving transition transform .3s ease
别忘了加上class绑定,拖动时是不须要过渡动画的(要跟随手指),而松开手指时才须要过渡动画。
<div class="drawer-wrap" :class="{'moving':moving,'will-change':willChange}" :style="{width:`${width}px`,left:`-${width)}px`,transform:`translate3d(${pos}px,0,0)`}"> <slot name="drawer"></slot> </div>
因此绑定touchend
事件的方法时要作这些步骤
const removeDrag = function (e) { if (isVerticle !== undefined) { if (!isVerticle) {//当断定为抽屉拖动才进入 let pos = this.pos; this.visible = pos > width * 3 / 5 //当前位置若是大于总宽度的3/5就断定为所有展开抽屉,不然将抽屉弹回隐藏 if (this.pos > 0 && this.pos < width) this.moving = true;//若是位置已经处于最小值或最大值处,不须要有动画效果了 } this.pos = this.visible ? width : 0; } if (!this.moving) { this.willChange = false; //留个悬念 } isVerticle = undefined; //取消touchmove和touchend事件绑定 document.removeEventListener(mouseEvents.move, drag, false); document.removeEventListener(mouseEvents.up, removeDrag, false); }.bind(this);
上面你可能发现代码里有个this.willChange = false
,它是干啥的捏?下面咱们请出css的will-change
大法
.will-change will-change transform
CSS 属性 will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器能够在元素属性真正发生变化以前提早作好对应的优化准备工做。 这种优化能够将一部分复杂的计算工做提早准备好,使页面的反应更为快速灵敏。
实际上是咱们在touchstart
能够预先告知浏览器抽屉可能要发生位移
const initDrag = function (e) { //... this.willChange = true; }.bind(this);
固然最后别忘了在transitionend
事件后把transition
和will-change
去掉,让浏览器歇一下子~
上面说的已经基本上把主要功能实现了,可是这其中还有没有哪里能够优化的?
咦?passive
是什么鬼?
网站使用被动事件侦听器以提高滚动性能,在您的触摸和滚轮事件侦听器上设置 passive 选项可提高滚动性能 具体看这里
原来这是现代浏览器的一个新特性,咱们须要以新的方式来绑定咱们的touch事件,固然首先先检测一下是否支持passive
const supportsPassive = (() => { let supportsPassive = false; try { const opts = Object.defineProperty({}, 'passive', { get: function () { supportsPassive = true; } }); window.addEventListener("test", null, opts); } catch (e) { } return supportsPassive; })();
因而咱们的绑定事件代码变成这样
document.addEventListener(mouseEvents.move, drag, supportsPassive ? {passive: true} : false);
是否有效果呢?有兴趣的朋友能够点这里看国外大神的视频
本文介绍了实现抽屉式导航栏的主要过程,详细代码已封装成vue-drawer-layout组件,支持更丰富的定制和使用方式,具体文档能够访问个人github或者npm官网检索。欢迎各位多多提issue,不吝赐教,感谢!