跳至内容

Canvas 2D Web 应用程序/可拖动对象

来自 Wikibooks,开放世界中的开放书籍

本章介绍可拖动对象,即可以使用鼠标或单指触控手势拖动的图像。呈现方法的一个显著特点是,任何数量的对象都可以同时在多点触控设备上拖动。此外,应用程序程序员不必担心处理多个同时触控事件:过程函数只接收带有单个坐标对的事件,并且一次只处理一个对象。

本章的示例(可在 网上 获得;也可以作为 可下载版本 获得)展示了两个可拖动对象:一个不能向下拖动超过 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;
}


< Canvas 2D Web 应用程序

除非另有说明,否则此页面上的所有示例源代码均授予公有领域。
华夏公益教科书