Canvas 2D Web 應用程式/可拖動物件
本章介紹可拖動物件,即可以使用滑鼠或單指觸控手勢拖動的影像。呈現方法的一個顯著特點是,任何數量的物件都可以同時在多點觸控裝置上拖動。此外,應用程式程式設計師不必擔心處理多個同時觸控事件:過程函式只接收帶有單個座標對的事件,並且一次只處理一個物件。
本章的示例(可在 網上 獲得;也可以作為 可下載版本 獲得)展示了兩個可拖動物件:一個不能向下拖動超過 100 畫素;第二個只能在兩點之間的直線上拖動,如果不再拖動,則會捕捉到其中一個端點。以下部分討論如何設定這些可拖動物件。有關其他部分,請參見有關 框架 的章節。
程式碼使用三個影像建立一個包含兩個可拖動物件的頁面(有關影像的討論,請參見有關 響應式按鈕 的章節)
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 src="cui2d.js"></script>
<script>
function init() {
// get images
imageNormalAlien.src = "alien_sleepy.png";
imageNormalAlien.onload = cuiRepaint;
imageFocusedAlien.src = "alien_wow.png";
imageFocusedAlien.onload = cuiRepaint;
imageGrabbedOnceAlien.src = "alien_smiley.png";
imageGrabbedOnceAlien.onload = cuiRepaint;
// set defaults for all pages
cuiBackgroundFillStyle = "#000080";
// initialize cui2d and start with myPage
cuiInit(myPage);
}
// create images for the smiley
var imageNormalAlien = new Image();
var imageFocusedAlien = new Image();
var imageGrabbedOnceAlien = new Image();
// create draggable objects
var draggable0 = new cuiDraggable();
var draggable1 = new cuiDraggable();
// create a page
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 draggable
if (draggable0.process(event, 50, 50, 80, 80, null,
imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien,
cuiConstants.isDraggableWithOneFinger)) {
if (draggable0.translationY > 100) {
draggable0.translationY = 100; // don't move further
}
cuiRepaint();
return true;
}
if (draggable1.process(event, 150, 50, 80, 80, null,
imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien,
cuiConstants.isDraggableWithOneFinger)) {
draggable1.translationX = 0; // stay on line
if (draggable1.translationY < 0) {
draggable1.translationY = 0; // don't move beyond 0
}
else if (draggable1.translationY > 100) {
draggable1.translationY = 100; // don't move beyond 100
}
if (!draggable1.isPointerDown) { // no more dragging?
if (draggable1.translationY < 50) { // y coordinate < 50
draggable1.translationY = 0; // snap to 0
}
else {
draggable1.translationY = 100; // else snap to 100
}
}
cuiRepaint();
return true;
}
// repaint this page?
if (null == event) {
// background
cuiContext.fillStyle = "#A0A0A0";
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>
對兩個可拖動物件的全域性變數的定義非常簡單
// create draggable objects
var draggable0 = new cuiDraggable();
var draggable1 = new cuiDraggable();
頁面的過程函式處理這兩個物件。第一個物件由以下程式碼處理
if (draggable0.process(event, 50, 50, 80, 80, null,
imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien,
cuiConstants.isDraggableWithOneFinger)) {
if (draggable0.translationY > 100) {
draggable0.translationY = 100; // don't move further
}
cuiRepaint();
return true;
}
可拖動物件的過程函式需要事件、矩形的座標、一個文字字串以及三個與 響應式按鈕 相似的影像。此外,常量 cuiConstants.isDraggableWithOneFinger 指定使用一根手指進行拖動應該是可能的。如果它處理了事件,則返回 true。這裡我們的反應是檢查物件是否已從其原始位置向下平移(即移動)超過 100 畫素。在這種情況下,平移將設定為這 100 個畫素。因此,物件無法被拖動到這條線之外。
第二個可拖動物件由以下程式碼處理
if (draggable1.process(event, 150, 50, 80, 80, null,
imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien,
cuiConstants.isDraggableWithOneFinger)) {
draggable1.translationX = 0; // stay on line
if (draggable1.translationY < 0) {
draggable1.translationY = 0; // don't move beyond 0
}
else if (draggable1.translationY > 100) {
draggable1.translationY = 100; // don't move beyond 100
}
if (!draggable1.isPointerDown) { // no more dragging?
if (draggable1.translationY < 50) { // y coordinate < 50
draggable1.translationY = 0; // snap to 0
}
else {
draggable1.translationY = 100; // else snap to 100
}
}
cuiRepaint();
return true;
}
這裡,x 方向的平移 (translationX) 設定為 0;即物件不能水平移動。此外,y 方向的平移被限制在 0 到 100 個畫素的範圍內。最後,如果拖動已停止 (!draggable1.isPointerDown),則 y 方向的平移將設定為 0 或 100,即它會捕捉到其中一個點。
當然,還有更多對物件拖動做出反應的可能性;例如,檢測輕掃手勢。
可拖動物件的實現實際上與按鈕的實現更相似,而不是人們期望的。首先定義一個建構函式,以及一些用於確定物件是否以及如何被點選的方法
/**
* @class cuiDraggable
* @classdesc Draggables can be translated by dragging.
*
* @desc Create a new cuiDraggable.
*/
function cuiDraggable() {
/**
* Difference in x coordinate by which the centre of the draggable has been moved relative to its
* initial position (specified by x + 0.5 * width with the arguments of {@link cuiDraggable#process}).
* @member {number} cuiDraggable.translationX
*/
this.translationX = 0;
/**
* Difference in y coordinate by which the centre of the draggable has been moved relative to its
* initial position (specified by y + 0.5 * height with the arguments of {@link cuiDraggable#process}).
* @member {number} cuiDraggable.translationY
*/
this.translationY = 0;
/**
* Flag specifying whether a mouse button or finger is inside the object's rectangle.
* @member {boolean} cuiDraggable.isPointerInside
*/
this.isPointerInside = false;
/**
* Flag specifying whether a mouse button or finger is pushing the object or has been
* pushing the object and is still held down (but may have moved outside the object's
* rectangle).
* @member {boolean} cuiDraggable.isPointerDown
*/
this.isPointerDown = false;
this.hasTriggeredClick = false; // click event has been triggered?
this.hasTriggeredDoubleClick = false; // double click event has been triggered?
this.hasTriggeredHold = false; // hold event has been triggered?
this.timeDown = 0; // time in milliseconds after January 1, 1970 when the pointer went down
this.eventXDown = 0; // x coordinate of the event when the pointer went down
this.eventYDown = 0; // y coordinate of the event when the pointer went down
this.identifier = -1; // identifier of the touch point
this.translationXDown = 0; // value of translationX when the pointer went down
this.translationYDown = 0; // value of translationY when the pointer went down
}
/**
* Determine whether the draggable has just been clicked.
* @returns {boolean} True if the draggable has been clicked, false otherwise.
*/
cuiDraggable.prototype.isClicked = function() {
return this.hasTriggeredClick;
}
/**
* Determine whether the draggable has just been double clicked.
* @returns {boolean} True if the button has been double clicked, false otherwise.
*/
cuiDraggable.prototype.isDoubleClicked = function() {
return this.hasTriggeredDoubleClick;
}
/**
* Determine whether the pointer has just been held down longer than {@link cuiTimeUntilHold}.
* @returns {boolean} True if the pointer has just been held down long enough, false otherwise.
*/
cuiDraggable.prototype.isHeldDown = function() {
return this.hasTriggeredHold;
}
可拖動物件比按鈕擁有更多的成員,因為它們必須在拖動過程中跟蹤各種位置。
過程函式根據物件的狀態和當前事件來計算這些成員的變化
/**
* Either process the event (if event != null) and return true if the event has been processed,
* or draw the appropriate image for the object state in the rectangle
* with a text string on top of it (if event == null) and return false.
* This function is usually called by {@link cuiPage.process} of a {@link cuiPage}.
* @param {Object} event – An object describing a user event by its "type", coordinates in
* page coordinates ("eventX" and "eventY"), an "identifier" for touch events, and optionally
* "buttons" to specify which mouse buttons are depressed. If null, the function should
* redraw the object.
* @param {number} x – The x coordinate of the top, left corner of the object's rectangle.
* @param {number} y – The y coordinate of the top, left corner of the object's rectangle.
* @param {number} width – The width of the object's rectangle.
* @param {number} height – The height of the object's rectangle.
* @param {string} text – A text that is written at the center of the rectangle. (May be null).
* @param {Object} imageNormal – An image to be drawn inside the object's rectangle if there
* are no user interactions. (May be null.)
* @param {Object} imageFocused – An image to be drawn inside the object's rectangle if the
* mouse hovers over the object's rectangle or a touch point moves into it. (May be null.)
* @param {Object} imagePressed – An image to be drawn inside the object's rectangle if a
* mouse button is pushed or the object is touched. (May be null.)
* @param {number} interactionBits – The forms of interaction, either {@link cuiConstants.none} or a bitwise-or
* of other constants in {@link cuiConstants}, e.g.
* cuiConstants.isDraggableWithOneFinger | cuiConstants.isLimitedToHorizontalDragging.
* @returns {boolean} True if event != null and the event has been processed (implying that
* no other GUI elements should process it). False otherwise.
*/
cuiDraggable.prototype.process = function(event, x, y, width, height,
text, imageNormal, imageFocused, imagePressed, interactionBits) {
// choose appropriate image
var image = imageNormal;
if (this.isPointerDown) {
image = imagePressed;
}
else if (this.isPointerInside) {
image = imageFocused;
}
// check or repaint button
var isIn = cuiIsInsideRectangle(event, x + this.translationX, y + this.translationY,
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 trigger events (these are set only once after the event and have to be cleared afterwards)
this.hasTriggeredClick = false;
this.hasTriggeredDoubleClick = false;
this.hasTriggeredHold = false;
// process double click events
if ("dblclick" == event.type) {
this.hasTriggeredDoubleClick = isIn;
return isIn;
}
// process our hold events
if ("mousehold" == event.type) {
if (event.timeDown == this.timeDown && event.identifier == this.identifier &&
this.isPointerDown) {
this.hasTriggeredHold = true;
return true;
}
return false;
}
// process other events
if ("wheel" == event.type || "mousewheel" == event.type) {
return isIn; // give directly to caller
}
// 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) && this.identifier >= 0) {
return false; // ignore mouse (except mousedown) if we are tracking a touch point
}
}
// state changes
if (!this.isPointerInside && !this.isPointerDown) { // passive object 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
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.eventXDown = event.eventX;
this.eventYDown = event.eventY;
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 object 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
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.eventXDown = event.eventX;
this.eventYDown = event.eventY;
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.isPointerDown) { // grabbed object 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
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.eventXDown = event.eventX;
this.eventYDown = event.eventY;
cuiRepaint();
return true;
}
else if ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons)) {
this.isPointerDown = false;
this.isPointerInside = isIn;
this.identifier = -1; // mouse
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if ("touchend" == event.type) {
this.isPointerDown = false;
this.isPointerInside = false;
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if ("touchcancel" == event.type) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if ("touchmove" == event.type || ("mousemove" == event.type)) {
this.isPointerInside = isIn;
if (cuiConstants.isDraggableWithOneFinger & interactionBits) {
if (!(cuiConstants.isLimitedToVerticalDragging & interactionBits)) {
this.translationX = this.translationXDown + (event.eventX – this.eventXDown);
}
if (!(cuiConstants.isLimitedToHorizontalDragging & interactionBits)) {
this.translationY = this.translationYDown + (event.eventY – this.eventYDown);
}
}
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 {
return false; // none of our business
}
}
// unreachable code
return false;
}