天天看點

小程式 | 原生實作左滑抽屜菜單

小程式 | 原生實作左滑抽屜菜單

使用小程式原生架構實作滑動顯示抽屜菜單效果,隻需50行代碼;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。