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 相关文章推荐
event.X和event.clientX的区别分析
Oct 06 Javascript
判断是否安装flash player及当前版本的JS代码
Aug 08 Javascript
浅析Js中的单引号与双引号问题
Nov 06 Javascript
用JQuery实现全选与取消的两种简单方法
Feb 22 Javascript
javascript计时器详解
Feb 28 Javascript
基于JS实现的倒计时程序实例
Jul 24 Javascript
jQuery自适应轮播图插件Swiper用法示例
Aug 24 Javascript
JQuery异步提交表单与文件上传功能示例
Jan 12 Javascript
微信小程序-获得用户输入内容
Feb 13 Javascript
设置cookie指定时间失效(实例代码)
May 28 Javascript
JS实现电话号码的字母组合算法示例
Feb 26 Javascript
vue - vue.config.js中devServer配置方式
Oct 30 Javascript
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
ThinkPHP基本的增删查改操作实例教程
2014/08/22 PHP
PHP检测字符串是否为UTF8编码的常用方法
2014/11/21 PHP
JavaScript DOM 学习第七章 表单的扩展
2010/02/19 Javascript
使用javascipt---实现二分查找法
2013/04/10 Javascript
通过隐藏iframe实现文件下载的js方法介绍
2014/02/26 Javascript
js 获取元素下面所有li的两种方法
2014/04/14 Javascript
node.js中的console.log方法使用说明
2014/12/09 Javascript
jQuery中unwrap()方法用法实例
2015/01/16 Javascript
关于JS 预解释的相关理解
2016/06/28 Javascript
学习Angular中作用域需要注意的坑
2016/08/17 Javascript
基于BootStrap的Metronic框架实现页面链接收藏夹功能按钮移动收藏记录(使用Sortable进行拖动排序)
2016/08/29 Javascript
JavaScript实现的select点菜功能示例
2017/01/16 Javascript
微信小程序侧边栏滑动特效(左右滑动)
2017/01/23 Javascript
简单谈谈require模块化jquery和angular的问题
2017/06/23 jQuery
利用百度地图API获取当前位置信息的实例
2017/11/06 Javascript
vue2 router 动态传参,多个参数的实例
2017/11/10 Javascript
vue实现密码显示隐藏切换功能
2018/02/23 Javascript
详解React之父子组件传递和其它一些要点
2018/06/25 Javascript
Vue中Table组件Select的勾选和取消勾选事件详解
2019/03/19 Javascript
微信小程序云开发之数据库操作
2019/05/18 Javascript
vue 解决数组赋值无法渲染在页面的问题
2019/10/28 Javascript
Python中的descriptor描述器简明使用指南
2016/06/02 Python
TensorFlow实现Softmax回归模型
2018/03/09 Python
Flask框架通过Flask_login实现用户登录功能示例
2018/07/17 Python
PyTorch中的padding(边缘填充)操作方式
2020/01/03 Python
python中pandas库中DataFrame对行和列的操作使用方法示例
2020/06/14 Python
英国在线滑雪板和冲浪商店:The Board Basement
2020/01/11 全球购物
JRE、JDK、JVM之间的关系怎样
2012/05/16 面试题
车工岗位职责
2013/11/26 职场文书
党员创先争优承诺书
2014/03/26 职场文书
2014年计划生育工作总结
2014/11/14 职场文书
慰问信格式
2015/02/14 职场文书
党支部半年考察意见
2015/06/01 职场文书
法定授权委托证明书
2015/06/18 职场文书
如果用一句诗总结你的上半年,你会用哪句呢?
2019/07/16 职场文书
python3.9之你应该知道的新特性详解
2021/04/29 Python