Canvas 2D Web 应用程序/覆盖层
本章扩展了关于 页面 的章节,为这三个页面添加了一个半透明的覆盖层,并将按钮从页面移动到该覆盖层。
cui2d 中的覆盖层只是一个页面(即一个 cuiPage
),它用作另一个页面的覆盖层(即它的 processOverlay
方法在另一个页面的处理函数中被调用)。除此之外,它们只是普通的页面,具有自己的处理函数(由 processOverlay
调用);因此,覆盖层页面实际上可以像任何其他页面一样使用。
本章的示例(可在 线上 获取;也可以 下载版本)为关于 页面 的章节的示例添加了另一个(覆盖层)页面及其处理函数。然后从覆盖层页面的处理函数中调用两个按钮。本章重点介绍如何创建一个覆盖层页面以及其他页面如何包含覆盖层页面。有关其他部分,请参阅关于 页面 和 响应式按钮 的章节。
<!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 src="cui2d.js"></script>
<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;
// initialize and start cui2d
cuiInit(firstPage);
}
// overlay page
var imageNormalButton = new Image();
var imageFocusedButton = new Image();
var imagePressedButton = new Image();
var button0 = new cuiButton();
var button1 = new cuiButton();
var overlayPage = new cuiPage(600, 90, overlayPageProcess);
overlayPage.isAdjustingHeight = false; // only adjust width
overlayPage.verticalAlignment = -1; // top align
overlayPage.interactionBits = (cuiConstants.isDraggableWithOneFinger | cuiConstants.isLimitedToVerticalDragging);
function overlayPageProcess(event) {
cuiContext.fillStyle = "#FFFFFF"; // draw in white
if (button0.process(event, 20, 20, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
if (cuiCurrentPage == secondPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = firstPage;
}
else if (cuiCurrentPage == thirdPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
cuiCurrentPage = secondPage;
}
cuiRepaint();
}
return true;
}
if (button1.process(event, 150, 20, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
if (cuiCurrentPage == firstPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = secondPage;
}
else if (cuiCurrentPage == secondPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
cuiCurrentPage = thirdPage;
}
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.globalAlpha = 0.3;
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
// first page
var firstPage = new cuiPage(400, 300, firstPageProcess);
function firstPageProcess(event) {
if (overlayPage.processOverlay(event)) {
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("First page using landscape format.", 200, 150);
cuiContext.fillStyle = "#C0C0C0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
// second page
var secondPage = new cuiPage(400, 400, secondPageProcess);
function secondPageProcess(event) {
if (overlayPage.processOverlay(event)) {
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Second page using square format.", 200, 200);
cuiContext.fillStyle = "#DDD0C0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
// third page
var thirdPage = new cuiPage(400, 533, thirdPageProcess);
function thirdPageProcess(event) {
if (overlayPage.processOverlay(event)) {
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Third page using portrait format.", 200, 266);
cuiContext.fillStyle = "#DDC0D0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
</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>
应该用作覆盖层页面的页面与其他页面一样创建。但是,覆盖层通常不可变换。在本例中,覆盖层只能用一根手指垂直拖动。如果页面及其覆盖层都不可变换,则它们应该具有相同的尺寸并使用相同的布局。但是,如果页面可变换但覆盖层不可变换,事情就会变得更加复杂。通常,覆盖层将调整为屏幕的宽度或高度,并将与相应的边缘之一对齐。在本例中,宽度进行了调整,并且覆盖层的顶部边缘对齐
...
var overlayPage = new cuiPage(600, 90, overlayPageProcess);
overlayPage.isAdjustingHeight = false; // only adjust width
overlayPage.verticalAlignment = -1; // top align
overlayPage.interactionBits = (cuiConstants.isDraggableWithOneFinger | cuiConstants.isLimitedToVerticalDragging);
...
覆盖层页面的处理函数包含两个按钮(“上一个”和“下一个”)。由于相同的覆盖层用于所有三个页面,因此这些按钮也用于所有三个页面,因此必须检查 cuiCurrentPage
的当前值才能将其设置为正确的新值
...
function overlayPageProcess(event) {
cuiContext.fillStyle = "#FFFFFF"; // draw in white
if (button0.process(event, 20, 20, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
if (cuiCurrentPage == secondPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = firstPage;
}
else if (cuiCurrentPage == thirdPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
cuiCurrentPage = secondPage;
}
cuiRepaint();
}
return true;
}
if (button1.process(event, 150, 20, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
if (cuiCurrentPage == firstPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = secondPage;
}
else if (cuiCurrentPage == secondPage) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
cuiCurrentPage = thirdPage;
}
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.globalAlpha = 0.3;
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
...
为了将页面用作另一个页面的覆盖层,它的 processOverlay(event)
方法应该从该另一个页面的处理函数中调用。在本例中
...
// first page
var firstPage = new cuiPage(400, 300, firstPageProcess);
function firstPageProcess(event) {
if (overlayPage.processOverlay(event)) {
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("First page using landscape format.", 200, 150);
cuiContext.fillStyle = "#C0C0C0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
...
正如您可能已经预料到的那样,如果 processOverlay(event)
处理了事件,则它将返回 true
,否则返回 false
。
另外两个页面以完全相同的方式工作。
覆盖层的主要用途是始终停留在屏幕上相同位置的菜单、图标和按钮。但是,它们也可以用于可拖动(或可变换)的调色板和工具箱。此外,它们对对话框很有用。它们甚至可以用来实现一个简单的窗口系统(但是没有可滚动的内容,也没有改变“窗口”纵横比的可能性)。
由于它们用途广泛,因此应该问一下什么时候不使用它们?覆盖层与可拖动或可变换元素的主要区别在于覆盖层不受其页面的变换的影响。一个具体的例子可能会有所帮助:想象一个应用程序,它呈现一张可缩放的地图,用户可以点击地图上的某些点以获取包含有关这些点的更多信息的 infobox。这些 infobox 应该是覆盖层还是可变换元素?如果它们是覆盖层,用户可能会很快用 infobox 塞满整个屏幕。如果它们是可变换元素,那么当用户平移到地图的其他部分时,打开的 infobox 会移出视线。但是,如果用户缩小,可变换元素的内容很快就会变得不可读,而覆盖层会保持其可读的大小。另一方面,覆盖层的位置并没有告诉用户点击点在地图上的位置,而可变换元素会一直靠近点击点,除非用户将其拖走。但是,如果用户想比较两个相距很远的点的 infobox 内容呢?用覆盖层更容易做到这一点。
因此,与可变换元素和可拖动元素相比,覆盖层有多个优点和缺点,具体取决于具体的应用程序。因此,关于是否使用覆盖层的决定必须针对每个特定案例做出。
processOverlay
的实现相对简单。步骤如下
- 变换事件坐标,因为覆盖层通常使用与页面不同的变换(事件坐标已对其进行变换)。
- 设置
cuiContext
的几何变换(同样因为页面通常使用不同的变换)。 - 调用覆盖层页面的处理函数。
- 处理覆盖层页面可变换
view
成员未处理的任何事件。
在代码中
/**
* Either process the event (if event != null) and return true if the event has been processed,
* or draw the page as an overlay (to another page) in the rectangle, which is specified in
* window coordinates (if event == null) and return false. This function is usually called
* by {@link cuiPage.process} of another page.
* @param {Object} event - An object describing a user event by its "type", coordinates in
* window coordinates ("clientX" and "clientY"), an "identifier" for touch events, and optionally
* "buttons" to specify which mouse buttons are depressed. If null, the function should
* redraw the overlay page.
* @returns {boolean} True if event != null and the event has been processed (implying that
* no other GUI elements should process it). False otherwise.
*/
cuiPage.prototype.processOverlay = function(event) {
var orgEvent = event;
var transform = {scale : 1.0, x : 0.0, y : 0.0};
this.computeInitialTransformation(transform);
if (null != orgEvent) {
event = {clientX : orgEvent.clientX, clientY : orgEvent.clientY,
eventX : orgEvent.eventX, eventY : orgEvent.eventY,
type : orgEvent.type, buttons : orgEvent.buttons,
deltaY : orgEvent.deltaY, wheelDelta : orgEvent.wheelDelta};
this.computeEventCoordinates(event, transform); // set event coordinates for our transformation
}
if (null == orgEvent) {
cuiContext.save();
this.setPageTransformation(transform); // set our transformation in cuiContext
}
var flag = this.process(event); // call our process function
if (!flag && null != event && (this.interactionBits != cuiConstants.none)) {
// event hasn't been processed and we have an event?
event.eventX = event.clientX; // we don't need any transformation here because the initial ...
event.eventY = event.clientY; // ... transformation is applied to the arguments of ...
// ... view.process() and the transformation in view is applied internally in view.process()
var oldTranslationX = this.view.translationX;
var oldTranslationY = this.view.translationY;
var oldFlag = this.view.isProcessingOuterEvents;
this.view.isProcessingOuterEvents = false; // don't let a page process outer events if it is an overlay
if (this.view.process(event, transform.x, transform.y, this.width * transform.scale,
this.height * transform.scale,
null, null, null, null, null, this.interactionBits)) {
flag = true;
// enforce interaction constraints with page
if (cuiConstants.isLimitedToVerticalDragging & this.interactionBits) {
this.view.translationX = oldTranslationX;
}
if (cuiConstants.isLimitedToHorizontalDragging & this.interactionBits) {
this.view.translationY = oldTranslationY;
}
}
this.view.isProcessingOuterEvents = oldFlag; // restore page's setting
}
if (null == orgEvent) {
cuiContext.restore();
}
return flag;
}