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小玩意 几个属性相加不能超过一个特定值.
Sep 29 Javascript
JS判断图片是否加载完成方法汇总(最新版)
May 13 Javascript
angularjs中的$eval方法详解
Apr 24 Javascript
将angular-ui的分页组件封装成指令的方法详解
May 10 Javascript
JavaScript代码执行的先后顺序问题
Oct 29 Javascript
vue-cli常用设置总结
Feb 24 Javascript
jQuery分组选择器简单用法示例
Apr 04 jQuery
Vue全局loading及错误提示的思路与实现
Aug 09 Javascript
小程序登录之支付宝授权的实现示例
Dec 13 Javascript
js函数和this用法实例分析
Mar 13 Javascript
Vue-cli3多页面配置详解
Mar 22 Javascript
element多个表单校验的实现
May 27 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
四月新番又没了,《Re:从零开始的异世界生活》第二季延期至7月播出
2020/05/06 日漫
需要发散思维学习PHP
2009/06/29 PHP
PHP字符串的编码问题的详细介绍
2013/04/27 PHP
php第一次无法获取cookie问题处理
2014/12/15 PHP
windows下安装php的memcache模块的方法
2015/04/07 PHP
php把字符串指定字符分割成数组的方法
2018/03/12 PHP
Laravel ORM 数据model操作教程
2019/10/21 PHP
php实现将数组或对象写入到文件的方法小结【三种方法】
2020/04/22 PHP
基于JavaScript 数据类型之Boolean类型分析介绍
2013/04/19 Javascript
ECMAScript 5严格模式(Strict Mode)介绍
2015/03/02 Javascript
通过AngularJS实现图片上传及缩略图展示示例
2017/01/03 Javascript
完美实现js选项卡切换效果(二)
2017/03/08 Javascript
微信小程序radio组件使用详解
2018/01/31 Javascript
微信小程序项目总结之点赞 删除列表 分享功能
2018/06/25 Javascript
Nodejs中使用puppeteer控制浏览器中视频播放功能
2019/08/26 NodeJs
javascript实现支付宝滑块验证码效果
2020/07/24 Javascript
JavaScript中window和document用法详解
2020/07/28 Javascript
[02:41]DOTA2英雄基础教程 亚巴顿
2014/01/02 DOTA
[00:32]DOTA2上海特级锦标赛 Ehome战队宣传片
2016/03/03 DOTA
使用Python生成随机密码的示例分享
2016/02/18 Python
Python堆排序原理与实现方法详解
2018/05/11 Python
python打印n位数“水仙花数”(实例代码)
2019/12/25 Python
Python实现aes加密解密多种方法解析
2020/05/15 Python
解决django 向mysql中写入中文字符出错的问题
2020/05/18 Python
SpringBoot首页设置解析(推荐)
2021/02/11 Python
HTML5混合开发二维码扫描以及调用本地摄像头
2017/12/27 HTML / CSS
HTML5 HTMLCollection和NodeList的区别详解
2020/04/29 HTML / CSS
旧时光糖果:Old Time Candy
2018/02/05 全球购物
美国正宗奢华复古手袋、珠宝及配饰网站:What Goes Around Comes Around
2018/07/21 全球购物
英国最大的宝石首饰超市:QP Jewellers
2018/09/23 全球购物
Mamaearth官方网站:印度母婴护理产品公司
2019/10/06 全球购物
意大利折扣和优惠券网站:Groupalia
2019/10/09 全球购物
公司前台辞职报告
2014/01/19 职场文书
《乌鸦和狐狸》教学反思
2014/02/08 职场文书
2015年财务人员工作总结
2015/04/10 职场文书
数据分析数据库ClickHouse在大数据领域应用实践
2022/04/03 MySQL