[crop系列] 4 drag實作
crop系列之第n篇,這篇要來說怎麼用rxjs來實作drag並且要支援手機的touch行為
如果還沒看過系列其他篇的麻煩這邊請
[crop系列] 1 scale和position的糾葛、[crop系列] 2 canvas drawimage應用、[crop系列] 3 圖片mask
所有的範例都會是以react為主,angular版本改天再出
mouse
Drag的操作行為分為desktop和mobile兩種,在desktop上面大家都很清楚就是用mouse,但是在mobile上面則用touch,基本上兩個的概念是差不多,只是差在事件上面的不同,那就讓我們先用mouse的event來做
event參數
首先要先了解drag的行為模式,點在一個物件上(down),然後移動(move),最後放開(up),那物件要根據移動的位移量來跟著移動,所以要計算位移量的話就會是跟x, y軸有關係,因此我們先來看看MDN有哪些參數可以使用
- clientX、clientY
Returns the coordinate of the mouse pointer, relative to the current window, when the mouse event was triggered - movementX、movementY
Returns the coordinate of the mouse pointer relative to the position of the lastmousemove
event - offsetX、offsetY
Returns the coordinate of the mouse pointer relative to the position of the edge of the target element - pageX、pageY
Returns the coordinate of the mouse pointer, relative to the document, when the mouse event was triggered - screenX、screenY
Returns the coordinate of the mouse pointer, relative to the screen, when an event was triggered
上面這些講的很漏漏等,還是用一張圖來呈現比較明確
看完圖片有沒有比較清楚知道每一個的用途,那最好用的應該會是movement
這個參數,但這個不支援部分瀏覽器,所以我們要改用其他的參數,而其中client
這個會比offset
還要好,等一下我們來實作看看兩個的差異
非rx實作
那我們先來用傳統的addEventListener來實作看看,首先我們用一個component叫做dragable,裡面只有一個div作為可以被拖拉的element,那我們第一步就是註冊各個事件
當然不要忘記在
componentWillUnMount
取消註冊
1 | componentDidMount () { |
- mouseDown
透過全域變數來記當下的座標以及開始拖拉
1 | mouseDown = event => { |
- mouseUp
取消拖拉狀態
1 | mouseUp = event => { |
- mouseMove
將目前所在位置 減掉 前一次移動的值 再加上目前element所在位置
,最後將計算的結果寫入到element
1 | mouseMove = event => { |
寫完以上的程式實際操作一下,發現會有點卡卡的,尤其是滑鼠會跑超過drag的框,但又希望dragable的物件能跟著走,因此在監聽的地方要改一下,mouseup
、mousemove
要改成document的全域監聽
1 | componentDidMount () { |
這樣改完再跑一次,發現還是跑的不順,這時候我們改用client
來取代offset
,再跑一次會發現整個順暢了,非常完美!完整的範例如下
那接著我們就要來使用rx的方法實作同樣的功能,首先一樣先定義事件
1 | componentDidMount(){ |
事件組合
接著要來開始把這些事件組合,首先我們是透過mousedown
作為起點,然後mousemove
接著再mouseup
的時候結束
1 | componentDidMount(){ |
這時候將up$
的事件當做結束,但是這樣會有一個問題,因為takeUntil
是有值進來的時候會觸發complete
,因此只要mouseup過一次以後這個流程就結束了
但期望的應該是每次mousedown
的時候都要能夠被觸發,因此我們應該要將takeUntil(up$)
放在move$
的身上,讓流程只斷移動,而不會影響到down$
1 | componentDidMount(){ |
計算位移
事件的流程搞定以後就可以開始的來針對位移量做計算,概念會跟前面有點類似,但又會有點不同,直接來說說不同的點,不是每次的偏移量,而是這次位置和初始位置的偏移量
1 | down$.pipe( |
很開心的寫完執行,發現怎麼跑的飛快,原因是delta的量是重新計算後跟一開始的偏移量,但我們卻拿最新的位置做計算,那當然會跑的飛快,這邊要微調一下,從down$
的時候要就拿到state
的資料
1 | down$.pipe( |
在執行看看,跟前面的效果是一樣,完美!完整範例如下
# touch做完滑鼠的接著當然就是要處理mobile上面的行為,跟mouse的差不多主要都是由touch
的事件做處理,一樣先來了解MDN的參數內容
event參數
- touches
- targetTouches
- changedTouches
在mobile有多點觸控的功能,所以會是一個陣列,所以在寫的時候必須要注意一下目前的觸控數有多少,那在上面的兩個屬性中,有的參數其實跟mouse
一樣
- screen
- client
- page
實作
了解完參數以後就可以開始動工啦,這邊我就不用傳統的寫法而是直接採用rx
1 | componentDidMount(){ |
這邊有兩個地方要注意一下
- 要記得filter個數,不然會發生跟預期不一樣的行為
- 這邊的邏輯好像跟上面mouse的差不多
重構
1 | componentDidMount(){ |
因為已經過濾掉只有一個觸控點的情況下會往後,因此直接拿陣列的第一個物件中client資料來使用,這樣在後面的資料都統一用{x, y}
的物件做處理
記得要加上*
preventDefault
*,但千萬記得不要加在end上面,會死的不明不白…
結合touch、mouse
前面有看到兩個的相似之處,而且在touch的時候我們也針對資料做了處理,接著就是要把這兩段的程式做結合,主要要進行串接的有兩個地方
資料的處理
將mouse的事件也加上前置處理1
2
3
4
5
6
7parseMouseEvent = obs => obs.pipe(
tap(event => event.preventDefault()),
map(event => ({
x: event.clientX,
y: event.clientY,
})),
);事件對事件的串接
先把move
加上end/up
的結束處理,再來就是兩個相同類型的事件做merge
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const start$ = fromEvent(this.dragNode, 'touchstart').pipe(this.parseTouchEvent);
const end$ = fromEvent(document, 'touchend');
const touchMove$ = fromEvent(document, 'touchmove').pipe(
this.parseTouchEvent,
takeUntil(end$),
);
const down$ = fromEvent(this.dragNode, 'mousedown').pipe(
this.parseMouseEvent,
merge(start$),
);
const up$ = fromEvent(document, 'mouseup');
const move$ = fromEvent(document, 'mousemove').pipe(
this.parseMouseEvent,
takeUntil(up$),
merge(touchMove$),
);
最後就是針對新的物件把邏輯做個微調
1 | down$.pipe( |
物件只能在區域內
最後來點進階的,要把dragable的物件限制在一個區域內,也就是左上、左下、右上、右下
但是要判斷四個資料寫起來也很煩,漏漏等,因此換個角度思考看看,如過只判斷一邊在一個範圍值之內是不是就可以,例如左邊只能在0100,上面只能在0200
首先一定要取出外框以及dragable的width、height,接著就要把外框的width、height扣掉dragable的width、height,這樣就能知道物件不超出的最大值是多少
1 | const parent = this.dragNode.parent; |
接著就是要來判斷最大最小值,然後這邊用個公式目前值
和最小值
的較大者,然後再和最大值
取較小者
1 | validValue = (value, min, max) => Math.min(Math.max(value, min), max) |
最後直接來看一下完整的程式
# 結論先說一下,原本打算這篇就要給他全部結束,但後來發現很難,因為太多程式碼要交代
Rx真的是我們的好朋友,尤其是在這種情況下使用,你可以發現很多邏輯都可以被抽出去,而且可以合併來合併去,我們就只要關注真正的邏輯面就好(要算算看我總共用了哪些operator嗎?其實答案就在程式的最上面,有沒有覺得使用的類型不多)
下一篇就會是更進階的zoom,會搭配上比較多的計算,希望下篇能夠完結!