# 手勢(shì)系統(tǒng)
業(yè)務(wù)開(kāi)發(fā)中,我們常需要監(jiān)聽(tīng)節(jié)點(diǎn) touch 事件,處理拖拽、縮放相關(guān)邏輯。由于 Skyline 采用雙線程架構(gòu),在進(jìn)行這樣的交互動(dòng)畫(huà)時(shí),會(huì)具有較大的異步延遲,這點(diǎn)可以參考 wxs 響應(yīng)事件。
Skyline 中 wxs 代碼運(yùn)行在 JS 線程,而事件產(chǎn)生在 UI 線程,因此 wxs 動(dòng)畫(huà) 性能有所降低,為了提升小程序交互體驗(yàn)的效果,我們內(nèi)置了一批手勢(shì)組件,使用手勢(shì)組件的優(yōu)勢(shì)包括
- 免去開(kāi)發(fā)者監(jiān)聽(tīng)
touch事件,自行計(jì)算手勢(shì)邏輯的復(fù)雜步驟 - 手勢(shì)組件直接在
UI線程響應(yīng),避免了傳遞到JS線程帶來(lái)的延遲
# 效果展示
下圖演示了使用手勢(shì)、協(xié)商手勢(shì)實(shí)現(xiàn)的拖動(dòng)小球,半屏彈窗手勢(shì)拖動(dòng)關(guān)閉,分段半屏等效果。點(diǎn)擊查看更多 Skyline 示例。

掃碼小程序示例,分別體驗(yàn) 基礎(chǔ)手勢(shì) 和 協(xié)商手勢(shì) 新特性

# 手勢(shì)組件
| 組件名稱 | 觸發(fā)時(shí)機(jī) |
|---|---|
<tap-gesture-handler> | 點(diǎn)擊時(shí)觸發(fā) |
<double-tap-gesture-handler> | 雙擊時(shí)觸發(fā) |
<scale-gesture-handler> | 多指縮放時(shí)觸發(fā) |
<force-press-gesture-handler> | iPhone 設(shè)備重按時(shí)觸發(fā) |
<pan-gesture-handler> | 拖動(dòng)(橫向/縱向)時(shí)觸發(fā) |
<vertical-drag-gesture-handler> | 縱向滑動(dòng)時(shí)觸發(fā) |
<horizontal-drag-gesture-handler> | 橫向滑動(dòng)時(shí)觸發(fā) |
<long-press-gesture-handler> | 長(zhǎng)按時(shí)觸發(fā) |
# 工作原理
手勢(shì)組件為虛組件,真正響應(yīng)事件的是其直接子節(jié)點(diǎn)。下方代碼中,我們給 container 節(jié)點(diǎn)添加了兩種類型的手勢(shì)監(jiān)聽(tīng)。
- 當(dāng)在屏幕上橫向滑動(dòng)時(shí),
horizontal-drag手勢(shì)節(jié)點(diǎn)的回調(diào)將被觸發(fā); - 當(dāng)在屏幕上縱向滑動(dòng)時(shí),
vertical-drag手勢(shì)節(jié)點(diǎn)的回調(diào)將被觸發(fā)。
<horizontal-drag-gesture-handler>
<vertical-drag-gesture-handler>
<view id="container"></view>
</vertical-drag-gesture-handler>
</horizontal-drag-gesture-handler>
觸摸屏幕時(shí),渲染引擎會(huì)從內(nèi)到外對(duì)手勢(shì)監(jiān)聽(tīng)器進(jìn)行手勢(shì)識(shí)別,當(dāng)某個(gè)手勢(shì)監(jiān)聽(tīng)器滿足條件時(shí),其余的手勢(shì)監(jiān)聽(tīng)器將會(huì)失效。如在 scroll-view 內(nèi)部添加縱向的手勢(shì)監(jiān)聽(tīng)時(shí),將會(huì)阻斷 scroll-view 內(nèi)的手勢(shì)監(jiān)聽(tīng)器,導(dǎo)致無(wú)法滑動(dòng)。
<scrol-view>
<vertical-drag-gesture-handler>
<view id="container"></view>
</vertical-drag-gesture-handler>
</scroll-view>
需要注意的是,pan 類型的判定條件比 vertical-drag 要寬松,因此縱向滑動(dòng)時(shí),vertical-drag 將會(huì)響應(yīng),而 pan 則會(huì)失效。當(dāng)橫向滑動(dòng)時(shí),pan 類型則會(huì)響應(yīng)。
<vertical-drag-gesture-handler>
<pan-gesture-handler>
<view id="container"></view>
</pan-gesture-handler>
</vertical-drag-gesture-handler>
# 通用屬性
| 屬性 | 類型 | 默認(rèn)值 | 必填 | 說(shuō)明 |
|---|---|---|---|---|
| tag | string | 無(wú) | 否 | 聲明手勢(shì)協(xié)商時(shí)的組件標(biāo)識(shí) |
| worklet:ongesture | eventhandler | 無(wú) | 否 | 手勢(shì)處理回調(diào) |
| worklet:should-response-on-move | callback | 無(wú) | 否 | 手指移動(dòng)過(guò)程中手勢(shì)是否響應(yīng) |
| worklet:should-accept-gesture | callback | 無(wú) | 否 | 手勢(shì)是否應(yīng)該被識(shí)別 |
| simultaneous-handlers | Array<string> | [] | 否 | 聲明可同時(shí)觸發(fā)的手勢(shì)節(jié)點(diǎn) |
| native-view | string | 無(wú) | 否 | 代理的原生節(jié)點(diǎn)類型 |
native-view 支持的枚舉值有 scroll-view 和 swiper。滾動(dòng)容器縱向滾動(dòng)時(shí),使用 <vertical-drag-gesture-handler> 手勢(shì)組件代理內(nèi)部手勢(shì),橫向滾動(dòng)時(shí),則使用 <horizontal-drag-gesture-handler>。
eventhandler類型是事件回調(diào),無(wú)返回值callback類型是開(kāi)發(fā)者注冊(cè)到組件的回調(diào)函數(shù),會(huì)在適當(dāng)時(shí)機(jī)被執(zhí)行以讀取返回值- 所有的回調(diào)都只能傳入一個(gè) worklet 回調(diào)
# 事件回調(diào)參數(shù)
# worklet:should-response-on-move
返回的參數(shù) pointerEvent 各字段如下。每次觸摸移動(dòng)時(shí)進(jìn)行回調(diào),返回 false 時(shí),則對(duì)應(yīng)的手勢(shì)組件無(wú)法收到該次 move 事件。
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| identifier | number | Touch 對(duì)象的唯一標(biāo)識(shí)符 |
| type | string | 事件類型 |
| deltaX | number | 相對(duì)上一次,X 軸方向移動(dòng)的坐標(biāo) |
| deltaY | number | 相對(duì)上一次,Y 軸方向移動(dòng)的坐標(biāo) |
| clientX | number | 觸點(diǎn)相對(duì)于可見(jiàn)視區(qū)左邊緣的 X 坐標(biāo) |
| clientY | number | 觸點(diǎn)相對(duì)于可見(jiàn)視區(qū)上邊緣的 Y 坐標(biāo) |
| radiusX | number | 返回能夠包圍接觸區(qū)域的最小橢圓的水平軸 (X 軸) 半徑 |
| radiusY | number | 返回能夠包圍接觸區(qū)域的最小橢圓的垂直軸 (Y 軸) 半徑 |
| rotationAngle | number | 返回一個(gè)角度值,表示上述由radiusX 和 radiusY 描述的橢圓為了盡可能精確地覆蓋用戶與平面之間的接觸區(qū)域而需要順時(shí)針旋轉(zhuǎn)的角度 |
| force | number | 用戶對(duì)觸摸平面的壓力大小 |
| timeStamp | number | 事件觸發(fā)的時(shí)間戳 |
Page({
shouldResponseOnMove(pointerEvent) {
'worklet'
return false
}
})
# worklet:should-accept-gesture
用法如下,框架手勢(shì)識(shí)別生效時(shí)進(jìn)行回調(diào),由開(kāi)發(fā)者決定手勢(shì)是否生效。以 Pan 手勢(shì)為例。
手指觸摸屏幕時(shí)進(jìn)入 State.Possible 狀態(tài),shouldAcceptGesture 返回 false 后進(jìn)入 State.CANCELLED 狀態(tài),返回 true 后進(jìn)入 State.Begin 狀態(tài),可繼續(xù)接收手續(xù) move 事件。
Page({
shouldAcceptGesture() {
'worklet'
return false
}
})
# worklet:ongesture
不同類型手勢(shì)組件返回的參數(shù)如下
# tap / double-tap
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| state | number | 手勢(shì)狀態(tài) |
| absoluteX | number | 相對(duì)于全局的 X 坐標(biāo) |
| absoluteY | number | 相對(duì)于全局的 Y 坐標(biāo) |
# pan / vertical-drag / horizontal-drag
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| state | number | 手勢(shì)狀態(tài) |
| absoluteX | number | 相對(duì)于全局的 X 坐標(biāo) |
| absoluteY | number | 相對(duì)于全局的 Y 坐標(biāo) |
| deltaX | number | 相對(duì)上一次,X 軸方向移動(dòng)的坐標(biāo) |
| deltaY | number | 相對(duì)上一次,Y 軸方向移動(dòng)的坐標(biāo) |
| velocityX | number | 手指離開(kāi)屏幕時(shí)的橫向速度(pixel per second) |
| velocityY | number | 手指離開(kāi)屏幕時(shí)的縱向速度(pixel per second) |
# scale
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| state | number | 手勢(shì)狀態(tài) |
| focalX | number | 中心點(diǎn)相對(duì)于全局的 X 坐標(biāo) |
| focalY | number | 中心點(diǎn)相對(duì)于全局的 Y 坐標(biāo) |
| focalDeltaX | number | 相對(duì)上一次,中心點(diǎn)在 X 軸方向移動(dòng)的坐標(biāo) |
| focalDeltaY | number | 相對(duì)上一次,中心點(diǎn)在 Y 軸方向移動(dòng)的坐標(biāo) |
| scale | number | 放大或縮小的比例 |
| horizontalScale | number | scale 的橫向分量 |
| verticalScale | number | scale 的縱向分量 |
| rotation | number | 旋轉(zhuǎn)角(單位:弧度) |
| velocityX | number | 手指離開(kāi)屏幕時(shí)的橫向速度(pixel per second) |
| velocityY | number | 手指離開(kāi)屏幕時(shí)的縱向速度(pixel per second) |
| pointerCount | number | 跟蹤的手指數(shù) |
- 多指滑動(dòng)時(shí),
focalX和focalY為多個(gè)觸摸點(diǎn)中心焦點(diǎn)的坐標(biāo) - 單指滑動(dòng)時(shí),
pointerCount = 1,此時(shí)效果同pan-gesture-handler,scale手勢(shì)是pan的超集。
# long-press
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| state | number | 手勢(shì)狀態(tài) |
| absoluteX | number | 相對(duì)于全局的 X 坐標(biāo) |
| absoluteY | number | 相對(duì)于全局的 Y 坐標(biāo) |
| translationX | number | 相對(duì)于初始觸摸點(diǎn)的 X 軸偏移量 |
| translationY | number | 相對(duì)于初始觸摸點(diǎn)的 Y 軸偏移量 |
| velocityX | number | 手指離開(kāi)屏幕時(shí)的橫向速度(pixel per second) |
| velocityY | number | 手指離開(kāi)屏幕時(shí)的縱向速度(pixel per second) |
# force-press
| 屬性 | 類型 | 說(shuō)明 |
|---|---|---|
| state | number | 手勢(shì)狀態(tài) |
| absoluteX | number | 相對(duì)于全局的 X 坐標(biāo) |
| absoluteY | number | 相對(duì)于全局的 Y 坐標(biāo) |
| pressure | number | 壓力大小 |
# 手勢(shì)狀態(tài)
所有手勢(shì) worklet:ongesture 回調(diào)均會(huì)返回一個(gè) state 狀態(tài)字段。
enum State {
// 手勢(shì)未識(shí)別
POSSIBLE = 0,
// 手勢(shì)已識(shí)別
BEGIN = 1,
// 連續(xù)手勢(shì)活躍狀態(tài)
ACTIVE = 2,
// 手勢(shì)終止
END = 3,
// 手勢(shì)取消
CANCELLED = 4,
}
我們將手勢(shì)分為如下兩種類型:
- 離散手勢(shì):
tap和double-tap,僅觸發(fā)一次 - 連續(xù)手勢(shì):其它類型的手勢(shì)組件,隨手指拖動(dòng)會(huì)觸發(fā)多次
tap-gesture-handler 手勢(shì)組件返回的 state 始終為 1。
pan-gesture-handler 手勢(shì)組件在一個(gè)完整的拖動(dòng)過(guò)程中,state 會(huì)按如下方式改變
- 手指剛接觸屏幕時(shí),
state = 0 - 移動(dòng)一小段距離,
pan手勢(shì)判定生效時(shí),state = 1 - 繼續(xù)移動(dòng),
state = 2 - 手指離開(kāi)屏幕
state = 3
由于嵌套的手勢(shì)會(huì)產(chǎn)生沖突(僅有一個(gè)最終判定識(shí)別生效),因此連續(xù)手勢(shì) state 的變化可能有如下一些情景,開(kāi)發(fā)者需要根據(jù) state 值來(lái)處理一些異常情況。
POSSIBLE -> BEGIN -> ACTIVE -> END正常流程POSSIBLE -> BEGIN -> ACTIVE -> CANCELLED提前中斷POSSIBLE -> CANCELLED手勢(shì)未識(shí)別
并不是所有的連續(xù)手勢(shì)均有 POSSIBLE 狀態(tài),如 scale-gesture-handler 手勢(shì)組件,當(dāng)雙指觸摸后松手,state 變化如下:
- 雙指觸摸屏幕,
state = 1, pointerCount = 2 - 雙指放大操作,
state = 2, pointerCount = 2 - 雙指離開(kāi)屏幕,
state = 3, pointerCount = 1,之后會(huì)相繼回調(diào) a.state = 1, pointerCount = 1b.state = 2, pointerCount = 1c.state = 3, pointerCount = 0
# 注意事項(xiàng)
- 手勢(shì)組件僅在
Skyline渲染模式下才能使用 - 手勢(shì)組件為虛組件,不會(huì)進(jìn)行布局,手勢(shì)組件上設(shè)置
style、class是無(wú)效的 - 手勢(shì)組件僅能含有一個(gè)直接子節(jié)點(diǎn),否則不生效
- 手勢(shì)組件的父組件樣式會(huì)直接影響其子節(jié)點(diǎn)
- 手勢(shì)組件的回調(diào)函數(shù)均需聲明為
worklet函數(shù) - 手勢(shì)不同于普通
touch事件,不會(huì)進(jìn)行冒泡 - 手勢(shì)組件的
eventhandler / callback均需聲明為worklet函數(shù),回調(diào)在UI線程觸發(fā)
# 使用方法
# 示例代碼
# Chaining API init 函數(shù)示例代碼
# 示例一:監(jiān)聽(tīng)拖動(dòng)手勢(shì)
<pan-gesture-handler on-gesture-event="handlePan">
<view></view>
</pan-gesture-handler>
Page({
handlePan(evt) {
"worklet";
console.log(evt.translateX);
},
});
# 示例二:監(jiān)聽(tīng)嵌套的手勢(shì)
<horizontal-drag-gesture-handler on-gesture-event="handleHorizontalDrag">
<vertical-drag-gesture-handler on-gesture-event="handleVerticalDrag">
<view class="circle">one-way drag</view>
</vertical-drag-gesture-handler>
</horizontal-drag-gesture-handler>
# 示例三:代理原生組件內(nèi)部手勢(shì)
對(duì)于 <scroll-view> 和 <swiper> 這樣的滾動(dòng)容器,內(nèi)部也是基于手勢(shì)來(lái)處理滾動(dòng)操作的。相比于 web,skyline 提供了更底層的訪問(wèn)機(jī)制,使得在做一些復(fù)雜交互時(shí),可以做到更細(xì)粒度、分階段的控制。
<vertical-drag-gesture-handler
native-view="scroll-view"
should-response-on-move="shouldScrollViewResponse"
should-accept-gesture="shouldScrollViewAccept"
on-gesture-event="handleGesture"
>
<scroll-view
scroll-y
type="list"
adjust-deceleration-velocity="adjustDecelerationVelocity"
bindscroll="handleScroll"
>
<view class="item" wx:for="{{list}}">
<view class="avatar" />
<view class="comment" />
</view>
</scroll-view>
</vertical-drag-gesture-handler>
以縱向滾動(dòng)的 <scroll-view> 為例,可使用 <vertical-drag-gesture-handler> 手勢(shì)組件,并聲明 native-view="scroll-view" 來(lái)代理其內(nèi)部手勢(shì)。
# 滾動(dòng)事件
當(dāng)滾動(dòng)列表時(shí),手勢(shì)組件的事件回調(diào)和 <scroll-view> 的 scroll 事件回調(diào)均會(huì)觸發(fā),它們的區(qū)別在于:
scroll事件僅在滾動(dòng)時(shí)觸發(fā),當(dāng)觸頂/底后,不再回調(diào)on-gesture-event手勢(shì)回調(diào)當(dāng)手指在屏幕上滑動(dòng)時(shí)會(huì)一直觸發(fā),直到松手
# 手勢(shì)控制
在前面介紹連續(xù)手勢(shì)狀態(tài)時(shí),我們知道手勢(shì)有自身的識(shí)別過(guò)程。例如 vertical-drag 手勢(shì),當(dāng)手指觸摸時(shí)為 POSSIBLE 狀態(tài),移動(dòng)一小段距離后才識(shí)別為 BEGIN 狀態(tài),此時(shí)稱手勢(shì)被識(shí)別(ACCEPT)。
# 1. 手勢(shì)識(shí)別
should-accept-gesture 屬性允許開(kāi)發(fā)者注冊(cè)一個(gè) callback,并返回一個(gè)布爾值,參與到手勢(shì)識(shí)別的過(guò)程。當(dāng)返回 false 時(shí),本次觸摸手勢(shì)不再生效,相關(guān)聯(lián)的 <scroll-view> 組件也無(wú)法滾動(dòng)。
# 2. 事件派發(fā)
should-response-on-move 屬性允許開(kāi)發(fā)者注冊(cè)一個(gè) callback,并返回一個(gè)布爾值,參與到事件派發(fā)的過(guò)程。當(dāng)返回 false 時(shí),當(dāng)次 move 的事件不再派發(fā),相關(guān)聯(lián)的 <scroll-view> 不繼續(xù)滾動(dòng)。該回調(diào)在手指移動(dòng)過(guò)程中會(huì)持續(xù)觸發(fā),可隨時(shí)改變,進(jìn)而控制滾動(dòng)容器繼續(xù)/暫停滾動(dòng)。
Page({
// 這里返回 false,則 scroll-view 無(wú)法滾動(dòng)
// should-accept-gesture 會(huì)在手勢(shì)識(shí)別的一開(kāi)始觸發(fā)一次
// should-response-on-move 是在 move 過(guò)程中不斷觸發(fā)
shouldScrollViewAccept() {
'worklet'
return true
},
// 這里返回 false,則 scroll-view 無(wú)法滾動(dòng)
shouldScrollViewResponse(pointerEvent) {
'worklet';
return true;
},
// 手指滑動(dòng)離開(kāi)滾動(dòng)組件時(shí),指定衰減速度
adjustDecelerationVelocity(velocity) {
'worklet';
return velocity;
},
// scroll-view 滾動(dòng)到邊界后,手指滑動(dòng),scroll 事件不再觸發(fā)
handleScroll(evt) {
'worklet';
},
// scroll-view 滾動(dòng)到邊界后,手指滑動(dòng),手勢(shì)回調(diào)仍然會(huì)觸發(fā)
handleGesture(evt) {
'worklet'
},
});
# 示例四:手勢(shì)協(xié)商
一些場(chǎng)景下,我們會(huì)遇到手勢(shì)沖突。如下代碼所示,存在嵌套的 <vertical-drag-gesture-handler> 組件,我們希望 outer 手勢(shì)組件來(lái)處理縱向的拖動(dòng),inner 手勢(shì)組件處理列表的滾動(dòng),但實(shí)際僅 inner 的手勢(shì)回調(diào)會(huì)觸發(fā)。
嵌套的同類型手勢(shì)組件,當(dāng)內(nèi)層的手勢(shì)識(shí)別后,外層的手勢(shì)組件將不會(huì)被識(shí)別。
<vertical-drag-gesture-handler tag="outer">
<vertical-drag-gesture-handler tag="inner" native-view="scroll-view">
<scroll-view scroll-y></scroll-view>
</vertical-drag-gesture-handler>
</vertical-drag-gesture-handler>
但上述場(chǎng)景又是很常見(jiàn)的,例如視頻號(hào)的評(píng)論列表,列表的滾動(dòng)和整個(gè)評(píng)論區(qū)的拖動(dòng)銜接的十分流暢。手勢(shì)協(xié)商機(jī)制用于解決該類問(wèn)題,使用上也十分簡(jiǎn)單,simultaneous-handlers 屬性聲明多個(gè)手勢(shì)可同時(shí)觸發(fā)。
<vertical-drag-gesture-handler tag="outer" simultaneous-handlers="{{["inner"]}}">
<vertical-drag-gesture-handler tag="inner" simultaneous-handlers="{{["outer"]}}" native-view="scroll-view">
<scroll-view scroll-y></scroll-view>
</vertical-drag-gesture-handler>
</vertical-drag-gesture-handler>
此時(shí),outer 和 inner 手勢(shì)組件的 on-gesture-event 回調(diào)會(huì)依次觸發(fā),結(jié)合上面提到的手勢(shì)控制原理,可以實(shí)現(xiàn)預(yù)期的效果。完整代碼參考示例 demo。