最近在認真開發一個 Web 上的互動程式,其中一個基本的功能,就是要透過 HTML 的按鈕、來做操作物體的移動與旋轉;而為了方便操作,除了一般的按下按鈕觸發一次事件外,也希望可以透過壓著按鈕,來做到持續性地控制。
而由於 HTML 現在的標準按鈕( <input type="button" />、參考)雖然可以長按,但是並沒有辦法偵測他是否被按下,所以,要不就是用別人寫好的,不然就是得自己實作了~
而由於一些原因,Heresy 是選擇自己實作這部分的功能。由於目標平台除了桌上型電腦外,還要考慮以平板為主的行動裝置,所以除了傳統的滑鼠事件外,自然就牽扯到一大多觸控的事件了。而本來想得很簡單,後來真的開始寫了之後,才發現要同時支援觸控、滑鼠,其實還滿繁瑣的,有一堆小細節都得注意到…
這方面的資料,可以參考 HTML5Rock 的《Touch And Mouse》這篇文章,本文有不少內容是參考這篇文章寫的。
針對滑鼠事件處理
HTML 的按鈕只有提供 click 的事件(參考)、並沒有辦法知道他是否正被壓著,所以這部份的程式得自己來寫。而要處理這樣事情的基本概念,就是自己去偵測「按下」與「放開」這兩種事件、用來記錄按鈕是否被按下。
而如果是一般的網頁開發的話,最簡單的想法,就是去偵測 mousedown 和 mouseup 這兩個事件了。前者就是滑鼠按下、後者則是滑鼠放開,透過處理這兩個事件,基本上就可以做按鈕是否正被按著的簡單判斷了~
例如下面的程式碼,會在滑鼠按下按鈕的時候,把按鈕的文字改成紅色、放開後再改回黑色。
<input type="button" value="test"
onmousedown ="this.style.color='red';"
onmouseup ="this.style.color='black';"
/>
但是光這樣寫,其實還有點問題;因為實際上,除了放開滑鼠的情況會讓按鈕被釋放外,壓著按鈕的狀況下,把游標移出按鈕,也會有釋放按鈕、但是卻不會觸發到 mouseup 的事件!所以在這邊,還需要去處理 mouseout 事件,才能比較好地對應滑鼠的事件。
這樣的話,按鈕會變成:
<input type="button" value="test"
onmousedown ="this.style.color='red';"
onmouseup ="this.style.color='black';"
onmouseout ="this.style.color='black';"
/>
這樣的話,滑鼠游標離開按鈕的時候,也會去把按鈕的文字改回黑色。如果是針對一般滑鼠的操作環境,上面這樣的程式應該就算差不多了~(其實還有些狀況可能會有問題,不過基本上 Heresy 覺得可無視)
針對觸控裝置做處理
但是如果把上面這個按鈕拿到觸控的行動裝置上(Heresy 這邊是用 Android 的 Chrome 做測試),應該會發現,他的動作和預期的差異很大…這主要是因為,網頁瀏覽器針對觸控裝置、其實有提供特殊的「觸碰」(touch)事件;他雖然也會把觸碰事件去模擬成滑鼠事件,但是行為模式還是不同的。
所以針對觸控裝置,這邊除了去處理滑鼠事件外,也需要去處理觸控的事件。這邊需要處理的事件,包括了開始接觸的 touchstart、結束觸控的 touchend 這兩個事件。如果把這部分的事件處理,也加上去的話,就會變成:
<input type="button" value="test"
onmousedown ="this.style.color='red';"
ontouchstart ="this.style.color='red';"
onmouseup     ="this.style.color='black';"
onmouseout ="this.style.color='black';"
ontouchend ="this.style.color='black';"
/>
如此一來,在觸控裝置上,這個按鈕的運作也就比較正常一點了~
但是,實際上這樣的事件處理,其實還是不夠的。因為在觸控裝置上的瀏覽器,通常也可以靠滑動來做捲動的動作;也就是說,當手指碰到螢幕上的按鈕、觸發了 touchstart 事件後,如果又往其他方向移動的話,有可能就會觸發到其他例如捲動頁面的事件,在這情況下,touchend 的事件是不會被觸發的!
所以,如果以上面的程式碼來執行的話,那用手指按下按鈕後、的確可以讓按鈕的文字變紅色,但是如果在沒放開手指的情況下,就在螢幕上把手指移出按鈕,那就會發現雖然按鈕已經被放開了,但是裡面的文字還是維持紅色、而不會變回黑色。
而要解決這個的問題,Heresy 自己是找到兩種方法。
ontouchcancel
第一個方法,是去針對 touchcancel 這個事件作處理。當觸控變成開始拖曳的時候,瀏覽器會先觸發這個事件、代表處控事件已經被取消了;所以只要也針對這個事件作處理,就可以避免上面的問題了。解決方法,就是再加上一行:
ontouchcancel ="this.style.color='black';"
這樣就可以在從壓按鈕變成拖曳的時候,也把按鈕的文字改回黑色了。而根據網路上找到的說法,在處理 touchend 事件的同時,也一定要去處理 touchcancel 的事件,才不會出問題的。
但是這樣做的缺點,是就算手指只是在按鈕內做小幅度的移動(很有可能發生),也會取消掉按鈕壓下的狀態,所以其實不是那麼方便。
取消移動造成的影響
第二個方法,則是更直接一點,直接把在按鈕上移動的事件給取消掉!這個方法,就是去處理 touchmove 這個事件,然後透過呼叫事件的 preventDefault() 這個函式,來取消所產生的事件、避免 touchcancel 事件的產生。
要這樣做的方法,就是先宣告一個用來取消事件的函式:
function CancelEvent(e) {
e.preventDefault();
}
然後再設定當 touchmove 這個事件被觸發後,就去呼叫這個函式;也就是再加上一行:
ontouchmove ="CancelEvent(event);"
而這樣做的話,就算手指壓下按鈕後,有移動位置,也不會造成問題了~但是相對的,它的效果會是:沒辦法壓著這個按鈕來拖曳整個頁面,而且就算把手指移出按鈕外、只要沒有離開螢幕,按鈕上的字都還會是紅色的。雖然不算完美,但是也算是夠用了。
不過要補充的是,這兩個方法在 Windows 8.1 的 IE11 上,看來都沒有用… orz
但是由於 IE11 對於移動手指造成的頁面拖曳比較沒那麼敏感,所以問題不算太大。
最終版本
結合了上面的各項說明,最後一個可以對應滑鼠與觸控裝置的可以長壓按鈕、大概會變成下面這樣:
<input type="button" value="test"
onmousedown ="ButtonPressed(this);"
ontouchstart ="ButtonPressed(this);"
onmouseup     ="ButtonReleased(this);"
onmouseout ="ButtonReleased(this);"
ontouchend ="ButtonReleased(this);"
ontouchcancel ="ButtonReleased(this);"
ontouchmove ="CancelEvent(event);"
/>
而其中 CancelEvent() 這個函式的內容,則就是:
function CancelEvent(e) { e.preventDefault();}
至於 ButtonPressed() 和 ButtonReleased() 這兩個函式要怎麼寫呢,基本上就是看個人需求了~
而這樣的一個按鈕,雖然在觸控裝置上,可能還是會遇到一些小問題,不過大致上,應該算是足夠了。至於有沒有更好的處理方法?Heresy 就不曉得了。至少,目前在 Heresy 這邊,這樣的程式是 ok 的。
停止瀏覽器的縮放
雖然上面寫出來的按鈕已經把大部分的問題都解掉了,但是真正在使用的時候,還會遇到一個問題就是:當用高頻率去連點按鈕的時候,會觸發到瀏覽器提供的縮放事件,導致整個操作被妨礙到。
如果要避免這個問題發生的話,可以透過特別的方法,來鎖定頁面的縮放。這個方法就是在 HTML 的 Header 裡面,加上一個名為「viewport」的 meta:
<meta name="viewport"
content='width=device-width,
initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,
user-scalable=0' />
透過這樣的標籤,可以指定頁面的寬度、以及縮放比例、並強制地讓網頁無法被縮放,並把縮放比例鎖死在 1.0。
(參考《Mobile Web 前端技術筆記(一): Viewport的設定》)
但是相對的,這樣不但讓點兩下縮放的功能被禁用,連用兩隻手指的縮放,也會一樣不能用;所以是否要使用這樣的功能,就是要看自己的需求了。
另外,此功能在 Windows 8.1 的 IE11 上,是沒有用的;而像是 Android 的 Chorme 也可以在設定內「協助工具」,找到「強制啟用縮放功能」,來無視這個標籤。