Vue侧滑菜单组件——DrawerLayout


Posted in Javascript onDecember 18, 2017

本文介绍一个简单的DrawerLayout(类似Android的DrawerLayout)布局组件的实现,基于Vue.js。介绍的内容已经制作成 vue-drawer-layout 组件。

前言

大家有兴趣先用手机扫一扫这个二维码,或者点我

然后点击页面中左上角的头像打开drawer或者向右向左拖拽,就可以看到下面gif的效果,打开自己的手机QQ,是不是很像:)

Vue侧滑菜单组件——DrawerLayout 

谷歌官方把这种布局叫做DrawerLayout(抽屉式导航栏)。那么我们要如何实现呢,好了正片开始!

HTML结构

页面结构很简单,一个抽屉,一个主容器,内容可以利用slot支持外部自行定制。

<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% 使其整个都藏在外部

使用Touch

首先,判断浏览器是否支持 touchEvent

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 事件

document.addEventListener(mouseEvents.down, initDrag, false);

先定义一些变量,手指按下的x坐标记为 startX ,滑动中手指的位置x坐标记为 nowX ,drawer的x坐标偏移量记为 startPos

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 ,这样抽屉已经跟随手指向右移动了,并且不会超过我们设置的拖动最大值。

区分垂直滑动和水平滑动

接下来你会发现一个问题,当手指垂直滚动主内容时,向右滑动手指也会拖出抽屉,这时应该做一件事:区分垂直滑动和水平滑动

当然,办法有很多,这里先介绍一种利用三角函数来判定的方法

Vue侧滑菜单组件——DrawerLayout 

假设,上图中的每个箭头是手指滑动的方向,绿色箭头代表可以拖出抽屉,红色箭头代表不可以拖出(注意,红色箭头也是有x坐标的偏移量的)。即当不可以拖出抽屉时,应触发默认事件,比如垂直方向的滚动等等。

当手指按下触发 touchstart 时,记录初始位置P 0 ;当滑动手指时,触发的第一次 touchmove 时,记录位置P 1 ,我们将P 0 到P 1 的矢量记为S(原谅我这个灵魂画手)

Vue侧滑菜单组件——DrawerLayout 

这时候很容易看出,∠θ大于某个值时,比如30度,就可能是垂直方向的滚动操作而不是拖动抽屉。所以,可以根据 y/x>tan30°

得到判断条件:

if (isVerticle === undefined) isVerticle = Math.abs(nowY - startY) / Math.abs(nowX - startX) > (Math.sqrt(3) / 3);

当 isVerticle 为 true 时,不执行drawer的拖动

让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 去掉,让浏览器歇一会儿~

还有什么可以优化的?

上面说的已经基本上把主要功能实现了,但是这其中还有没有哪里可以优化的?

Vue侧滑菜单组件——DrawerLayout 

咦? 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官网检索。

Javascript 相关文章推荐
js的闭包的一个示例说明
Nov 18 Javascript
JS常用字符串处理方法应用总结
May 22 Javascript
jquery easyui使用心得
Jul 07 Javascript
JS获取时间的方法
Jan 21 Javascript
跟我学习javascript的var预解析与函数声明提升
Nov 16 Javascript
基于AngularJS实现的工资计算器实例
Jun 16 Javascript
vue底部加载更多的实例代码
Jun 29 Javascript
微信小程序scroll-view实现字幕滚动
Jul 14 Javascript
JavaScript遍历DOM元素的常见方式示例
Feb 16 Javascript
小程序绑定用户方案优化小结
May 15 Javascript
Layui带搜索的下拉框的使用以及动态数据绑定方法
Sep 28 Javascript
Vue elementUI表单嵌套表格并对每行进行校验详解
Feb 18 Vue.js
switchery按钮的使用方法
Dec 18 #Javascript
three.js实现3D影院的原理的代码分析
Dec 18 #Javascript
JS函数节流和函数防抖问题分析
Dec 18 #Javascript
vue 将页面公用的头部组件化的方法
Dec 18 #Javascript
浅谈使用React.setState需要注意的三点
Dec 18 #Javascript
vue 项目如何引入微信sdk接口的方法
Dec 18 #Javascript
微信小程序实现给嵌套template模板传递数据的方式总结
Dec 18 #Javascript
You might like
给apache2.2加上mod_encoding模块後 php5.2.0 处理url出现bug
2007/04/12 PHP
PHP实时显示输出
2008/10/02 PHP
PHP下SSL加密解密、验证、签名方法(很简单)
2020/06/28 PHP
用jquery ajax获取网站Alexa排名的代码
2009/12/12 Javascript
jquery win 7透明弹出层效果的简单代码
2013/08/06 Javascript
getAsDataURL在Firefox7.0下无法预览本地图片的解决方法
2013/11/15 Javascript
js为空或不是对象问题的快速解决方法
2013/12/11 Javascript
javascript中expression的用法整理
2014/05/13 Javascript
利用AJAX实现WordPress中的文章列表及评论的分页功能
2016/05/17 Javascript
更靠谱的H5横竖屏检测方法(js代码)
2016/09/13 Javascript
vue.js实现表格合并示例代码
2016/11/30 Javascript
深入理解Angularjs向指令传递数据双向绑定机制
2016/12/31 Javascript
Async Validator 异步验证使用说明
2017/07/03 Javascript
详解webpack的配置文件entry与output
2017/08/21 Javascript
VUE 实现滚动监听 导航栏置顶的方法
2018/09/11 Javascript
vue中v-for循环给标签属性赋值的方法
2018/10/18 Javascript
js html实现计算器功能
2018/11/13 Javascript
基于JS实现一个随机生成验证码功能
2019/05/29 Javascript
json数据格式常见操作示例
2019/06/13 Javascript
用webAPI实现图片放大镜效果
2020/11/23 Javascript
Python实现优先级队列结构的方法详解
2016/06/02 Python
神经网络(BP)算法Python实现及应用
2018/04/16 Python
基于python log取对数详解
2018/06/08 Python
Django 路由控制的实现代码
2018/11/08 Python
python利用Tesseract识别验证码的方法示例
2019/01/21 Python
手写一个python迭代器过程详解
2019/08/27 Python
详解python 中in 的 用法
2019/12/12 Python
Tripadvisor新西兰:阅读评论,比较价格和酒店预订
2018/02/10 全球购物
美国伊甸园兄弟种子公司:Eden Brothers
2018/07/01 全球购物
Yankee Candle官网:美国最畅销蜡烛品牌之一
2020/01/05 全球购物
毕业生求职的求职信
2013/12/05 职场文书
数学系毕业生求职信
2014/05/29 职场文书
初中生庆国庆演讲稿范文2014
2014/09/25 职场文书
党员自我评议对照检查材料
2014/09/27 职场文书
女方家长婚礼答谢词
2015/09/29 职场文书
分享CSS盒子模型隐藏的几种方式
2022/02/28 HTML / CSS