畫布 2D Web 應用程式/響應式按鈕
本章擴充套件了關於靜態按鈕的章節,涵蓋響應式按鈕,這些按鈕會隨著使用者點選它們或將滑鼠指標懸停在它們上面而改變其外觀。
本章的示例(可在線上獲取;也可作為可下載版本)擴充套件了關於靜態按鈕的章節的示例,使用三種影像之一(“正常”、“聚焦”和“按下”)根據按鈕的當前狀態呈現按鈕。因此,示例還必須包含每個按鈕的 cuiButton 物件,用於跟蹤其狀態。
儘管進行了這些更改,但示例的基本結構仍然相同:myPageProcess 函式重新繪製畫布並處理事件,同時呼叫每個按鈕的 process 函式以處理其重新繪製和影響它的事件處理。
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no">
<script>
function init() {
// get images
imageNormalButton.src = "normal.png";
imageNormalButton.onload = cuiRepaint;
imageFocusedButton.src = "selected.png";
imageFocusedButton.onload = cuiRepaint;
imagePressedButton.src = "depressed.png";
imagePressedButton.onload = cuiRepaint;
// set defaults for all pages
cuiBackgroundFillStyle = "#000000";
cuiDefaultFont = "bold 20px Helvetica, sans-serif";
cuiDefaultFillStyle = "#FFFFFF";
// initialize cui2d and start with myPage
cuiInit(myPage);
}
// create images for the buttons
var imageNormalButton = new Image();
var imageFocusedButton = new Image();
var imagePressedButton = new Image();
// create a color
var myColor = "#000000";
// create buttons
var button0 = new cuiButton();
var button1 = new cuiButton();
var button2 = new cuiButton();
// create a new page of size 400x300 and attach myPageProcess
var myPage = new cuiPage(400, 300, myPageProcess);
// a function to repaint the canvas and return false (if null == event)
// or to process user events (if null != event) and return true
// if the event has been processed
function myPageProcess(event) {
// draw and react to buttons
if (button0.process(event, 50, 50, 80, 50, "red",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
myColor = "#FF0000";
cuiRepaint();
}
return true;
}
if (button1.process(event, 150, 50, 80, 50, "green",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
myColor = "#00FF00";
cuiRepaint();
}
return true;
}
if (button2.process(event, 250, 50, 80, 50, "blue",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
myColor = "#0000FF";
cuiRepaint();
}
return true;
}
// repaint this page?
if (null == event) {
// draw color box
cuiContext.fillStyle = myColor;
cuiContext.fillRect(150, 150, 80, 80);
// background
cuiContext.fillStyle = "#404040";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
</script>
</head>
<body bgcolor="#000000" onload="init()"
style="-webkit-user-drag:none; -webkit-user-select:none; ">
<span style="color:white;">A canvas element cannot be displayed.</span>
</body>
</html>
本次討論假設您已閱讀關於靜態按鈕的章節,並重點介紹與該章節中介紹的示例的差異。
現在有三個影像(imageNormalButton、imageFocusedButton 和 imagePressedButton)。我們為它們都設定 onload 為 cuiRepaint(),以確保它們在載入後立即正確呈現。
程式碼不再將 myPage.isDraggableWithOneFinger 設定為 false,因為我們不允許使用者透過點選背景將顏色更改為黑色,因此可以允許他們拖動頁面。
此示例的主要新功能是三個 cuiButton 物件
// create buttons
var button0 = new cuiButton();
var button1 = new cuiButton();
var button2 = new cuiButton();
請注意,這些物件的建構函式沒有引數,因為所有物件都從“正常”狀態開始,其外觀由對 process 函式的呼叫定義。出於同樣的原因,cui2d 中大多數 GUI 元素的建構函式都沒有引數。(cuiPage 是一個例外,主要是因為它的 process 函式僅在 cui2d 的渲染迴圈中內部呼叫。)
透過呼叫 process 函式來呈現和處理三個按鈕
// draw and react to buttons
if (button0.process(event, 50, 50, 80, 50, "red",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
myColor = "#FF0000";
cuiRepaint();
}
return true;
}
if (button1.process(event, 150, 50, 80, 50, "green",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
myColor = "#00FF00";
cuiRepaint();
}
return true;
}
if (button2.process(event, 250, 50, 80, 50, "blue",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
myColor = "#0000FF";
cuiRepaint();
}
return true;
}
按鈕的 process 函式要麼重新繪製按鈕(對於 null == event),要麼嘗試處理事件(對於 null != event)。如果事件已被處理,它將返回 true。在這種情況下,我們檢查按鈕是否被 isClicked() 方法點選。如果是這種情況,myColor 將相應地設定,並呼叫 cuiRepaint() 以請求重新繪製畫布。
此時,可能值得退一步,看看程式碼的結構。在 cui2d 中,對小部件的呼叫的總體結構為
var <widget> = new <widget type>();
...
if (<widget>.process(<event>, <configuration arguments>)) {
<handle widget events>
return true;
}
重要的是,所有相關資訊都在一個地方
- 小部件在頁面上的佈局由小部件 process 函式的配置引數指定
- 小部件的外觀也由配置引數指定。
- 程式對任何小部件事件的反應是在呼叫小部件 process 函式之後立即指定的。
唯一不在同一個地方的部分是對建構函式的呼叫。但是,該呼叫通常不會提供任何相關資訊,因為 cui2d 中大多數小部件建構函式都沒有引數。因此,所有相關部分都集中在程式碼中的一個地方。
將其與標準 GUI 程式設計進行比較,在標準 GUI 程式設計中,將佈局定義與外觀定義和事件處理分開被認為是良好的程式設計風格——通常甚至跨不同的檔案。雖然可能有充分的理由將這些內容分開,但它會使程式碼變得更難閱讀和更改;因此,原型製作更難,這很可能導致更少的原型製作,因此會導致更糟糕的產品。
cuiButton 型別在 cui2d.js 中實現。它定義了一個沒有引數的建構函式,該建構函式在初始狀態下建立了一個新物件。此初始狀態始終是使用者與物件互動之前的狀態
/**
* Buttons are clickable rectangular regions.
* @typedef cuiButton
*/
/**
* Creates a new cuiButton.
* @constructor
*/
function cuiButton() {
this.isPointerInside = false; // mouse or touch point inside rectangle?
this.isPointerDown = false; // mouse button down or finger down _on_this_button_?
this.identifier = -1; // the identifier of the touch point
this.hasTriggeredClick = false; // click event has been triggered?
}
然後定義一個函式來檢查按鈕是否已被點選
/** Returns whether the button has just been clicked. */
cuiButton.prototype.isClicked = function() {
return this.hasTriggeredClick;
}
此外,還定義了一個 process 函式。對於 event == null,這只是一個用於重新繪製按鈕的函式。對於 event != null,最好將其視為自動機的一步;即,根據按鈕的當前狀態(如 this 中定義)和 event,設定一個新狀態。請注意,通常最好讓頂層 if 語句區分 this 中變數的不同值(例如:if (!this.isPointerInside && !this.isPointerDown)),因為這表示按鈕的狀態。只有在這些 if 語句內部,程式碼才應區分不同型別的事件(例如:if (isIn && ("touchend" == event.type)))。這種結構允許程式設計師輕鬆地檢查是否涵蓋了所有狀態和所有狀態的相關事件。將這種結構與適當的縮排結合使用還可以讓讀者輕鬆地識別某個狀態以及從該狀態的所有轉換——這正是圖形狀態轉換圖也擅長的。(事實上,如果描述轉換的程式碼結構良好、註釋良好且格式良好,它可能與狀態轉換圖一樣易讀,但它具有機器可讀的優點。)
/**
* Either process the event (if event != null) and return true if the event has been processed,
* or draw the appropriate image for the button state in the rectangle
* with a text string on top of it (if event == null) and return false.
*/
cuiButton.prototype.process = function(event, x, y, width, height,
text, imageNormal, imageFocused, imagePressed) {
// choose appropriate image
var image = imageNormal;
if (this.isPointerDown && this.isPointerInside) {
image = imagePressed;
}
else if (this.isPointerDown || this.isPointerInside) {
image = imageFocused;
}
// check or repaint button
var isIn = cuiIsInsideRectangle(event, x, y, width, height, text, image);
// note that the event might be inside the rectangle (isIn == true)
// but the state might still be isPointerDown == false (e.g. for touchend or
// touchcancel or if the pointer went down outside of the button)
// react to event
if (null == event) {
return false; // no event to process
}
// clear isReleased (this is set only once after the click and has to be cleared afterwards)
this.hasTriggeredClick = false;
// ignore mouse or touch points that are not the tracked point (apart from mousedown and touchstart)
if (this.isPointerInside || this.isPointerDown) {
if ("touchend" == event.type || "touchmove" == event.type || "touchcancel" == event.type) {
if (event.identifier != this.identifier) {
return false; // ignore all other touch points except "touchstart" events
}
}
else if (("mousemove" == event.type || "mouseup" == event.type) && event.identifier >= 0) {
return false; // ignore mouse (except mousedown) if we are tracking a touch point
}
}
// state changes
if (!this.isPointerInside && !this.isPointerDown) { // passive button state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else if (isIn && ("mousemove" == event.type || "mouseup" == event.type ||
"touchmove" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = true;
if ("touchmove" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else {
return false; // none of our business
}
}
else if (this.isPointerInside && !this.isPointerDown) { // focused button state (not pushed yet)
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("touchmove" == event.type || "touchend" == event.type ||
"touchcancel" == event.type || "mousemove" == event.type || "mouseup" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return false; // none of our business
}
else {
return false; // none of our business
}
}
else if (!this.isPointerInside && this.isPointerDown) { // focused button state (pushed previously)
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else if (isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons))) {
this.isPointerDown = false;
this.isPointerInside = true;
this.identifier = -1; // mouse
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (isIn && ("touchmove" == event.type || ("mousemove" == event.type && 0 < event.buttons))) {
this.isPointerInside = true;
cuiRepaint();
return true;
}
else if (!isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons) ||
"touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && (("mousedown" == event.type && this.identifier < 0) ||
("touchstart" == event.type && this.identifier == event.identifier))) {
this.isPointerDown = false;
this.isPointerInside = false;
return false; // none of our business
}
else if ("touchmove" == event.type || "mousemove" == event.type) {
return true; // this is our event, we feel responsible for it
}
else {
return false; // none of our business
}
}
else if (this.isPointerInside && this.isPointerDown) { // depressed button state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else if (isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons))) {
this.isPointerDown = false;
this.isPointerInside = true;
this.identifier = -1; // mouse
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons) ||
"touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("touchmove" == event.type || ("mousemove" == event.type && 0 < event.buttons))) {
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && (("mousedown" == event.type && this.identifier < 0) ||
("touchstart" == event.type && this.identifier == event.identifier))) {
this.isPointerDown = false;
this.isPointerInside = false;
return false; // none of our business
}
else if ("touchmove" == event.type || "mousemove" == event.type) {
return true; // this is our event, we feel responsible for it
}
else {
return false; // none of our business
}
}
// unreachable code
return false;
}