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;
}