小程序原生实现左滑抽屉菜单

2022-07-22,,

目录
  • wxs 响应事件
  • 方案a
    • 页面结构和样式
  • wxs 事件回调函数
    • wxs 脚本
      • 遮罩层
        • 方案b
          • 为什么要使用 wxs
            • 结语 & 参考资料
              • 参考资料:

                在移动端,侧滑菜单是一个很常用的组件(通常称作 drawer,抽屉)。因为现在手机屏幕太大,点击角落的菜单按钮明显不如在屏幕中间滑动方便。

                相比其他平台,小程序的组件库支持明显还不够完善,各个框架也还不太成熟。由于之前使用框架的过程中被各种神秘bug搞的头秃,还是用回了原生环境。

                最近研究了一下如何在原生框架中实现滑动抽屉菜单效果,本来以为很麻烦,结果发现其实只需要几十行代码,而且可以类比实现很多灵活的效果。感觉现在网上相关资料较少,因此在此分享一下。除了文中贴出的代码块,也可以点击链接在小程序开发工具中预览效果、查看代码片段。这里实现了三种常见效果,先看一下动图,下面将一一讲解代码实现。

                a 菜单在上层 

                a2 菜单在上层,下层遮罩 

                b 菜单在下层

                wxs 响应事件

                手势控制菜单的原理很简单:小程序提供了一系列触摸手势触发的事件,包括触摸开始、移动、结束(touchstart, touchmove, touchend)等等。在这些事件上绑定自定义的事件响应函数,即可实现根据手势打开关闭菜单的操作。

                出于性能考虑,事件处理函数最好放在 wxs、而不是 js 文件中。具体原理与小程序的运行环境有关,感兴趣的话可以去文末查看。wxs 是小程序的专用脚本语言(wxs 与 js 的关系相当于 wxss 与 css 的关系),语法和 js 类似,有部分区别,比如:

                • 与 js 隔离,不能调用其他 javascript 文件中定义的函数,也不能调用小程序提供的api
                • 只能响应小程序内置组件的事件,不支持自定义组件的事件回调
                • 变量与函数默认为模块私有,通过 module.exports 对外暴露
                • 使用标签在 wxml 中引入使用(必须使用相对路径)

                wxs 文件和 wxml 文件中的基本写法如下:

                // index.wxs
                
                function touchstart(e, ins) {}
                function touchmove(e, ins) {}
                function touchend(e, ins) {}
                
                module.exports = {
                  touchstart: touchstart,
                  touchmove: touchmove,
                  touchend: touchend
                }
                <wxs module="drawer" src="./index.wxs"></wxs>
                
                <view bindtouchstart="{{drawer.touchstart}}"
                      bindtouchmove="{{drawer.touchmove}}" 
                      bindtouchend="{{drawer.touchend}}">
                </view>
                
                

                方案a

                页面结构和样式

                这是最常见的抽屉菜单样式之一,滑动主体内容不动,菜单在上层显示。首先写出基本的 html 结构和 css 样式(省略了一些美观方面的样式表):

                <wxs module="drawer" src="./index.wxs"></wxs>
                
                <view>
                  <view class="main" bindtouchstart="{{drawer.touchstart}}"
                    bindtouchmove="{{drawer.touchmove}}" bindtouchend="{{drawer.touchend}}">
                    <view>
                      右滑显示侧边菜单 方案a
                    </view>
                  </view>
                
                  <view class="drawer" data-drawerwidth="150">
                    <view class="drawer-item">drawera</view>
                    <view wx:for="{{[1, 2, 3]}}" class="drawer-item">
                      <text>menu item {{item}}</text>
                    </view>
                  </view>
                </view>
                
                

                wxml 中的几个重点:

                • 正确引入 wxs 模块(必须用相对路径)
                • 进行滑动手势时菜单是隐藏的,所以实际上是在主界面上进行滑动,所以三个滑动事件回调需要绑定在主体内容的 view 上面
                • 进行移动的是 .drawer 元素,需要设置好 class 属性方便获取
                • 抽屉元素的 data-drawerwidth 属性通过 dataset 传值给 wxs 脚本,规定了菜单的宽度,需要和样式保持一致

                wxss 没啥好说的,写在注释里了:

                .main {
                  height: 100vh;
                  width: 100%;
                  position: absolute;
                }
                
                .drawer {
                  height: 100vh;
                  width: 150px;
                  position: absolute;
                  transition: transform 0.4s ease; /* 位移使用transform实现,加个过渡动画更顺滑 */
                  left: -150px;  /* width、偏移与wxml中的数值保持一致,初始状态隐藏菜单 */
                }

                wxs 事件回调函数

                wxs 函数有两个入参

                • event 是小程序事件对象,并在此基础上多了触发事件的组件的实例 event.instance
                • ownerinstance 是触发事件的组件的父组件(页面)的实例

                wxs 中组件实例是封装好的 componentdescriptor 对象,能够操作组件的 dataset、设置 style、class 等,对于交互动画基本够用了。更多用法可参考文档。

                var wxsfunction = function(event, ownerinstance) {
                    var instance = ownerinstance.selectcomponent('.classselector') // 返回组件的实例
                    instance.setstyle({
                        "font-size": "14px" // 支持rpx
                    })
                    instance.getdataset()
                    instance.setclass(classname)
                
                    return false // 不往上冒泡,相当于调用了同时调用了stoppropagation和preventdefault
                }
                
                

                wxs 脚本

                条件判断为主,逻辑没啥特别的,结合情景不难理解

                • 不要用 let, const 声明变量,会报错
                • 把设置 transform 属性 x 位移的代码简单封装一下,看起来更美观
                • judge point 类似于吸附效果,就是菜单划出来超过某一位置就自动把剩余部分打开
                var startmark = 0;
                var status = 0;  // 菜单开闭状态
                var judgepoint = 0.7;
                
                function touchstart(e, ins) {
                  var pagex = (e.touches[0] || e.changedtouches[0]).pagex;
                  startmark = pagex;
                }
                
                function touchmove(e, ins) {
                  var pagex = (e.touches[0] || e.changedtouches[0]).pagex;
                  var offset = pagex - startmark;
                  var drawercomp = ins.selectcomponent('.drawer');
                  var drawerwidth = drawercomp.getdataset().drawerwidth;
                
                  if (offset > 0 && status == 0) {
                    setcomptransx(drawercomp, math.min(drawerwidth, offset))
                  } else if (offset < 0 && status == 1) {
                    setcomptransx(drawercomp, math.max(0, offset))
                  }
                }
                
                function touchend(e, ins) {
                  var pagex = (e.touches[0] || e.changedtouches[0]).pagex;
                  var offset = pagex - startmark;
                  var drawercomp = ins.selectcomponent('.drawer');
                  var drawerwidth = drawercomp.getdataset().drawerwidth;
                
                  if (offset > 0 && status == 0) {
                    if (offset < drawerwidth * judgepoint) {
                      setcomptransx(drawercomp, 0);
                    } else {
                      setcomptransx(drawercomp, drawerwidth);
                      status = 1;
                    }
                  } else if (offset < 0) {
                    setcomptransx(drawercomp, 0);
                    status = 0;
                  }
                }
                
                function setcomptransx(comp, x) {
                  comp.setstyle({
                    transform: 'translatex(' + x + 'px)',
                  })
                }
                
                module.exports = {
                  touchstart: touchstart,
                  touchmove: touchmove,
                  touchend: touchend
                }

                遮罩层

                点击文首或文末链接在小程序开发工具中查看完整代码。

                遮罩层只需要在菜单和主容器之间增加一个 view 即可:

                <view class="main"></view>
                <view class="mask" data-maxopacity="0.6"></view>
                <view class="drawer" data-drawerwidth="150"></view>
                

                样式中很重要的是这个 pointer-events 属性,设置为 none 之后点击动作会穿透这个 view 达到下层。因为遮罩层不像抽屉是处在画面以外的,它虽然透明度为0,但实际上一直覆盖在 .main 上方,如果不加这个属性,所有对 .main 的点击操作都会点到 .mask 上面,那不管是滑动还是其他按钮都无效了。

                .mask {
                  height: 100vh;
                  width: 100%;
                  position: fixed;
                  transition: opacity 0.4s ease;
                  opacity: 0;
                  pointer-events: none;
                  background-color: #548ca8;
                }
                

                wxs 脚本也基本完全一致,只需要以相似的方法获取到 .mask 的实例以及 dataset 中的透明度参数,并在设置位移属性的同时设置遮罩层的透明度属性即可。

                function setdrawer(x) {
                  setcomptransx(drawercomp, x);
                  maskcomp.setstyle({
                    opacity: x / drawerwidth * maskopacity,
                  })
                }
                

                方案b

                点击文首或文末链接在小程序开发工具中查看完整代码。

                方案b 与方案a 的区别主要在于滑动时是主界面向右移动露出下层的菜单,其余各部分实现并无不同。这里只贴出主要差异的部分。

                因为移动的是 .main 元素,因此把宽度配置数据放到了该元素的标签中,这样可以少获取一个组件实例。

                <view class="drawer"></view>
                
                <view class="main" 
                      data-drawerwidth="150" 
                      bindtouchstart="{{drawer.touchstart}}"
                      bindtouchmove="{{drawer.touchmove}}" 
                      bindtouchend="{{drawer.touchend}}">
                </view>
                
                

                transition 动画属性也放在 .main 中,.drawer 的偏移不需要了。

                .main {
                  height: 100vh;
                  width: 100%;
                  position: absolute;
                  transition: transform 0.4s ease;
                }
                
                .drawer {
                  height: 100vh;
                  width: 150px;
                  position: absolute;
                }
                
                

                wxs 脚本中除了获取的组件不同外,连设置位移都不需要改。

                function touchmove(e, ins) {
                  var pagex = (e.touches[0] || e.changedtouches[0]).pagex;
                  var offset = pagex - startmark;
                  var maincomp = ins.selectcomponent('.main');
                  var drawerwidth = maincomp.getdataset().drawerwidth;
                
                  if (offset > 0 && status == 0) {
                    setcomptransx(maincomp, math.min(drawerwidth, offset))
                  } else if (offset < 0 && status == 1) {
                    setcomptransx(maincomp, math.max(0, offset))
                  }
                }
                
                

                为什么要使用 wxs

                小程序在很多地方与 web 开发很像,但底层存在一些区别。网页中,渲染和脚本执行在同一个线程中执行(因此执行脚本可能会导致页面整个卡死);小程序在不同的线程中分别运行逻辑层(js脚本)和渲染层(wxml和wxss),线程间经由客户端(native)进行通信。

                因此,如果使用 js 脚本响应事件,每次触发 touchmove 都会产生两次进程间通信(下图左所示),通信开销较大;同时“setdata 渲染也会阻塞其它脚本执行”(文档这么说的,我也不知道为什么)。由于一次手势会触发巨量的 touchmove 事件,上述原因会造成动画的卡顿。

                而 wxs 函数运行在视图层,不存在上述问题(下图右所示)。

                结语 & 参考资料

                以上就是原生小程序的几种抽屉菜单实现方法,希望对你有所帮助;对于文中存在的疏漏欢迎讨论指正。

                点击链接可以在小程序开发工具中查看完整代码(使用小程序开发工具的代码片段分享,对开发工具版本有一定要求)。他这个分享代码片段有点玄学,如果直接打开失败,可以在登录后尝试在“项目-导入代码片段”中直接输入链接或链接最后一段id。

                参考资料:

                小程序框架/视图层/事件系统/wxs 响应事件

                官方 demo

                小程序宿主环境

                到此这篇关于小程序原生实现左滑抽屉菜单的文章就介绍到这了,更多相关小程序 左滑抽屉菜单内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

                《小程序原生实现左滑抽屉菜单.doc》

                下载本文的Word格式文档,以方便收藏与打印。