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实现绿白相间竖向网页百叶窗动画切换效果
Mar 02 Javascript
jquery日历插件e-calendar升级版
Nov 10 Javascript
Javascript 两种刷新方法以及区别和适用范围
Jan 17 Javascript
JS+DIV实现的卷帘效果示例
Mar 22 Javascript
JavaScript使用原型和原型链实现对象继承的方法详解
Apr 05 Javascript
Vue.js tab实现选项卡切换
May 16 Javascript
jquery加载单文件vue组件的方法
Jun 20 jQuery
使用js获取伪元素的content实例
Oct 24 Javascript
小程序云开发部署攻略(图文教程)
Oct 30 Javascript
layui radio单选限制下一个radio单选的实例
Sep 03 Javascript
使用 Element UI Table 的 slot-scope方法
Oct 10 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中分页用法实例分析
2015/12/26 PHP
PHP+shell脚本操作Memcached和Apache Status的实例分享
2016/03/11 PHP
Laravel中基于Artisan View扩展包创建及删除应用视图文件的方法
2016/10/08 PHP
动态调用CSS文件的JS代码
2010/07/29 Javascript
jquery绑定事件不生效的解决方法
2014/02/11 Javascript
JavaScript中指定函数名称的相关方法
2015/06/04 Javascript
学习JavaScript设计模式之策略模式
2016/01/12 Javascript
第一次接触神奇的Bootstrap网格系统
2016/07/27 Javascript
Node.js Express 框架 POST方法详解
2017/01/23 Javascript
Bootstrap实现各种进度条样式详解
2017/04/13 Javascript
微信小程序 本地数据读取实例
2017/04/27 Javascript
vue 打包后的文件部署到express服务器上的方法
2017/08/09 Javascript
webpack打包js的方法
2018/03/12 Javascript
教你搭建按需加载的Vue组件库(小结)
2019/07/29 Javascript
微信小程序 scroll-view 实现锚点跳转功能
2019/12/12 Javascript
[12:29]2018国际邀请赛 开幕秀
2018/08/22 DOTA
[48:48]VGJ.T vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
python with statement 进行文件操作指南
2014/08/22 Python
Python使用smtplib模块发送电子邮件的流程详解
2016/06/27 Python
python 全文检索引擎详解
2017/04/25 Python
python 字符串转列表 list 出现\ufeff的解决方法
2017/06/22 Python
python对配置文件.ini进行增删改查操作的方法示例
2017/07/28 Python
Python+Socket实现基于UDP协议的局域网广播功能示例
2017/08/31 Python
python爬虫超时的处理的实例
2018/12/19 Python
使用Python实现企业微信的自动打卡功能
2019/04/30 Python
实习鉴定评语
2014/01/19 职场文书
剪彩仪式主持词
2014/03/19 职场文书
乡镇纠风工作实施方案
2014/03/22 职场文书
婚纱店策划方案
2014/05/22 职场文书
专题组织生活会发言材料
2014/10/17 职场文书
刑事附带民事代理词
2015/05/25 职场文书
2015年清剿火患专项行动工作总结
2015/07/27 职场文书
python实现批量移动文件
2021/04/05 Python
MySQL sql_mode的使用详解
2021/05/08 MySQL
详解在SQLPlus中实现上下键翻查历史命令的功能
2022/03/18 SQL Server
鸿蒙3.0体验感怎么样? 鸿蒙3.0系统评测向
2022/08/14 数码科技