网站的内链青岛网站制作设计
原文:Beginning Windows Store Application Development – HTML and JavaScript Edition
协议:CC BY-NC-SA 4.0
八、WinJS 自定义控件
使用我在最后三章中提到的控件,你可以构建一个应用来满足几乎所有的需求,并且当我们深入研究这些控件和其他概念的更多细节时,你将会看到,这正是你从第九章开始要做的事情。也就是说,有时您可能会发现自己希望获得一些额外的控件,而定制控件可能正是您所需要的。
在这一章中,我将研究在你的应用中需要一些时钟功能的情况。虽然使用一些基本的 HTML 和 JavaScript 构建一个时钟并不困难,但是您虚构的应用需求表明您在整个应用的多个地方都有时钟。无论是现成的控件还是自定义控件,使用控件的最大好处之一就是它们很容易重用。如果生成一个表示时钟的控件,就可以多次使用它,就像可以向应用中添加多个文本输入框一样。
自定义控件
简而言之,控件可以被认为是用户界面的任何一部分,以某种方式捆绑在一起,允许你将它添加到你的应用中。至少就本章而言,自定义控件是指在开发 Windows 应用商店应用时没有提供的现成控件。通常,当开发人员提到自定义控件时,他们指的是使用这些控件的应用的开发人员编写的控件。这当然没有错,但该术语也可以指从第三方控件供应商处购买的控件。不缺少第三方控件,例如,可以用来显示图表或图形,您可以在您最喜欢的 web 搜索引擎的帮助下找到许多这样的控件。
也就是说,本章的重点不是那些第三方控件。相反,我将介绍构建您自己的控件的步骤,特别是 WinJS 控件,以满足您自己的特定需求。我将讨论构建自定义控件的两种常用方法。
- 我将用本章的大部分时间讨论如何用 JavaScript 构建自定义 WinJS 控件。
- 我将简要介绍如何使用 WinJS 提供的
PageControl
构建定制的 WinJS 控件,您已经在前几章中看到了。
您当然可以不使用 WinJS 库来创建自定义控件。如果您是一名经验丰富的 JavaScript 开发人员,这是一个完全合理的选择,尽管这超出了本书的范围。因为 Windows Store 应用是用 HTML 和 JavaScript 构建的,所以您可以在应用中使用许多流行的 JavaScript 库和几乎所有常见的 JavaScript 技术。但是,使用 WinJS 的功能来构建自定义控件也有好处。WinJS 库提供了一种模式,可以帮助您维护代码的一致性并避免与 JavaScript 语言相关的常见缺陷,从而使您能够将更多的精力放在业务需求上,而不是控制构建的内部工作。像任何软件抽象一样,它并不总是正确的选择,但我发现几乎总是,我更喜欢构建自定义 WinJS 控件,而不是纯粹用 JavaScript 构建控件。
用 JavaScript 定制 WinJS 控件
正如我上面提到的,我们将用本章的大部分时间用 JavaScript 和 WinJS 库构建一个自定义时钟控件。让我们首先确定这种控制的一些要求。
- 时钟可以以 12 小时或 24 小时格式显示当前本地时间。
- 当用于显示当前时间时,用户可以选择显示或隐藏秒。
- 该时钟可以用作倒计时或“向上计数”计时器,以测量经过的时间。
- 当用作倒计时或递增计时时,可以指定初始时间。
- 当用作倒计时或计数计时器时,计时器可以在创建时自动启动。
- 时钟可以通过编程启动、停止或复位。
- 包含时钟控件的页面可以通过事件得到通知,例如时钟开始、停止或重置的时间,以及倒计时结束的时间。
注本书附带的源代码包括一个名为 WinJSControlsSample 的完整项目,其中包含了在第五章、第六章、第七章和第八章中使用的样本代码。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
在前几章中,您将在现有的 WinJSControlsSample 项目中构建此功能。在我进入构建这个控件的细节之前,您必须做一些项目设置。在 Visual Studio 中打开项目后,按照第五章中的步骤添加一个名为customcontrols.html
的页面控件。确保将所有的示例代码放在页面控件的<section aria-label="Main content" role="main">
和</section>
元素之间。您还必须为这个页面控件添加一个导航按钮到home.html
。
接下来,添加一个名为controls
的新文件夹到你的项目的根目录,并在其中添加另一个名为js
的文件夹(见图 8-1 )。这是您将要创建自定义控件的地方,所以右键单击该文件夹,并选择向项目添加新项的选项。添加一个名为clockControl.js
的新 JavaScript 文件(参见图 8-2 )。
图 8-1 。JavaScript 控件的主页
图 8-2 。在项目中创建 JavaScript 控件
JavaScript 自定义 WinJS 控件的剖析
WinJS JavaScript 控件只需创建一个新的类,遵循一些特定的约定。虽然面向对象的开发任务,比如创建类,可以用 JavaScript 来完成,但是与使用许多其他语言(比如 C#)相比,这样做要冗长得多。对面向对象概念的全面介绍,更不用说 JavaScript 了,已经超出了本书的范围。(如果你想探索这个话题,请参阅罗斯·哈梅斯和达斯汀·迪亚兹的《Pro JavaScript 设计模式》。)幸运的是,WinJS 抽象掉了这种冗长的麻烦,允许您以更简单的方式创建类和控件。清单 8-1 展示了一个简单的模板,当你在应用中定义控件时,你可以使用它。
清单 8-1。 定义控件的模板
(function () {"use strict";var controlDefinition = WinJS.Class.define(function Control_ctor(element, options) {this.element = element || document.createElement("div");this.element.winControl = this;// control initialization},{// instance members},{// static members});WinJS.Namespace.define("WinJSControlsSample.UI", {HelloWorld: controlDefinition});
})();
如果你有相当多的 JavaScript 经验,你可能会自己弄清楚清单 8-1 中的代码在做什么,如果是这样的话,我邀请你跳到下一节。但是,如果您是 JavaScript 新手,或者接触的内容有限,我将逐步介绍这段代码,让您大致了解一下发生了什么。让我们从顶部开始……从底部开始。
第一行(function () {
和最后一行})();
构成了通常所说的自执行功能。如果这对你来说看起来有点奇怪,你并不孤单。第一次看的时候,我也有点不知所措。用技术术语来说,这段代码定义了一个匿名 JavaScript 函数,并立即执行它。从最后一行的末尾开始,向后看,你会看到左括号和右括号,后面跟着一个分号,就像你在一行代码的末尾看到你调用了你写的函数一样。这就是“自执行功能”的“执行”部分那么,正在执行的功能是什么?在右括号后面再后退一个字符。这与清单 8-1 第一行的第一个左括号匹配。这两个括号之间的所有内容都是匿名函数,正如它听起来的那样,是一个没有名字或不需要名字的函数。一旦定义了这个函数,这段代码就会立即执行它。在清单 8-2 和清单 8-3 中,你会看到两种手段殊途同归。这两个代码块做同样的事情,并向用户显示一个对话框。
清单 8-2。 示例函数定义和函数调用
function myFunction() {alert("foo");
}myFunction();
清单 8-3。 样本自动执行功能
(function() {alert("foo");
})();
我们为什么要这么做?有几个原因,网上有很多关于这个主题的详细文章可以阅读,但简单地说,我们这样做是为了界定范围。函数内定义的变量对该函数外的任何代码都不可用。这意味着我们可以安全地创建自己的成员——变量、属性和函数——而不用担心与另一个同名成员冲突。在清单 8-1 中,名为controlDefinition
的变量不会对我们应用中名为controlDefinition
的任何其他变量产生任何影响,也不会受其影响。通过以这种方式保护我们的函数成员,我们可以有选择地以一种有意义的方式公开它们。如果您熟悉 C#之类的语言,这是一个类似于定义私有变量和函数的概念,这些变量和函数在定义它们的类之外不可用,并且提供公共成员,以便其他代码仍然可以以有意义的方式使用该类。你会在整本书中看到更多这样的内容。如果现在有点雾,不用担心。等你看了几遍,就更清楚了。现在,重要的一点是记住在自执行函数中定义控件。
注对于自执行函数应该叫什么,不同的人有不同的看法。其他常见的名字还有立即调用函数表达式(IIFE)、立即执行函数、或自执行匿名函数。只要你知道不同的名字,并且理解这个概念,任何名字都可以。为了保持一致,在本书的其余部分,我将把它们称为自执行函数*。*
清单 8-1 中代码的下一行是字符串文字"use strict";
。这声明该范围内的代码处于严格模式,这允许更好的错误检查。在严格模式下,某些做法是不允许的,通常是那些可能导致歧义或其他意外行为的做法。这使您能够更快地发现代码中的潜在错误,而不是部署一个在测试中似乎可以工作的应用,结果却导致问题。它有助于减少“它在我的机器上工作”的错误。
到目前为止,在本节中,我已经讨论了自执行函数和"use strict"
指令。这两个概念都是常见的现代 JavaScript 开发实践,关于它们的文章层出不穷。在清单 8-1 中,接下来是 WinJS 开发特有的东西。我们使用WinJS.Class.define
方法定义一个类,并将该类定义赋给一个名为controlDefinition
的私有变量。
注意如果你的背景是 C#这样的静态类型语言,那么给一个变量赋值函数和类定义可能看起来很奇怪,尽管在 C#中类似的模式越来越常见。这里需要注意的是,JavaScript 中的类和函数与任何其他值一样,可以作为变量值赋值。描述这一点的常用短语是说函数是 JavaScript 中的一级对象。
用三个参数调用WinJS.Class.define
方法。第一个是构造函数,在这里命名为Control_ctor
。每次创建该类的新实例时,都会调用该函数。定义控件时,此构造函数需要两个参数。我们的例子,以及通常的约定,将这些参数命名为element
和options
。第一个是对用于将控件放置在页面上的 HTML 元素的引用。正如你在第六章中看到的,第二个用于在你的 HTML 标记中使用data-win-options
属性为你的控件提供特定的选项。
第二个参数是描述类的实例成员的对象,第三个参数是定义类的静态成员的对象。示例场景可能是描述实例和静态成员的最简单的方法。
假设您正在创建一个Person
类。在这种情况下,您可能有一个名为firstName
的实例属性。它被称为实例属性,因为firstName
是描述一个Person
的单个实例的东西。您的Person
类也可能有一个名为search
的静态方法,您可以调用它来查找符合某些标准的单个Person
或一组Person
对象。清单 8-4 展示了一个如何引用这些属性的例子。注意,firstName
属性需要一个Person
类的实例,对于这个类,search
函数可以直接从Person
类获得,而不需要该类的任何特定实例。随着本章的进行,您将看到如何为实例成员构造传递给WinJS.Class.define
方法的对象的例子。我们在本章中的例子不需要任何静态成员,但是指定它们是以同样的方式完成的。
清单 8-4。 你的虚构人物类的属性和方法
var myPerson = new Person();
myPerson.firstName = "Scott";var searchResults = Person.search("Scott");
清单 8-1 中剩余的代码用于定义一个名称空间,其中包括你的类定义。这就是如何向应用中需要引用它的其他代码公开私有的controlDefinition
类。您的代码调用了带有两个参数的WinJS.Namespace.define
方法。第一个是名称空间本身的名称,它用于对相关功能进行分组并防止命名冲突(两个类可以各有一个同名的属性)。第二个参数是一个对象,描述您向其他代码公开的不同内容。在这种情况下,您将您的私有controlDefinition
类暴露给应用的其余部分,并在WinJSControlsSample.UI
名称空间中给它一个公共名称HelloWorld
。这样,您的基本控件定义就完成了,您可以在应用的其他地方使用它,要么以声明方式将控件添加到您的 HTM 页面(您将在下一节看到),要么在 JavaScript 中使用类似于清单 8-5 的代码。
清单 8-5。 创建你的控件类的实例
var myControl = new WinJSControlsSample.UI.HelloWorld();
版本 1:一个简单的 12 小时时钟
好了,你已经在前面的部分制作了一个控件,但是它实际上并不做任何事情。让我们在这个例子的基础上创建一些真正的功能。打开clockControl.js
JavaScript 文件,添加来自清单 8-6 的代码。你会认出我在清单 8-1 中描述的模式,有一些不同。首先,我没有将你的类公开为HelloWorld
,而是将名字改为Clock,
来表示这个控件将提供的功能。其次,也是最值得注意的,我已经为WinJS.Class.define
的实例成员参数提供了相当多的定义。
清单 8-6。 你的第一个“真实”自定义控件
(function () {"use strict";var controlDefinition = WinJS.Class.define(function Control_ctor(element, options) {this.element = element || document.createElement("div");this.element.winControl = this;this._init();},{// instance members_init: function () {this.start();},start: function () {setInterval(this._refreshTime.bind(this), 500);},_refreshTime: function () {var dt = new Date();var hr = dt.getHours();var min = dt.getMinutes();var sec = dt.getSeconds();var ampm = (hr >= 12) ? " PM" : " AM";hr = hr % 12;hr = (hr === 0) ? 12 : hr;min = ((min < 10) ? "0" : "") + min;sec = ((sec < 10) ? "0" : "") + sec;var formattedTime = new String();formattedTime = hr + ":" + min + ":" + sec + ampm;this.element.textContent = formattedTime;},});WinJS.Namespace.define("WinJSControlsSample.UI", {Clock: controlDefinition,});})();
这个实例成员对象是使用 JavaScript 对象表示法(JSON)语法定义的。JSON 允许您直接指定一个对象,就地定义它,而不是创建一个类定义,然后设置许多属性。它也是一种广泛使用的格式,用于在应用之间传输数据,因为它很容易与任何编程语言可读的文本表示进行相互转换。在 JSON 中,对象的成员用逗号分隔,每个成员都用模式memberName: memberDefinition
定义。您可以在我们的示例中看到这一点,其中您的类的实例成员的对象有三个自己的成员:_init
函数、start
函数和_refreshTime
函数。
注意使用 WinJS 实用程序定义类时使用的惯例是,私有成员(仅在类本身中可用的变量和函数)的名称以下划线字符(_)开头,而公共成员(使用类的代码中可用的变量和函数)的名称以字母开头。
我作为构造函数的最后一行调用的_init
函数非常简单:它只是调用了start
函数。反过来,start
函数创建一个间隔,这是一个 JavaScript 特性,允许代码在一定时间后重复执行,在本例中是 500 毫秒。_refreshTime
函数获取当前时间,对其进行格式化,然后通过设置用于将该控件添加到页面的 HTML 元素的textElement
属性,在页面上显示该时间。
说到那个元素,是时候看看怎么做这个了。第一步是将自定义控件 JavaScript 文件的引用添加到 HTML 页面中。将清单 8-7 中的代码添加到customcontrols.html
的head
部分的末尾。然后将清单 8-8 中的代码添加到主部分。
清单 8-7。 给 HTML 文件添加脚本引用
<head><!-- SNIPPED --><script src="/controls/js/clockControl.js"></script>
</head>
清单 8-8。 添加您的自定义控件
<div id="myClock12" data-win-control="WinJSControlsSample.UI.Clock"></div>
如果你仔细看看你刚刚在customcontrols.html
中做了什么,你会发现,一旦定义了一个自定义控件,将它添加到页面就像添加你在第六章和第七章中看到的 WinJS 控件一样。添加一个对定义控件的脚本文件的引用——本例中为clockControl.js
,前面章节中为base.js
或ui.js
——然后添加一个div
元素,并将data-win-control
属性设置为控件的全名。当您运行应用时,您应该会看到一个显示当前本地时间的时钟(参见图 8-3 ),并且它会随着时间的流逝而更新。
图 8-3 。自定义时钟控件
版本 2:增加 24 小时时钟 选项
到目前为止,您已经完成了我在本章开始时为您的自定义控件定义的需求的一半。您制作了一个时钟,它以 12 小时制显示本地时间,但不是 24 小时制。在本节中,您将完成该需求的其余部分,并添加可选地显示或隐藏当前时间的秒数的功能。
让我们从描述你的时钟控制可以使用什么模式开始。在清单 8-9 中高亮显示的代码定义了一个包含两种不同时钟模式的变量,这是你当前开发任务的一部分。您将使用Object.freeze
JavaScript 函数来防止clockModes
变量被更改。这与在名称空间中公开clockModes
一起,有效地允许您使用WinJSControlsSample.UI.ClockModes
作为值的枚举。这将允许您以后通过变量名来指定模式,而不是使用数字或字符串,这样更容易被开发人员在将控件添加到页面时键入错误。
清单 8-9。 定义不同时钟模式的选项
(function () {"use strict";var controlDefinition = WinJS.Class.define(// SNIPPED);// clockModes is an enum(eration) of the different ways our clock control can behavevar clockModes = Object.freeze({CurrentTime12: "currenttime12",CurrentTime24: "currenttime24",});WinJS.Namespace.define("WinJSControlsSample.UI", {Clock: controlDefinition,ClockModes: clockModes,});})();
既然您已经定义了可供时钟选择的不同模式,那么您需要一种方法来设置您的时钟的每个实例所需的单独模式。现在,让我们添加一种方法来显示或隐藏当前时间的秒数。这样做有三个步骤,如清单 8-10 中的所示。
- 为 模式和 showClockSeconds 添加实例属性定义:在我们的示例中,
mode
属性同时定义了get
和set
函数,因此它对于使用控件的代码是可读和可写的。然而,showClockSeconds
属性只是可写的,因为它只定义了一个set
函数。在本章的后面,您将创建一个只有get
函数的属性,使它成为一个只读属性。 - *在我们的构造函数中为这些属性设置默认值:*默认情况下,您的时钟将处于 12 小时模式,并将显示当前时间的秒数。
- 调用
WinJS.UI.setOptions
方法,该方法将options
传递给我们的构造函数,并设置mode, showClockSeconds
*和任何其他属性:*您将在本节的后面看到这是如何工作的。
清单 8-10。 给你的时钟控件添加一个模式属性
var controlDefinition = WinJS.Class.define(function Control_ctor(element, options) {this.element = element || document.createElement("div");this.element.winControl = this;// Set option defaultsthis._mode = clockModes.CurrentTime12;this._showClockSeconds = true;// Set user-defined optionsWinJS.UI.setOptions(this, options);this._init();},{// instance membersmode: {get: function () {return this._mode;},set: function (value) {this._mode = value;}},showClockSeconds: {set: function (value) {this._showClockSeconds = value;}},_init: function () {this.start();},// SNIPPED}
);
现在让我们向您的页面添加另一个时钟控件。将清单 8-11 中的代码添加到customcontrols.html
中。请注意,我将模式设置为显示当前时间,24 小时格式,并且我已经决定隐藏该时钟的秒数。
清单 8-11。 添加 24 小时时钟
<div id="myClock24"data-win-control="WinJSControlsSample.UI.Clock"data-win-options="{mode: WinJSControlsSample.UI.ClockModes.CurrentTime24,showClockSeconds: false}">
</div>
继续运行应用。不是你所期望的?到目前为止,您已经描述了时钟可以使用的模式,并且在新的时钟控件上设置了属性,以选择 24 小时时钟模式并隐藏秒。然而,您还没有更改实际呈现时间的代码。用清单 8-12 中的高亮代码更新_refreshTime
函数后,再次运行应用。这一次,你应该看到一个新的 24 小时时钟,没有秒(见图 8-4 )。尽管我没有为您最初的时钟控制指定mode
或showClockSeconds
的值,但它仍然表现相同,因为我在清单 8-10 中的构造函数中设置了默认值。
清单 8-12 。 根据属性值渲染不同的时钟
_refreshTime: function () {var dt = new Date();var hr = dt.getHours();var min = dt.getMinutes();var sec = dt.getSeconds();var ampm = (hr >= 12) ? " PM" : " AM";if (this._mode === clockModes.CurrentTime12) {hr = hr % 12;hr = (hr === 0) ? 12 : hr;} else {ampm = "";}min = ((min < 10) ? "0" : "") + min;sec = ((sec < 10) ? "0" : "") + sec;var formattedTime = new String();formattedTime = hr + ":" + min+ ((this._showClockSeconds) ? ":" + sec : "") + ampm;this.element.textContent = formattedTime;
},
图 8-4 。自定义时钟控件的两个实例
版本 3:增加定时器选项和引发事件
完成了两个要求,还有五个。你有一个完美的工作控件来显示当前时间。剩下的需求将让你选择使用这个控件作为一个定时器。对于本教程来说,更重要的是,您将添加对控件上调用方法的支持,并从控件中引发事件,这些事件可以在显示控件的页面上处理。
让我们首先将清单 8-13 中的代码添加到clockControl.js
中。这个清单包括该文件的完整源代码,突出显示了不同的部分。我将在本节的剩余部分介绍这些变化。它有几页长,但我建议你在继续之前通读一遍。当您通读它时,您会注意到向您的控件添加新模式的更改,以允许它用作计时器,并提供为计时器设置初始值的能力。此外,您将看到新的代码,它将使您能够启动、停止和重置计时器,并根据控件的状态引发事件。
***清单 8-13。***clock control . js 的完整来源
(function () {"use strict";var controlDefinition = WinJS.Class.define(function Control_ctor(element, options) {this.element = element || document.createElement("div");this.element.winControl = this;// Set option defaultsthis._mode = clockModes.CurrentTime12;this._showClockSeconds = true;this._initialCounterValue = [0, 0, 0];this._autoStartCounter = false;// Set user-defined optionsWinJS.UI.setOptions(this, options);this._init();},{// instance members_intervalId: 0,_counterValue: 0,isRunning: {get: function () {return (this._intervalId != 0);}},mode: {get: function () {return this._mode;},set: function (value) {this._mode = value;}},autoStartCounter: {get: function () {return this._autoStartCounter;},set: function (value) {this._autoStartCounter = value;}},initialCounterValue: {set: function (value) {if (isNaN(value)) {// if not a number, value is an array of hours minutes and secondsthis._counterValue = (value[0] * 3600) + (value[1] * 60) + (value[2]);this._initialCounterValue = value;} else {this._counterValue = value;this._initialCounterValue = [0, 0, value];}}},showClockSeconds: {set: function (value) {this._showClockSeconds = value;}},_init: function () {if (this._mode === clockModes.CurrentTime12|| this._mode === clockModes.CurrentTime24) {this.start();} else {this._updateCounter();if (this._autoStartCounter) {this.start();}}},start: function () {if (!this.isRunning) {if (this._mode === clockModes.CurrentTime12|| this._mode === clockModes.CurrentTime24) {this._intervalId =setInterval(this._refreshTime.bind(this), 500);} else {this._intervalId =setInterval(this._refreshCounterValue.bind(this), 1000);}this.dispatchEvent("start", {});}},stop: function () {if (this.isRunning) {clearInterval(this._intervalId);this._intervalId = 0;this.dispatchEvent("stop", {});}},reset: function () {this.initialCounterValue = this._initialCounterValue;this._updateCounter();this.dispatchEvent("reset", {});},_refreshTime: function () {var dt = new Date();var hr = dt.getHours();var min = dt.getMinutes();var sec = dt.getSeconds();var ampm = (hr >= 12) ? " PM" : " AM";if (this._mode === clockModes.CurrentTime12) {hr = hr % 12;hr = (hr === 0) ? 12 : hr;} else {ampm = "";}min = ((min < 10) ? "0" : "") + min;sec = ((sec < 10) ? "0" : "") + sec;var formattedTime = new String();formattedTime = hr + ":" + min+ ((this._showClockSeconds) ? ":" + sec : "") + ampm;this.element.textContent = formattedTime;},_refreshCounterValue: function () {if (this._mode === clockModes.CountDown) {this._counterValue--;if (this._counterValue <= 0) {this._counterValue = 0;this.stop();this.dispatchEvent("countdownComplete", {});}} else {this._counterValue++;}this._updateCounter();this.dispatchEvent("counterTick", {value: this._counterValue});},_updateCounter: function () {var sec = this._counterValue % 60;var min = ((this._counterValue - sec) / 60) % 60;var hr = ((this._counterValue - sec - (60 * min)) / 3600);min = ((min < 10) ? "0" : "") + min;sec = ((sec < 10) ? "0" : "") + sec;var formattedTime = new String();formattedTime = hr + ":" + min + ":" + sec;this.element.textContent = formattedTime;},});// clockModes is an enum(eration) of the different ways our clock control can behavevar clockModes = Object.freeze({CurrentTime12: "currenttime12",CurrentTime24: "currenttime24",CountDown: "countdown",CountUp: "countup",});WinJS.Namespace.define("WinJSControlsSample.UI", {Clock: controlDefinition,ClockModes: clockModes,});WinJS.Class.mix(WinJSControlsSample.UI.Clock,WinJS.Utilities.createEventProperties("counterTick"),WinJS.Utilities.createEventProperties("countdownComplete"),WinJS.Utilities.createEventProperties("start"),WinJS.Utilities.createEventProperties("stop"),WinJS.Utilities.createEventProperties("reset"),WinJS.UI.DOMEventMixin);})();
你都明白了吗?从技术的角度来看,清单 8-13 的大部分内容与上一节相似,设置默认值并添加实例成员。让我们在这里快速浏览一下这些变化。
- 我们在构造函数中为两个私有属性
_initialCounterValue
和_autoStartCounter
设置默认值。然后我们将这些作为实例属性公开,分别命名为initialCounterValue
和autoStartCounter
。 - 在我们对
initialCounterValue
的定义中,我们利用了 JavaScript 是一种动态类型语言这一事实,允许将值设置为整数秒或表示小时、分钟和秒的三个数字的数组。 - 我们添加了一个名为
_counterValue
的私有属性来跟踪计数器的当前值,以秒为单位。一个名为reset
的新方法被用来(出人意料地)将_counterValue
重置为_initialCounterValue
。 - 我们添加了名为
_intervalId
的属性来跟踪让计数器滴答作响的 JavaScript 间隔。根据是否设置了_intervalId
属性,isRunning
属性指示计数器当前是否正在运行。属性在我们修改后的start
方法中被设置,并在新的stop
方法中被清除。 - 我们修改了
start
和_init
方法,使得当控件处于我们现有的时钟模式之一时,它们的行为不变,但是当控件处于新添加的计数器模式之一CountDown
和CountUp
时,它们的行为会有所不同。 - 我们添加了一个新的
_updateCounter
方法来格式化和显示计数器的当前值。同样,每隔 1000 毫秒,start
函数调用_refreshCounterValue
方法来递增或递减计数器值。
虽然所有这些更改确实为您的控件添加了相当多的新功能,但实现它们的过程与我在上一节中讨论的过程非常相似。然而,在清单 8-13 中有一些新概念,我想更详细地介绍一下。在文件末尾,我定义了五个新的事件混合,并用WinJS.Class.mix
方法将它们附加或合并到您的控制中:counterTick
、countdownComplete
、start
、stop,
和reset
。清单 8-14 是一个例子,摘录自清单 8-13 中的,它创建事件并将它们附加到你的控件中。WinJS.Class.mix
方法将您的控件作为第一个参数,后面是我们想要附加的 mixin 对象列表。
清单 8-14。 定义一个事件,摘自清单 8-13
WinJS.Class.mix(WinJSControlsSample.UI.Clock,WinJS.Utilities.createEventProperties("counterTick"),WinJS.Utilities.createEventProperties("countdownComplete"),WinJS.Utilities.createEventProperties("start"),WinJS.Utilities.createEventProperties("stop"),WinJS.Utilities.createEventProperties("reset"),WinJS.UI.DOMEventMixin);
注意mixin 是定义可重用功能的对象,在这里,定义事件。然后,可以将这些 mixins 附加到其他类,以添加这种可重用的功能。如果您熟悉面向对象编程,这有点类似于在您的类中实现接口。主要区别在于 mixin 包括可重用功能的实现,而接口描述了您的类必须实现的功能。
这些事件使您能够在类中发生某些事情时通知调用代码。然而,简单地定义这些事件没有任何作用。您必须决定在您的控制范围内何处引发这些事件。理论上,您可以在_init
方法中引发countdownComplete
事件,但是,当然,这没有意义。相反,你使用从WinJS.UI.DOMEventMixin
mixin 添加到你的类中的dispatchEvent
方法来引发start
方法中的start
事件(参见清单 8-15 )和stop
方法中的stop
事件。
清单 8-15。 引发开始事件
this.dispatchEvent("start", {});
在_refreshCounterValue
方法中引发了countdownComplete
事件和counterTick
事件。请特别关注一下counterTick
。对dispatchEvent
的调用带有两个参数:要引发的事件的名称和要传递给事件处理程序的特定于事件的数据参数的对象。对于所有其他事件,没有任何数据传递给事件处理程序,但是在counterTick
的情况下,事件处理程序知道私有_counterValue
属性的当前值可能是有用的(参见清单 8-16 )。在本节的后面,您将看到如何利用这个论点。
清单 8-16。 用数据参数引发 counterTick 事件
this.dispatchEvent("counterTick", {value: this._counterValue
});
随着您的控制最终完成,让我们看看如何利用这些新功能。将清单 8-17 和清单 8-18 中的代码添加到customcontrols.html
中。请注意,在清单 8-17 中,计数器将从 10 秒的初始值开始倒数,而在清单 8-18 中,计数器将自动开始向上计数,测量经过的时间,从 10 小时 59 分 50 秒开始。运行应用,看看发生了什么(参见图 8-5 )。
清单 8-17。 增加一个“倒计时”计时器
<div id="myCountDown"data-win-control="WinJSControlsSample.UI.Clock"data-win-options="{mode: WinJSControlsSample.UI.ClockModes.CountDown,initialCounterValue: 10 }">
</div>
<button id="downStart">Start</button>
<button id="downStop" disabled>Stop</button>
<button id="downReset" disabled>Reset</button>
<span id="downEventStatus"></span>
清单 8-18。 增加一个“倒计时”计时器
<div id="myCountUp"data-win-control="WinJSControlsSample.UI.Clock"data-win-options="{mode: WinJSControlsSample.UI.ClockModes.CountUp,initialCounterValue: [10, 59, 50],autoStartCounter: true }">
</div>
<div id="upEventStatus">10 second ticks: </div>
图 8-5 。增加了计时器
此时,您的 12 小时时钟、24 小时时钟和“countup”计时器都在更新。倒计时器不算了,因为还没开始。你有按钮,但是它们还不做任何事情,所以让我们将清单 8-19 中突出显示的代码添加到customcontrols.js
中的ready
函数。因为myCountDown
是我们在清单 8-17 中添加的div
元素的 id,我们使用winControl
属性来引用该元素表示的控件。有了那个引用,我们可以调用你的控件的start
、stop,
和reset
方法。
清单 8-19。 添加按钮点击处理程序
ready: function (element, options) {downStart.addEventListener("click", function (e) {myCountDown.winControl.start();}, false);downStop.addEventListener("click", function (e) {myCountDown.winControl.stop();}, false);downReset.addEventListener("click", function (e) {myCountDown.winControl.reset();downReset.disabled=true;}, false);
},
既然您可以控制“倒计时”计时器,那么我要介绍的最后一件事就是处理您的控件引发的事件。你会注意到在清单 8-17 和清单 8-18 中占位符分别被命名为downEventStatus
和upEventStatus
。您将使用它们来显示控件引发的每个事件的结果。检查清单 8-20 中更新的ready
函数,你会注意到的第一个变化是一个名为handleCountDownEvent
的函数。在一个真实的应用中,你可以用自己的逻辑来处理每一个事件,但是我现在想保持事情简单。因此,handleCountDownEvent
函数将事件的名称作为参数,并将其显示在downEventStatus
占位符中,然后根据正在处理的事件切换按钮的状态。在myCountDown
上处理的四个事件中的每一个都简单地调用handleCountDownEvent
,将事件类型作为参数传入。最后,当我们处理myCountUp
控件的counterTick
事件时,我们检查我们在清单 8-16 中设置的值数据参数,并且每当另一个 10 秒过去时,向upEventStatus
占位符添加一个“tick”字符。
清单 8-20 。?? 为你的定时器事件添加事件处理程序
ready: function (element, options) {downStart.addEventListener("click", function (e) {myCountDown.winControl.start()}, false);downStop.addEventListener("click", function (e) {myCountDown.winControl.stop()}, false);downReset.addEventListener("click", function (e) {myCountDown.winControl.reset()}, false);var handleCountDownEvent = function (eventName) {downEventStatus.textContent = eventName;var enableStart = (eventName === "start") ? false : true;downStart.disabled = !enableStart;downStop.disabled = enableStart;downReset.disabled = !enableStart;};myCountDown.addEventListener("countdownComplete", function (e) {handleCountDownEvent(e.type);}, false);myCountDown.addEventListener("start", function (e) {handleCountDownEvent(e.type);}, false);myCountDown.addEventListener("stop", function (e) {handleCountDownEvent(e.type);}, false);myCountDown.addEventListener("reset", function (e) {handleCountDownEvent(e.type);}, false);myCountUp.addEventListener("counterTick", function (e) {if (e.value % 10 === 0) upEventStatus.textContent += "'";}, false);
},
至此,您的Clock
控件的所有四个实例现在都完全正常了。您可以为每个控件设置属性、调用方法和处理事件。再次运行应用,看看它们是如何组合在一起的(见图 8-6 )。
图 8-6 。您的全功能时钟控制
使用 PageControl 自定义 WinJS 控件
之前用 JavaScript 创建 WinJS 控件的方法可能非常强大。事实上,它本质上是用来创建我在第六章中讨论的所有控件的过程。然而,正如我在本章开始时提到的,在 JavaScript 中还有其他创建自定义控件的方法。我将在这里简要介绍的另一种方法是使用PageControl
创建控件。在前几章中,您已经使用了PageControl
向您的应用添加新的屏幕,但是在这里,您将看到一个如何创建一个简单的Contact
控件的示例,您可以在您的应用的页面上显示这个控件,将一个PageControl
嵌套在另一个PageControl
中。我们的Contact
控制不了多少。这个版本的控件的唯一要求是显示一个人的名字、姓氏和生日,但这可以扩展到包括更多关于这个人的数据,甚至包括编辑一个人的联系信息的控件。
您将按照前几章中使用的相同步骤添加一个PageControl
,但在此之前,让我们为控件创建一个更符合逻辑的 home。实际上,您几乎可以在 Visual Studio 项目中的任何位置添加该控件,但是像任何项目一样,将文件组织到适当的文件夹中有助于保持有序,尤其是当您的项目变大并且有许多文件时。让我们从在现有的controls
文件夹中创建一个名为pages
的文件夹开始。然后,在该文件夹中,创建一个contactControl
文件夹,并在该文件夹中添加一个名为contactControl.html
的新PageControl
。当你完成后,你的解决方案资源管理器应该看起来像图 8-7 。现在,用清单 8-21 中的代码替换contactControl.js
的内容。
清单 8-21。 新页面控件的代码隐藏文件
(function () {"use strict";var controlDefinition = WinJS.UI.Pages.define("/controls/pages/contactControl/contactControl.html",{// This function is called whenever a user navigates to this page. It// populates the page elements with the app's data.ready: function (element, options) {options = options || {};this._first = "";this._last = "";this._birthday = "";// Set user-defined optionsWinJS.UI.setOptions(this, options);firstContent.textContent = this.first;lastContent.textContent = this.last;birthdayContent.textContent = this.birthday;},first: {get: function () { return this._first; },set: function (value) {this._first = value;}},last: {get: function () { return this._last; },set: function (value) { this._last = value; }},birthday: {get: function () { return this._birthday; },set: function (value) { this._birthday = value; }},unload: function () {// TODO: Respond to navigations away from this page.},updateLayout: function (element, viewState, lastViewState) {/// <param name="element" domElement="true" />// TODO: Respond to changes in viewState.}});WinJS.Namespace.define("WinJSControlsSample.UI", {Contact: controlDefinition,});
})();
图 8-7 。您的新页面控件
浏览这个文件,你会发现它与我们在清单 8-13 中创建的 JavaScript 控件有些不同,但是有一些相似的概念。要注意的第一个区别是,我们不是定义一个类,而是使用WinJS.UI.Pages.define
方法来定义一个PageControl
。然而,我们仍然将该控件公开作为我们的WinJSControlsSample.UI
名称空间的一部分。另一个区别是PageControl
没有构造函数;然而,它有一个ready
函数,用来初始化你的控件属性。最后,虽然实例成员没有对象,但是您的first
、last
和birthday
属性是在ready
函数旁边声明的。
JavaScript 控件和PageControl
的显著区别在于PageControl
还包括一个 HTML(和 CSS)文件,允许您在标记中定义更多的控件布局和设计。清单 8-22 包含了您需要为您的PageControl
添加到contactControl.html
中的代码。如果你熟悉使用 ASP.NET 进行 web 开发,JavaScript 控件在概念上类似于 ASP.NET 服务器控件,而PageControl
在概念上类似于 ASP.NET 用户控件。像 ASP.NET 服务器控件一样,JavaScript 控件可以并且经常被定义在一个代码文件中。这使得项目之间的可重用性非常简单。另一方面,PageControl
的定义分布在多种类型的多个文件中,通常一个文件包含表示逻辑——HTML 用于PageControl
和 ASCX 用于 ASP.NET 用户控件——另一个文件包含行为逻辑——JavaScript 用于PageControl
和. NET 语言,如 C#或 VB.NET,用于 ASP.NET 用户控件。虽然这提供了一种更简单的方法来控制控件的表示,但它确实使项目之间的可重用性稍显不便。
清单 8-22。 页面控件的标记
<script src="/controls/pages/contactControl/contactControl.js"></script><body><div class="contactControl"><p class="contactControl-first"><strong>First name:</strong><span id="firstContent">First name goes here</span></p><p class="contactControl-last"><strong>Last name:</strong><span id="lastContent">Last name goes here</span></p><p class="contactControl-birthday"><strong>Birthday:</strong><span id="birthdayContent">Birthday goes here</span></p></div>
</body>
一旦创建了页面控件,向页面添加控件并设置选项的过程与添加自定义 JavaScript 控件或 WinJS 提供的现成控件没有任何不同。将清单 8-23 中的代码添加到customcontrols.html
中。在本例中,您在标记中以声明方式设置了first
、last,
和birthday
属性。实际上,你可能会在 JavaScript 中设置这些值,可能是在数据绑定的过程中,这个主题我将在第十一章中更全面地讨论。
清单 8-23。 将页面控件添加到 customcontrols.html
<div id="myContact"data-win-control="WinJSControlsSample.UI.Contact"data-win-options="{first: 'Scott',last: 'Isaacs',birthday: 'December 1' }">
</div>
当然,这个PageControl
例子非常简单,但是您可以看到开发一个PageControl
与开发一个定制的 JavaScript 控件是多么相似。当您需要一个可重用的复合控件(一个本身包含多个控件的控件)时,这尤其有用。如果你想一想,这正是我们从《??》第五章开始对《??》所做的事情。您一直使用它来包含示例应用的整个屏幕的内容,其中包含几个子控件。您将控件视为独立的单元,彼此独立。然而,通过定义一些额外的属性,您可以看到如何使用清单 8-21 中的PageControl
和清单 8-22 中的来显示从其自身定义之外提供的数据(参见图 8-8 )。
图 8-8 。你的页面控件和你的自定义 JavaScript 控件一起显示
结论
在本章中,我介绍了两种在应用中创建可重用控件的技术。通过将一些用户界面和行为封装到一个包中,自定义控件提供了方便的可重用性,无论是在您的项目中还是在您将来可能开发的其他项目中。每当您发现自己多次构建同一个界面时,都应该考虑自定义控件。这一章的大部分时间我都在构建一个自定义的 JavaScript WinJS 控件,您将在本书的后面再次使用它,并简要介绍了PageControl
,您已经在几章中使用了它。
九、构建用户界面
现在,我已经介绍了触摸概念、Microsoft 设计风格的原则、创建 Visual Studio 项目以及使用 Windows 应用商店应用的许多可用控件,是时候做一些更有趣的事情了。在本章中,您将开始构建一个真实世界的应用,并在本书的其余部分继续构建。我们将构建一个面向软件顾问、设计师、自由职业者和任何其他执行基于项目的工作的人的计时应用。
当我不写书的时候,我会花时间做软件开发顾问。像许多咨询公司一样,我工作的公司使用第三方时间和费用跟踪系统来支持它与各种客户合作的所有顾问。从任何地方都可以使用 web 浏览器访问它,这是一个非常完整的系统,具有各种功能和配置选项。不幸的是,这不是最容易使用的系统。所有的附加功能和可配置选项都需要大量的点击和导航来输入我每天的计费时间。因此,像许多必须记录项目花费时间的人一样,我发现自己使用穷人的通用数据库:Microsoft Excel 来记录我所有的时间。每月一次,我在一个窗口打开我们的时间和费用系统,在另一个窗口打开 Excel,将时间从一个窗口复制到另一个窗口。还不算太糟,但是有时我会在同一天接到多个客户的账单。我可以通过调整我在 Excel 中记录时间的方式来解决这个问题,或者我可以写一个应用。因为我刚好在写一本关于构建 Windows Store 应用的书,所以我选择了后者,这样我们就可以一起构建了。
介绍克洛克
在本书的剩余章节中,我们将构建 Clok,一个 Windows 商店的时间表应用。为了防止这本书长达 1500 页,我将保持基本的特性集。以下是顾问可以使用 Clok 执行的高级功能:
- 通过启动和停止计时器来跟踪项目的时间
- 将照片和文稿添加到项目
- 管理项目列表
- 管理以前跟踪的时间条目
这听起来可能不多,但是您会发现,有许多技术对于使用 HTML 和 JavaScript 构建 Windows Store 应用非常有用,这些技术可以组合在一起构建一个真实但简单的应用。在图 9-1 中,你可以看到当你完成这一章时,我们应用的主屏幕将会是什么样子。
图 9-1 。完整的 Clok 仪表板
我定义的四个高级功能都有一个按钮。最大的按钮用于开启和关闭计时器。在图 9-1 中,计时器当前正在运行,我已经指出了我正在进行的项目,并添加了一些注释。此外,还添加了徽标和当前时间。在接下来的几页中,您将看到这个屏幕是如何创建的。
注本书附带的源代码包括一个完整的项目,其中包含本章使用的所有源代码和图像文件。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
创建项目外壳
因此,让我们卷起袖子,开始使用导航应用模板创建一个新的 Visual Studio 项目。如果图 9-2 还不够,你可以参考第五章并遵循同样的步骤。将新项目命名为 Clok。我将在本书的剩余部分构建这个项目中应用的所有功能。
图 9-2 。“新建项目”对话框
正如您在第五章中看到的,导航应用项目模板为我们创建了许多文件。在这一章中,我们编辑这些文件中的大部分,并创建一些新文件,以实现如图图 9-1 所示的应用设计。除了我们在本章中开发的主屏幕,我们将在应用中使用这种设计,因为我们将在后面的章节中添加更多的屏幕。
实现设计
当我们在第五章中创建项目时,我们配置应用使用ui-light.css
中定义的主题,而不是ui-dark.css
中定义的默认主题。然而,对于 Clok,我们将保留默认的黑暗主题。不要求使用这两个主题中的任何一个;但是,它们提供了一种简单的方法来确保您的应用的外观和行为与您的用户已经安装和使用的许多其他应用一样。
在ui-dark.css
中定义的主题具有深色背景和浅色文本。默认背景是深灰色,文本是白色的。相反,ui-light.css
中定义的主题有浅色背景和深色文本,默认为白色背景上的黑色文本。虽然我们确实想使用深色主题,但我们希望在屏幕右下角有一个蓝色背景和一个模糊版本的应用徽标。将清单 9-1 中突出显示的代码添加到default.css
中。一定要将background.png
(可以在本书附带的源代码中找到)添加到images
文件夹中。
清单 9-1。 改变我们应用的背景
#contenthost {height: 100%;width: 100%;background-color: #3399aa;background-image: url('/img/background.png');background-repeat: no-repeat;background-attachment: fixed;background-position: 100% calc(100% - 85px);
}
详细的 CSS 教程超出了本书的范围,但简单地说,这段代码将应用的背景颜色设置为蓝色,用#3399aa
表示,并设置一个显示一次的背景图像(no-repeat
),当用户滚动应用窗口(fixed
)时保持不动,并位于屏幕底部最右侧 85 像素处。如果你正在寻找关于 CSS 的更深入的报道,除了数百本书之外,你可以用你最喜欢的网络搜索引擎找到大量的信息,比如大卫·鲍尔斯的开始 CSS 3(a press,2012)。现在运行应用将显示如图 9-3 所示的应用。我们还没到那一步,但已经开始成形了。
图 9-3 。我们的新应用背景在一个分辨率为 1366×768 的屏幕上
它看起来很好,但是 Clok 标志比图 9-1 中的大了很多。根据您的屏幕分辨率,它可能会比您想要的占用更多的屏幕空间。比如很多 Windows RT 平板的屏幕分辨率都是 1366×768;不过我的笔记本电脑屏幕是 1920×1080 的屏幕分辨率。图 9-4 显示了同一个应用在这个更大的屏幕上的样子。
图 9-4 。我们相同的新应用背景显示在分辨率为 1920×1080 的大屏幕上
我们真的不希望我们的时间输入表单覆盖我们的应用徽标。我们希望用户可以看到这个标志,尤其是在 Clok 应用的主屏幕上。我们可以将徽标变小,这样它就不会被平板电脑等较小屏幕上的表单隐藏,但在较大的屏幕分辨率下,徽标可能会显得太小。解决方案是使用 CSS 媒体查询来定义仅在满足特定条件时才应用的 CSS 规则。在default.css
的末尾添加来自清单 9-2 的代码。当应用窗口宽度小于 1400 像素时,这将导致背景徽标调整大小。在下一节中,我将介绍测试这种行为的技术。
清单 9-2。 在较小的屏幕上改变背景图像的大小
@media screen and (max-width: 1400px) {#contenthost {background-size: 40%;}
}
实现 Clok 设计的下一步是将当前时间添加到屏幕底部。我们将使用我们在第八章中构建的时钟控件的修改版本。修改包括几个简单的助手函数,这些函数将在以后派上用场,同时还更改了名称空间。修改后的版本包含在本章的源代码管理中。清单 9-3 突出显示了对default.html
添加控件所做的修改。
清单 9-3。 添加当前时间
<!DOCTYPE html>
<html>
<head><title>Clok</title><!-- WinJS references --><link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" /><script src="//Microsoft.WinJS.1.0/js/base.js"></script><script src="//Microsoft.WinJS.1.0/js/ui.js"></script><!-- Clok references --><link href="/css/default.css" rel="stylesheet" /><script src="/js/default.js"></script><script src="/js/navigator.js"></script><script src="/controls/js/clockControl.js"></script>
</head>
<body><div id="contenthost"data-win-control="Application.PageControlNavigator"data-win-options="{home: '/pages/home/home.html'}"></div><div id="currentTime" data-win-control="Clok.UI.Clock"></div>
</body>
</html>
控件的默认配置是显示当前时间,包括秒。你会从第八章中记起,你可以用data-win-options
将showClockSeconds
设置为false
。您也可以在 JavaScript 中设置相同的值,方法是添加清单 9-4 到default.js
中突出显示的代码。
清单 9-4。 在 JavaScript 中设置控件选项
args.setPromise(WinJS.UI.processAll().then(function () {currentTime.winControl.showClockSeconds = false;if (nav.location) {nav.history.current.initialPlaceholder = true;return nav.navigate(nav.location, nav.state);} else {return nav.navigate(Application.navigator.home);}
}));
最后,将清单 9-5 中的 CSS 规则添加到default.css
中。此规则设置字体大小和粗细,将颜色设置为半透明白色,并将时间放在屏幕的左下角。
清单 9-5。 造型当前时间
#currentTime {font-size: 60pt;font-weight: 200;letter-spacing: 0;line-height: 1.15;color: rgba(255, 255, 255, 0.2);position: fixed;top: calc(100% - 85px);left: 10px;
}
实现我们的设计的最后一步是添加一些增量 CSS 规则来覆盖各种控件的颜色,以便它们与我们想要的设计相匹配。Microsoft 提供了大量示例应用来说明构建 Windows 应用商店应用的不同方面。您可以单独下载许多示例,但我建议从 MSDN 下载整个示例应用包:http://msdn.microsoft.com/en-US/windows/apps/br229516
。虽然这个包中的所有示例都有助于理解如何在您的代码中实现不同的功能,但主题 Roller 示例实际上是开发人员的一个有用软件。它允许你选择一个亮或暗的主题,并指定一些你想在应用中使用的不同颜色。然后它会生成并预览一些 CSS 规则来添加到你自己的应用代码中(见图 9-5 )。
`
图 9-5 。主题滚轮示例应用
注主题滚轮应用包含在示例 app 包中,也可以单独下载:
http://code.msdn.microsoft.com/windowsapps/Theme-roller-sample-64b679f2
。
本章附带的示例代码包括一个名为themeroller.css
的文件,该文件包含由 Theme Roller 示例应用生成的增量 CSS 规则。您可以将该文件复制到您的 Visual Studio 项目中,或者您可以使用主题 Roller 示例应用自己生成 CSS 规则。你必须将这个 CSS 文件的引用添加到default.html
(参见清单 9-6 )。
清单 9-6。 引用新的 CSS 文件
<!-- SNIPPED -->
<link href="/css/default.css" rel="stylesheet" />
<link href="/css/themeroller.css" rel="stylesheet" />
<!-- SNIPPED -->
因为 Clok 目前是相当空的,所以您实际上还看不到我们应用中的任何变化,但是生成的样式覆盖了ui-light.css
和ui-dark.css
中的一些默认样式,所以我们稍后添加的控件匹配我们已经定义的主题。例如,清单 9-7 包含了来自themeroller.js
的一个片段,它将改变你添加的任何下拉列表控件的颜色,比如在图 9-1 中用于选择一个项目的控件。
清单 9-7。 用增量 CSS 规则覆盖默认 CSS 规则
/*
Text selection color
*/
::selection, select:focus::-ms-value {background-color: rgb(0, 0, 70);color: rgb(255, 255, 255);
}/*
Option control color
*/
option:checked {background-color: rgb(0, 0, 70);color: rgb(255, 255, 255);
}option:checked:hover, select:focus option:checked:hover {background-color: rgb(33, 33, 94);color: rgb(255, 255, 255);}
至此,我们已经完成了 Clok 的外壳。类似于我们在第五章到第八章中所做的工作,除了我们将在本章后面添加的SettingsFlyout
控件之外,这个应用中的大多数功能都将使用页面控件加载到这个 shell 中,在我看来,这使得导航应用模板非常方便。
使用模拟器进行调试
基于图 9-3 和图 9-4 之间的差异,你可以明白为什么用不同类型和尺寸的设备定期测试你的应用是一个好的实践。在尽可能多的真实设备(台式机、笔记本电脑和平板电脑)上测试您的应用是无可替代的。但是,有时您不容易访问这些设备,或者您可能正在积极开发功能,还没有准备好部署到多台机器上进行更严格的测试。如果你没有各种硬件来测试,微软视窗模拟器可以帮助你。模拟器是你在开发机器上运行的软件,允许你在不同的设备上模拟运行你的应用。
最重要的是,您已经有了模拟器,因为它是随 Visual Studio 一起安装的。你可能已经注意到了 Visual Studio 中调试按钮旁边的一个小菜单指示器(见图 9-6 )。此菜单允许您设置调试目标。默认情况下,选择本地机器,但是有两个附加选项:远程机器和模拟器。选择模拟器,然后运行您的应用。模拟器将打开,您的应用将启动(参见图 9-7 )。
图 9-6 。使用模拟器进行调试
图 9-7 。在模拟器中完成版本的 Clok
注意模拟器不是虚拟机,也不是和你的开发机器隔离的。模拟器只是运行与您的开发计算机上安装的相同的 Windows 安装,并且使用您用来启动 Visual Studio 的相同凭据运行。因此,我发现在我的开发机器的后台运行的一些程序在我启动模拟器时偶尔会表现异常,通常是因为该程序的两个实例——一个在我的开发机器上,一个在模拟器上——试图访问同一个锁定的资源。
模拟器提供了许多特性来促进基本测试。在模拟器工具栏的顶部,您会发现一个图钉图标,您可以切换该图标以将模拟器保持在所有其他窗口的顶部(参见图 9-8 )。在它的正下方是四个图标,允许您选择交互模式。您可以在鼠标模式、基本触摸模式、挤压/缩放触摸模式和旋转触摸模式之间进行选择。在鼠标模式下,您的应用在模拟器中的行为与在开发机器上的行为相同。
图 9-8 。选择交互模式(左);触摸模式下的鼠标光标(中间)和按下时的鼠标光标(右侧)
在基本触摸模式下,鼠标光标被替换为单目标图标,而在捏合/缩放或旋转触摸模式下,它是双目标图标。这些目标指示当你点击鼠标时,你的虚拟手指将触摸屏幕的位置。当您在其中一种触摸模式下单击鼠标时,光标会再次改变,以指示触摸交互正在进行中。在基本触摸模式下,您的鼠标的行为可能与您预期的一样:单击鼠标以点击,然后单击并按住鼠标以拖动或打开上下文菜单。
捏/缩放和旋转触摸模式一开始使用起来有点棘手,但只需几次尝试就能习惯。你滚动鼠标滚轮来移动目标。在挤压/缩放触摸模式下,这将使目标靠近或远离,而在旋转触摸模式下,一个目标将围绕另一个目标旋转。一旦你的目标被设置成你想要的样子,点击并按住你的鼠标按钮,同时用你的鼠标滚轮滚动来实际执行手势。我的主要开发机器是一台没有触摸屏的笔记本电脑。对于更精确的手势,我发现用两只手更容易。我使用笔记本电脑内置的触摸板按钮来点击,同时使用鼠标上的滚轮来挤压、缩放或旋转。不过,你的手指可能比我的更协调。
下一组图标,如图 9-9 所示,允许您测试对模拟设备的更改。前两个选项允许您顺时针或逆时针旋转设备 90 度,在横向、纵向、横向翻转和纵向翻转方向之间循环。图 9-9 中的第三个图标提供了改变模拟器屏幕分辨率的方法。我在创建图 9-3 和图 9-4 中的图像时使用了这个功能。这组按钮中的最后一个图标提供了更改模拟器位置的方法,这在测试使用地理定位来确定应用用户位置的功能时非常有用。
图 9-9 。更改设备设置
其余的工具栏图标如图 9-10 所示。相机图标将获取模拟器的截图,齿轮图标允许您指定截图将存储在开发机器的驱动器上的什么位置。最后,熟悉的问号图标将为您提供模拟器所提供的各种功能的更多帮助。
图 9-10 。更改设备设置
我鼓励你花点时间在模拟器上玩一玩。打开您已经安装的其他应用,观察这些应用在屏幕旋转或调整大小时的行为。熟悉不同的交互模式。虽然在开发 Clok 时,我们不太需要使用收缩/缩放和旋转交互模式,但在构建更复杂的应用时,它们肯定会派上用场。尽管模拟器很有帮助,但建议您在将应用发布到 Windows 应用商店之前,在尽可能多的真实设备上测试应用。
注意如果你在网络上有第二个测试设备,远程机器调试非常方便,我经常用它来测试我平板电脑上的应用。为调试配置远程目标既快速又简单。MSDN 上有一个很好的攻略:
http://msdn.microsoft.com/en-us/library/windows/apps/hh441469.aspx
。
添加设置弹出按钮
随着我们在接下来的几章中构建 Clok,我们将添加一些应用级别的设置,用户可以利用这些设置来定制应用的行为。在这一节,我们将添加一个SettingsFlyout
,我们将在本书的其余部分继续添加设置。我们还将在另一个SettingsFlyout
中添加一些关于 Clok 的信息,给潜在用户一个应用的概述和一个找到更多信息的方法。
我们已经在第六章的中看到了如何添加一个空的SettingsFlyout
到我们的应用中,我们将在本章中遵循同样的基本步骤。然而,我们将在 Visual Studio 中以稍微不同的方式组织我们的文件,以最小化混乱。让我们从在 Visual Studio 中添加一个settings
文件夹到我们项目的根目录开始(见图 9-11 )。
图 9-11 。在解决方案资源管理器中添加设置文件夹
注意我们用来构建应用的文件只是普通的 HTML、CSS 和 JavaScript 文件。我们可以按照我们认为合适的方式来组织我们的项目,只要所有的部分都与正确的路径相链接。例如,我们可以选择将每个
PageControl
的 CSS 文件放到项目的css
文件夹中。或者,如果我们计划添加大量的页面,我们可以将它们组织到子文件夹中。
锁定选项设置弹出按钮
将名为options.html
的 HTML 文件添加到settings
文件夹中。用清单 9-8 中的代码替换该文件的默认内容。这将是我们的用户想要改变 Clok 默认设置时打开的SettingsFlyout
。我们将在整本书中添加更多内容。
***清单 9-8。***options.html 的 HTML 代码
<!DOCTYPE html>
<html>
<head><title>Options</title>
</head>
<body><div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout"aria-label="Options"data-win-options="{settingsCommandId:'options',width:'narrow'}"><div class="win-ui-dark win-header" style="background-color: #000046;"><button type="button" class="win-backbutton"onclick="WinJS.UI.SettingsFlyout.show()"></button><div class="win-label clok-logo">Options</div></div><div class="win-content"><div class="win-settings-section"><h3>Settings Section Header</h3><p>Put your settings here.</p></div><div class="win-settings-section"><h3>Settings Section Header</h3><p>Put your settings here.</p></div></div></div>
</body>
</html>
当您查看这段代码时,您会发现它与清单 6-18 中的非常相似。除了为标题指定窄的宽度和不同的颜色,最显著的区别是我们用 CSS 类win-settings-section
在div
元素中添加了一些占位符内容。这个类是由 WinJS 提供的,并且是一个简单的方法来应用一个样式到我们的SettingsFlyout
上,这个样式和其他的 Windows 应用是一致的。您可以在一个名为ui-dark.css
的文件中看到这个 CSS 规则,以及 WinJS 提供的任何其他 CSS 规则。为了在解决方案资源管理器中找到这个文件,展开References
文件夹,然后是Windows Library for JavaScript 1.0
文件夹,最后是css
文件夹(参见图 9-12 )。您不能编辑这些文件,但您可以通过添加类似于我们之前使用主题滚轮的增量样式来更改样式。
图 9-12 。WinJS 提供的 CSS 文件
关于锁定设置弹出按钮
您现在使用的大多数应用都有一个包含应用信息的屏幕。通常情况下,会打开一个弹出窗口,显示应用的描述和链接,以了解有关应用或构建它的公司的更多信息。这一概念延续到了 Windows Store 应用,弹出窗口被一个SettingsFlyout
所取代。要在 Clok 中添加这个特性,需要在settings
文件夹中添加一个名为about.html
的 HTML 文件。用清单 9-9 中的代码替换该文件的默认内容。
***清单 9-9。***about.html 的 HTML 代码
<!DOCTYPE html>
<html>
<head><title>About Clok</title>
</head>
<body><div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout"aria-label="About Clok"data-win-options="{settingsCommandId:'about',width:'narrow'}"><div class="win-ui-dark win-header" style="background-color: #000046;"><button type="button" class="win-backbutton"onclick="WinJS.UI.SettingsFlyout.show()"></button><div class="win-label clok-logo">About Clok</div></div><div class="win-content"><div class="win-settings-section"><h3>About Clok</h3><p>Clok is a sample application being developed in conjunction with<em>Beginning Windows Store Application Development: HTML and JavaScriptEdition</em>, an upcoming title about building Windows Store applicationswith HTML, JavaScript and CSS using the WinJS and WinRT libraries. It iswritten by <a href="http://www.tapmymind.com">Scott Isaacs</a> and<a href="http://apress.com/">Apress Media LLC</a> will publish thetitle in Summer 2013.</p><p>For more information, please visit:<a href="http://clok.us/">http://clok.us/</a>.</p></div></div></div>
</body>
</html>
正如您所看到的,在这种情况下使用SettingsFlyout
是一种合适的技术,尽管它实际上并不用于修改任何应用设置,正如其名称所暗示的那样。
将设置弹出按钮添加到设置窗格
最后一步是注册我们的两个SettingsFlyout
控件,以便 Windows 在设置面板上显示它们。打开default.js
并添加清单 9-10 中突出显示的代码。
清单 9-10。 注册我们的设置弹出按钮
// SNIPPEDif (app.sessionState.history) {nav.history = app.sessionState.history;
}// add our SettingsFlyout to the list when the Settings charm is shown
WinJS.Application.onsettings = function (e) {e.detail.applicationcommands = {"options": {title: "Clok Options",href: "/settings/options.html"},"about": {title: "About Clok",href: "/settings/about.html"}};WinJS.UI.SettingsFlyout.populateSettings(e);
};args.setPromise(WinJS.UI.processAll().then(function () {currentTime.winControl.showClockSeconds = false;if (nav.location) {nav.history.current.initialPlaceholder = true;return nav.navigate(nav.location, nav.state);} else {return nav.navigate(Application.navigator.home);}
}));// SNIPPED
运行应用并打开“设置”面板。您可以使用以下方法之一打开“设置”面板:
- 将鼠标移动到屏幕的右上角以显示 Windows charms,然后单击设置按钮
- 从触摸屏右侧向内滑动以显示 Windows charms,然后点击设置按钮
- 在键盘上按下 Windows 徽标键+I
一旦你这样做了,你会看到我们的两个SettingsFlyout
控件在图 9-13 中列出。
图 9-13 。“设置”面板中列出了我们的设置弹出控件
构建仪表板
到目前为止,在这一章中,我们已经将重点放在实现应用的通用设计元素上——这些元素将在 Clok 的每个屏幕上可见。在这一部分,我们将开始构建仪表板的用户界面,如图 9-14 所示。
图 9-14 。仪表盘上的 UI 元素
当然,您可以在您的应用中使用任何您希望的布局技术,因为我们只是使用 HTML 和 CSS 来布局我们的界面元素。我将借此机会讨论 CSS3 中两种不同的布局选项,以 Clok dashboard 为例。我将介绍 flexbox 布局和网格布局,这两者都是万维网联盟(W3C)的工作草案。换句话说,这些 CSS3 布局很可能会成为跨不同浏览器工作的标准。
注意用 HTML 和 JavaScript 构建的 Windows Store 应用使用了与 Internet Explorer (IE) 10 相同的渲染引擎。因此,IE 10 中支持的 CSS 和 JavaScript 在您的应用中也受支持。
Flexbox 布局
flexbox 或 flexible box 布局是一个新的 CSS 布局选项,通过将元素的display
属性设置为-ms-flexbox
来启用。它提供了一种简单的方法来指示该元素的子元素的大小是灵活的,通过指定它们如何增长或收缩来填充可用空间。flexbox 布局有很多选项,关于它可以写几十页。我不会深入探讨这个布局选项,我将只介绍两个用例作为例子。首先,我们将通过定义灵活的区域来创建仪表板的整体页面布局。然后我们将再次使用 flexbox 布局来定位四个菜单选项,以匹配图 9-14 。
使用 Flexbox 定义页面布局
图 9-14 中的仪表板有两个内容区域。在左边,我们有四个菜单选项,右边是一个时间输入表单。打开home.html
并用清单 9-11 中突出显示的代码替换 main section 元素的内容。
清单 9-11。 仪表盘上的两个内容区
<section aria-label="Main content" role="main"><div id="mainMenu"></div><div id="timeEntry"></div>
</section>
在本节的后面,我们将向mainMenu
元素添加菜单选项,向timeEntry
元素添加大的计时器显示和表单字段。在我们添加所有这些控件之前,让我们先来看看这两个区域的布局。将清单 9-12 中突出显示的代码添加到home.css
中,将 CSS 规则添加到三个元素中:主要部分和两个新内容区域中的每一个。
清单 9-12。 在 CSS 中设置 Flexbox
.homepage section[role=main] {margin-left: 120px;width: calc(100% - 120px);display: -ms-flexbox;-ms-flex-direction: row;-ms-flex-align: start;-ms-flex-pack: start;-ms-flex-wrap: nowrap;
}.homepage #mainMenu {width: 424px;-ms-flex: 0 auto;border: 2px solid yellow; /* temporary */height: 500px; /* temporary */}.homepage #timeEntry {margin-left: 20px;margin-right: 20px;-ms-flex: 1 auto;border: 2px solid yellow; /* temporary */height: 500px; /* temporary */}@media screen and (-ms-view-state: snapped) {.homepage section[role=main] {margin-left: 20px;}
}@media screen and (-ms-view-state: portrait) {.homepage section[role=main] {margin-left: 100px;}
}
将display
属性设置为-ms-flexbox
表示主部分应该被视为 flexbox 容器。将-ms-flex-direction
属性设置为row
会导致该 flexbox 的子对象的水平布局。该属性的其他选项包括column
,用于垂直定向,以及row-reverse
和column-reverse
,用于以与定义时相反的顺序显示子项。
-ms-flex-align
属性用于指定子元素如何垂直于-ms-flex-direction
对齐。也就是说,当使用row
指定水平布局时,-ms-flex-align
属性指定如何垂直显示子元素,当使用column
时,它指定如何水平显示子元素。因为我们的例子有一个水平布局,将-ms-flex-align
设置为start
将会在容器顶部对齐我们的两个内容区域。该属性的其他选项包括end
、center
、stretch
和baseline
。
当-ms-flex-align
属性控制垂直于布局方向的显示时,-ms-flex-pack
属性控制平行于布局方向的布局。因为我们已经将这个属性设置为start
,这个 flexbox 的子对象将会向左对齐。该属性的其他选项包括end
、center
和justify
。
既然我们的 flexbox 容器已经定义好了,让我们看看我们为两个内容区域添加的规则。添加一个width
设置将会使菜单选项保持在左边,给timeEntry
元素添加边距将会防止定时器和表单与菜单选项冲突。-ms-flex
属性是 flexbox 的最后一块魔力。为mainMenu
元素指定0
将阻止它增长或收缩以适应可用空间,但为timeEntry
元素指定1
将导致该元素增长以填充剩余空间。如果我们有两个非零值的元素,剩余的空间将按比例分割成这个值,这意味着如果一个被设置为1
而另一个被设置为2
,第二个元素将伸缩两倍于第一个元素。
注意关于 flexbox 布局的更深入的讨论,包括本章使用的所有属性,可以在 MSDN:
http://msdn.microsoft.com/en-us/library/ie/hh673531.aspx
找到。
对于mainMenu
和timeEntry
元素,我临时添加了一个height
和一个border
,这样我们可以很容易地看到 flexbox 实际上是如何布局其子元素的。你可以在图 9-15 中看到结果。
图 9-15 。我们最初的 flexbox 布局
用 Flexbox 定位菜单选项
到目前为止,我们已经看到了一个使用 flexbox 布局在页面中创建区域的例子。它还可以用于在这些区域中的一个区域内布置内容。我们还将使用 flexbox 在图 9-14 的左侧布置菜单选项。首先,让我们添加 HTML。打开home.html
并用清单 9-13 中突出显示的代码替换主节元素的内容。
***清单 9-13。***Clok 仪表盘的内容
<section aria-label="Main content" role="main"><div id="mainMenu"><div id="toggleTimerMenuItem" class="mainMenuItem primaryMenuItem"></div><div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem"></div><div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem"></div><div id="timesheetMenuItem" class="mainMenuItem secondaryMenuItem"></div></div><div id="timeEntry"><div id="elapsedTime"><h2 id="elapsedTimeClock"data-win-control="Clok.UI.Clock"data-win-options="{ mode: Clok.UI.ClockModes.CountUp }"></h2></div><div><label for="project">Project</label><select id="project"><option value="">Choose a project</option><option value="1">Website Redesign (ABC Telecom)</option><option value="2">Windows Store App (ABC Telecom)</option></select></div><div><label for="timeNotes">Notes</label><textarea id="timeNotes"></textarea></div><div><button id="saveTimeButton">Save</button><button id="discardTimeButton">Discard</button></div></div>
</section>
我在mainMenu
区域添加了四个空的div
元素,作为菜单选项的占位符。我将在下一节添加实际的内容——图标和文本。我还在timeEntry
区域添加了计时器和时间输入表单字段。清单 9-14 包含了home.css
的代码。
清单 9-14。 新 CSS 规则布局菜单选项和时间录入表单字段
.homepage section[role=main] {margin-left: 120px;width: calc(100% - 120px);display: -ms-flexbox;-ms-flex-direction: row;-ms-flex-align: start;-ms-flex-pack: start;-ms-flex-wrap: nowrap;
}.homepage #mainMenu {width: 424px;-ms-flex: 0 auto;display: -ms-flexbox;-ms-flex-direction: row;-ms-flex-align: start;-ms-flex-wrap: wrap;}/* all menu buttons */.homepage .mainMenuItem {border: 2px solid transparent;margin: 4px;background: rgba(0,0,50,0.65);}/* just the big menu button */.homepage .primaryMenuItem {height: 408px;width: 408px;}/* the smaller menu buttons */.homepage .secondaryMenuItem {height: 128px;width: 128px;}.homepage #timeEntry {margin-left: 20px;margin-right: 20px;-ms-flex: 1 auto;}.homepage #timeEntry label {display: block;font-size: 2em;}.homepage #elapsedTime {padding-bottom: 30px;}.homepage #elapsedTimeClock {font-size: 8em;}.homepage #project {width: 400px;}.homepage #timeNotes {width: 400px;height: 75px;}@media screen and (-ms-view-state: snapped) {.homepage section[role=main] {margin-left: 20px;}
}@media screen and (-ms-view-state: portrait) {.homepage section[role=main] {margin-left: 100px;}
}
我去掉了向两个主要内容区域添加黄色边框和高度的规则,但是我还添加了许多其他规则。我们对timeEntry div
及其包含的控件所做的更改非常简单,所以我不会在这里讨论它们。不过,你会注意到,我们通过将display
属性设置为-ms-flexbox
来声明mainMenu div
是另一个 flexbox 容器。与主部分一样,该容器也是通过将-ms-flex-direction
属性设置为row
以水平布局排列的,并且通过将-ms-flex-align
设置为start
其内容在顶部对齐。然而,这一次,我们已经通过将-ms-flex-wrap
属性设置为wrap
来指示不适合第一行的项目应该换行到下一行。运行应用,查看我们目前的进度(图 9-16 )。
图 9-16 。菜单选项的占位符,以及我们的时间输入表单
我们离图 9-1 中的目标越来越近了。我们将一个 flexbox 容器嵌套在另一个中。这是一种构建复杂布局的强大方法,正如您将在下一节中看到的,可以在这个 flexbox 容器中进一步嵌套网格布局。
网格布局
与 flexbox 类似,网格布局是另一个新的 CSS 布局选项,通过将元素的display
属性设置为-ms-grid
来启用。顾名思义,这种布局允许您指示该元素的子元素排列在一个网格中。
如果你是十年或更久以前的 web 开发人员,你可能熟悉使用 HTML table
元素来布局 web 页面。在使用 CSS 进行网页布局变得突出之前,这是一种常见的做法。虽然它允许对布局进行简单的控制,但由于几个原因,它不再受欢迎,特别是因为它严重地将表示逻辑与页面内容混合在一起。大约在从使用 HTML table
元素布局到使用 CSS 布局的转变开始的同时,我开始花更多的时间在 web 应用的后端,而不是在布局上。因此,我仍然对基于table
的布局情有独钟,尽管我知道有更好的选择。
幸运的是,网格布局现在已经可用。我和像我一样的其他人现在可以使用熟悉的基于表格的概念来实现我们想要的布局,但仍然保持我们的内容和表示逻辑之间的分离,因为网格现在是在 CSS 中指定的,而不是用table
元素指定的。我们将使用网格布局向每个菜单选项添加图标和文本。将清单 9-15 中突出显示的代码添加到home.html
中。另外,一定要添加这里引用的图像文件;它们包含在本书附带的源代码中。
***清单 9-15。***Clok 仪表盘的菜单选项
<div id="mainMenu"><div id="toggleTimerMenuItem" class="mainMenuItem primaryMenuItem"><img class="mainMenuItem-image" id="timerImage" src="/img/Clock-Stopped.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title" id="timerTitle">Start Clok</h4></div></div><div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem"><img class="mainMenuItem-image" src="/img/Camera.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title">Camera</h4></div></div><div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem"><img class="mainMenuItem-image" src="/img/Projects.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title">Projects</h4></div></div><div id="timesheetMenuItem" class="mainMenuItem secondaryMenuItem"><img class="mainMenuItem-image" src="/img/Timesheet.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title">Time Sheets</h4></div></div>
</div>
对于我们之前创建为空的div
元素的每个菜单选项,我们现在添加了一个图标和一个标签。四个菜单选项中的每一个所使用的代码模式都是相同的,除了第一个被分配了一个 CSS 类primaryMenuItem
,而其他三个使用了secondaryMenuItem
CSS 类。无论大小,所有四个都分配了mainMenuItem
CSS 类。将多个类分配给一个元素的能力允许我们指定适用于所有元素的 CSS 规则,以及仅适用于某些元素的单独规则。清单 9-16 中突出显示了home.css
所需的 CSS 变化。
清单 9-16。 CSS 实现菜单选项 的网格布局
/* SNIPPED */.homepage #mainMenu {-ms-flex: 0 auto;width: 424px;display: -ms-flexbox;-ms-flex-align: center;-ms-flex-direction: row;-ms-flex-wrap: wrap;
}/* all menu buttons */.homepage .mainMenuItem {border: 2px solid transparent;margin: 4px;background: rgba(0,0,50,0.65);display: -ms-grid;-ms-grid-columns: 1fr;}.homepage .mainMenuItem:hover {cursor: pointer;border: 2px solid #ffffff;}.homepage .mainMenuItem .mainMenuItem-image {-ms-grid-row-span: 2;}.homepage .mainMenuItem .mainMenuItem-overlay {-ms-grid-row: 2;padding: 6px 15px;background: rgba(0,0,35,0.65);}/* just the big menu button */.homepage .primaryMenuItem {height: 408px;width: 408px;-ms-grid-rows: 1fr 70px;}.homepage .primaryMenuItem .mainMenuItem-image {height: 382px;width: 382px;margin: 10px;}.homepage .primaryMenuItem .mainMenuItem-overlay .mainMenuItem-title {font-size: 2.5em;}/* the smaller menu buttons */.homepage .secondaryMenuItem {height: 128px;width: 128px;-ms-grid-rows: 1fr 32px;}.homepage .secondaryMenuItem .mainMenuItem-image {height: 128px;width: 128px;padding: 0;}.homepage .secondaryMenuItem .mainMenuItem-overlay .mainMenuItem-title {font-size: 1em;}.homepage #timeEntry {margin-left: 20px;margin-right: 20px;-ms-flex: 1 auto;
}/* SNIPPED */
从清单 9-16 的顶部开始,我们做的第一个改变是通过将display
属性设置为-ms-grid
来表明我们的任何mainMenuItem
元素都将是网格布局容器。我们在-ms-grid-columns
属性中定义了一个单列。-ms-grid-columns
属性和-ms-grid-rows
属性(我们稍后将介绍)都可以采用各种不同的值,包括以下内容:
- 一个或多个带单位的指定尺寸,如
3px
或1.5em
- 一个或多个百分比值
- 剩余空间的一个或多个部分,如
1fr
或1fr 2fr
- 这些值的任意组合,比如
150px 1fr 2fr 150px
。本例将定义四列(-ms-grid-columns
)或四行(-ms-grid-rows
),其中第一列和第四列各为 150 像素,剩余空间的三分之一将分配给第二列或第二行,三分之二分配给第三列或第三行。
对于-ms-grid-columns
属性和-ms-grid-rows
属性还有一些其他的选项,我不会在这里介绍,但是你可以在http://msdn.microsoft.com/en-us/library/windows/apps/hh466340.aspx
和http://msdn.microsoft.com/en-us/library/windows/apps/hh466350.aspx
关于这些 CSS 属性的内容。
接下来,我们添加了 CSS 规则来将鼠标光标变为指针,并在用户的鼠标悬停在其中一个元素上时显示白色边框。因为我们用-ms-grid-row-span
属性指定图像将跨越两行,并使用-ms-grid-row
属性将覆盖图放在第二行,所以覆盖图将放在图像底部的顶部。当我们在这里将背景颜色设置为半透明的深蓝色时,当各种primaryMenuItem
和secondaryMenuItem
CSS 规则被定义后,覆盖的确切大小和位置将被确定。
注意注意这个例子中
-ms-grid-rows
和-ms-grid-row
的区别。前者应用于网格容器,以定义网格将有多少行。后者应用于网格中的一个项目,以指示该项目的放置行。同样的建议也适用于-ms-grid-columns
和-ms-grid-column
CSS 属性。
我之前提到过,虽然我们所有的菜单项都应用了mainMenuItem
CSS 类,但是它们也有primaryMenuItem
类或者secondaryMenuItem
类。到目前为止,我在本节中介绍的所有 CSS 规则都适用于我们所有的菜单选项,不管它们是大还是小。您会注意到,我们还没有为任何菜单选项指定任何大小。为了解决这个问题,我们为primaryMenuItem
类和secondaryMenuItem
类添加了一组相似的 CSS 规则。清单 9-17 包含一段 CSS 代码,取自清单 9-16 ,它定义了大菜单选项的各种尺寸。
清单 9-17。 CSS 覆盖为大菜单选项
/* just the big menu button */
.homepage .primaryMenuItem {height: 408px;width: 408px;-ms-grid-rows: 1fr 70px;
}.homepage .primaryMenuItem .mainMenuItem-image {height: 382px;width: 382px;margin: 10px;}.homepage .primaryMenuItem .mainMenuItem-overlay .mainMenuItem-title {font-size: 2.5em;}
您可以看到,这是我们指定网格行大小的地方,表明第二行是70px
高,第一行填充剩余的空间。图像和标题文本的大小也被设置为适合大菜单选项的值。回头参考清单 9-16 ,你会看到小菜单选项非常相似的 CSS 规则。我不会在这里详细介绍它们,因为除了定义更小的维度之外,它们与清单 9-17 中的几乎相同。
如果您再次运行 Clok,您将会看到我们所有的菜单选项都已就位(参见图 9-17 )。当然,没有一个菜单选项可以使用,但是我们会在第十章中看到如何开始添加一些功能。
图 9-17 。我们的菜单选项,每个都用 CSS 网格布局定义
注意你可能认为向项目添加文档和照片并不完全符合时间跟踪功能。你说得对,但它是一个有用的功能,原因有很多,比如为费用报告记录收据。更重要的是,它给了我们一个在应用中处理文件的理由。我们将在第十六章和第二十二章中进一步探讨这个问题。
结论
我们在这一章中涉及了很多内容。我们为 Clok 创建了一些高级需求,Clok 是我们将在本书剩余部分构建的示例应用。我们创建了应用的整体外观,每当我们向 Clok 添加一个新的PageControl
时,它就会自动应用。我们添加了两个SettingsFlyout
控件,当我们添加用户可以修改的选项时,它们将在整本书中更新。最后,我们使用新的 CSS flexbox 和网格布局添加了应用主页的所有用户界面元素 Clok 仪表板。Clok 还不做任何事情,但它开始看起来像一个真正的应用。我们将在第十章中开始添加一些基本功能,包括一些有用的动画,以便在用户执行某些任务时为他们提供视觉反馈。`
十、过渡和动画
微妙的动画存在于整个 Windows 8。当切换到开始屏幕时,磁贴会放大以填充熟悉的网格,它们会对被点击或触摸做出反应。激活的搜索或设置窗格像抽屉一样从屏幕一侧滑出。同样,AppBar
控件从屏幕的顶部或底部滑入。我用了微妙这个词来描述这些动画。我的意思是,在日常使用中,你更有可能注意到动画是否已经被删除,而不是它们最初的存在。
我们当然可以构建一个没有动画的应用,但是许多好的应用利用动画为用户提供关于正在发生的变化或他或她刚刚发起的动作的直观反馈。好的动画很短,发生的很快。此外,它们经常模拟一些真实世界的运动,例如当按钮被按下时,它看起来会移开。在应用中添加动画时要记住的一点是,动画不应该分散用户对应用主要目的的注意力。
转场和动画:有什么不同
到目前为止,我在一般意义上使用单词 animation 来表示“屏幕上正在移动的东西”,但是这个术语在技术上是不正确的。实际上,Windows Store 应用可以定义两种不同类型的动作:CSS 过渡和 CSS 动画,或者简单地说,过渡和动画。这两者在很多方面都很相似。转场和动画都会在一段时间内在屏幕上产生运动,因为它们会修改应用中 HTML 元素的 CSS 属性,如大小、颜色、旋转、位置、透视和透明度。
不过,转场和动画在一些方面有所不同。最显著的区别是动画可以定义关键帧,这使您可以更好地控制动画元素的 CSS 属性如何随时间变化。例如,通过定义关键帧,单个动画可以将元素的颜色从白色更改为黄色,然后再更改为红色,最后重置回白色。在本章的后面你会看到一个类似的例子。
动画允许您在动画的不同点指定各种 CSS 属性的值。另一方面,转换不定义 CSS 属性的值,但是定义 CSS 属性如何在原始值和更改后的值之间转换。例如,我们可以使用一个过渡来指示任何时候我们改变一个元素的位置,它应该缓慢地进入和退出(开始缓慢移动,然后在减速到停止之前加速)或者以恒定的速度从开始位置移动到新的位置。
实际上,动画通常用于提供一些反馈,在完成时将 CSS 属性重置为原始状态。另一方面,过渡不会自动将 CSS 属性重置为其原始状态。因此,如果我们使用一个过渡将一个元素的颜色从白色变为黄色再变为红色,那么这个元素将一直保持红色,直到我们将它变回白色。
动画(和过渡)的方法
那么,我们如何使我们的应用的元素动画化呢?正如软件开发中常见的那样,有许多方法可以实现这一点。我将在本章中介绍四种不同的技术,如下所示:
- 纯粹在 CSS 中
- 在我们的 JavaScript 代码中使用内置于 WinJS 动画库中的动画
- 使用 JavaScript 以编程方式操作我们的 CSS
- 用 JavaScript 定义我们自己的过渡和动画,并在我们的 JavaScript 代码中执行它们
纯 CSS 动画
最后,我将在本章中介绍的所有动画和过渡都是 CSS 动画和 CSS 过渡。屏幕上的运动或变化是更改 CSS 属性并允许客户端以平滑的方式呈现从一个值到另一个值的更改的结果。虽然我将介绍一些从 JavaScript 代码中启动这些动画和过渡的技术,但是一些简单但有用的动画可以直接在我们的 CSS 中定义。
我们将通过在home.css
中定义关键帧,为我们在第九章的中用 Clok 创建的计时器添加一个动画。关键帧允许我们在一个特殊的@keyframes
CSS 规则中定义动画中许多中间步骤的 CSS 规则,它包含我们正在定义的每个步骤或帧的规则。将清单 10-1 中的代码从添加到home.css
的末尾。
清单 10-1。 在 CSS 中定义关键帧
@keyframes animateTimeIn {from, to {color: rgba(255, 255, 255, 1);}50% {color: rgba(255, 255, 0, 0.5);}
}
首先要注意的是,我们已经将我们的@keyframes
规则命名为animateTimeIn
,以便我们以后可以引用它。我们的初始状态在from
规则中定义,我们的最终状态在to
规则中定义。因为这两者是相同的,我们可以声明一次规则,用逗号分隔规则名。这正是我们在清单 10-1 中所做的,在那些情况下设置前景色为白色。我们已经创建了另一个规则,它将在动画进行到一半时将颜色更改为半透明的黄色。
注意类似于我们在
50%
定义动画中点的方式,你也可以用0%
定义初始状态,用100%
定义最终状态。这些值分别相当于from
和to
。
到目前为止,我们已经定义了当动画出现时什么值会改变,但是在我们的 CSS 中还没有提到我们的计时器。为了将动画附加到我们的计时器上,我们必须将清单 10-2 中突出显示的代码添加到home.css
中。
清单 10-2。 将我们的关键帧动画应用到计时器
.homepage section[role=main] #timeEntry #elapsedTime #elapsedTimeClock {font-size: 8em;animation: animateTimeIn 750ms ease-in-out 1s 2 normal;
}
通过指定我们在清单 10-1 中定义的名称,将animation
CSS 属性添加到该规则中允许我们确定我们想要将哪个动画应用到elapsedTimeClock
元素。我们的动画将在 750 毫秒的时间内渐入渐出。它将在 1 秒钟的延迟后启动,并将重复两次。这个快捷语法允许我们在一行中定义动画属性。我们也可以单独设置这些属性(见清单 10-3 )。
清单 10-3。 长格式相当于清单 10-2 中的
.homepage section[role=main] #timeEntry #elapsedTime #elapsedTimeClock {font-size: 8em;animation-name: animateTimeIn;animation-duration: 750ms;animation-timing-function: ease-in-out;animation-delay: 1s;animation-iteration-count: 2;animation-direction: normal;
}
注意关于这些和其他 CSS 动画属性的更完整的描述可以在 MSDN 的
http://msdn.microsoft.com/en-us/library/hh673530.aspx
找到。
当您现在启动应用时,定时器控件将在白色和黄色之间交替两次(参见图 10-1 )。目前,这不是很有用,因为我们在每次应用启动时都显示动画。然而,想象一个场景,我们启动 Clok,计时器已经在运行,或者计时器没有运行,但是有一个值还没有保存。在那些独特的情况下,使用这个动画或类似的动画,可能是提醒用户应用当前处于“进行中”状态的一种微妙方式。
图 10-1 。我们的计时器处于初始状态(上图)和修改后的颜色(下图)。底部图像中的颜色与我们应用中的颜色不同。它已经变暗,以增加印刷书籍中的对比度
纯 CSS 过渡
除了在 CSS 中定义动画,您还可以定义过渡。值得注意的是,CSS 转场本身不会在屏幕上产生运动。例如,您不能使用transition
属性指定新的颜色。转换实际上做的是定义目标元素如何从它的当前样式改变到它的新样式。这意味着您必须为目标元素定义两套 CSS 规则:转换的开始和结束。
因为我们有三个尚未实现的菜单选项,我们将添加一个当用户将鼠标悬停在它们上面时改变它们的过渡。我们要做的第一件事是向我们还没有实现的菜单选项添加另一个 CSS 类。通过将notImplemented
添加到三个较小菜单选项的class
属性来修改home.html
(参见清单 10-4 中突出显示的代码)。
清单 10-4。 标记菜单选项为未实现
<div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem notImplemented"><img class="mainMenuItem-image" src="/img/Camera.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title">Camera</h4></div>
</div>
<div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem notImplemented"><img class="mainMenuItem-image" src="/img/Projects.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title">Projects</h4></div>
</div>
<div id="timesheetMenuItem" class="mainMenuItem secondaryMenuItem notImplemented"><img class="mainMenuItem-image" src="/img/Timesheet.png" /><div class="mainMenuItem-overlay"><h4 class="mainMenuItem-title">Time Sheets</h4></div>
</div>
既然我们已经指出了哪些菜单选项应该应用转换,我们必须定义转换完成后将生效的 CSS 规则。将清单 10-5 中的 CSS 代码添加到home.css
中。
清单 10-5。 为未实现的菜单选项添加 CSS
/* buttons that haven't been implemented yet */
.homepage .mainMenuItem.notImplemented:hover {cursor: default;border: 2px solid transparent;background: rgba(50,50,50,0.65);background-image: url('/img/Thumb-Down.png');
}.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-image {visibility: hidden;}.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-overlay {background: rgba(35,35,35,0.65);}.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-overlay .mainMenuItem-title {display: none;}.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-overlay::after {content: 'Coming Soon';}
如果你现在运行 Clok,任何时候你将鼠标悬停在三个小菜单选项中的一个上,背景色就会变成灰色,图标就会变成“拇指朝下”图标。此外,描述性文本将变为“即将推出”一旦您移开鼠标,菜单选项将恢复正常状态。图 10-2 显示了时间表按钮的两种状态。
图 10-2 。未实现的菜单选项的正常状态(左)和同一菜单选项的悬停状态(右)
当您运行应用并将鼠标移到 Time Sheets 选项上时,您可能会注意到,虽然样式发生了变化,但这是一个突然的变化。这是因为,到目前为止,我们只定义了 CSS 的最终状态,但是我们还没有指出 CSS 应该如何从初始状态转换到最终状态。让我们给home.css
再加一行代码。清单 10-6 中突出显示的代码行将使背景颜色从默认的蓝色逐渐过渡到灰色。
清单 10-6。 设置初始 CSS 规则和悬停状态规则之间的短暂转换
.homepage .mainMenuItem.notImplemented:hover {cursor: default;border: 2px solid transparent;background: rgba(50,50,50,0.65);background-image: url('/img/Thumb-Down.png');transition: background 500ms ease-in-out 0s;
}
我鼓励您在继续之前先尝试一下 CSS。例如,您可能还想给覆盖图的背景色添加一个过渡,因为目前,它仍然会立即从深蓝变为深灰。CSS transition
属性和其他相关属性的文档可以在http://msdn.microsoft.com/en-us/library/hh673535.aspx
找到。
WinJS 动画库
当你使用 Windows 8 时,你会发现有许多常见的动画。WinJS 通过一个动画库提供了其中的许多内容,我们可以在我们的应用中使用它们来提供与其他 Windows 应用商店应用以及 Windows 本身一致的视觉反馈。例如,有一些预定义的动画,用于向ListView
控件添加一个项目,用于淡入或淡出视图,以及用于让一个元素对被点击或触摸做出反应。当用户按下一个按钮时,我们将使用这个库来激活菜单选项。用来自清单 10-7 的代码替换home.js
中的页面定义。
清单 10-7。 添加指针动画
WinJS.UI.Pages.define("/pages/home/home.html", {ready: function (element, options) {this.initializeMenuPointerAnimations();},initializeMenuPointerAnimations: function () {var buttons = WinJS.Utilities.query(".mainMenuItem");buttons.listen("MSPointerDown", this.pointer_down, false);buttons.listen("MSPointerUp", this.pointer_up, false);buttons.listen("MSPointerOut", this.pointer_up, false);},pointer_down: function (e) {WinJS.UI.Animation.pointerDown(this);e.preventDefault();},pointer_up: function (e) {WinJS.UI.Animation.pointerUp(this);e.preventDefault();},
});
在新的initializeMenuPointerAnimations
方法中,我们找到了所有的菜单选项按钮——那些带有mainMenuItem
CSS 类的 HTML 元素。对于我们找到的每个项目,我们监听MSPointerDown
和MSPointerUp
事件,它们代表鼠标或触摸交互。我们创建了两个名为pointer_down
和pointer_up
的函数,通过调用 WinJS 动画库中的适当方法WinJS.UI.Animation.pointerDown
或WinJS.UI.Animation.pointerUp
来激活被点击或触摸的项目,从而处理这些事件。如果你仔细观察图 10-3 ,你可以看到当按钮被按下时,它的尺寸会稍微缩小,给人一种被推开的感觉。在与应用交互时,效果会明显得多。
图 10-3 。推开我们的按钮
此外,我们正在处理MSPointerOut
事件,就像它是一个MSPointerUp
事件一样。如果我们忽略该事件,很容易使菜单选项停留在按下状态,例如,单击它并在释放鼠标按钮之前将鼠标滑离它。
我们在这里只介绍了动画库中的两个动画。还有其他几个可用的,你可以在http://msdn.microsoft.com/en-us/library/windows/apps/br229780.aspx
的 MSDN 上了解更多。
使用 JavaScript 操作 CSS
正如我们在前面几节中看到的,直接在 CSS 中配置动画和过渡很简单,使用 WinJS 动画库也一样简单。但是,有时候你需要更多一点的控制。例如,您可能希望让用户指定在上面的动画中使用的颜色。他们可能会选择橙色,而不是黄色。或者他们可以选择#E3A238
。或者,您可能希望在动画或过渡完成后运行一些代码,这正是我们在本节中要做的。
我们将向 Clok 添加一个 CSS 转换,当用户保存他们的时间条目时,就会触发这个转换。在这种情况下,动画可能是不必要的,但它有助于向用户提供反馈,尤其是那些通过触摸与 Clok 交互的用户。如果我们简单地保存数据并重置表单,用户可能不会确信他们的数据已经保存,因为这在视觉上看起来与他们放弃时间输入时一样。为了提供清晰的反馈,我们将使用 CSS 转换来缩小表单,并使其向时间表菜单选项移动,以表明他们的条目已经保存到他们的时间表中。图 10-4 显示了用户输入一些注释并按下保存按钮后的时间输入表单。让我们看看在他或她按下保存按钮后,我们需要添加什么来使有趣的事情发生。
图 10-4 。一个 Clok 用户正在保存她的时间条目
首先要做的事情:让计时器滴答作响
在我们添加代码来添加这个转换之前,我们有一些设置工作要做。用清单 10-8 中突出显示的代码更新home.js
中的页面定义。这段代码并不特定于我们将要添加的转换,所以我不会详细讨论它,但是它是配置基本表单行为所必需的。当你滚动这段代码时,你会看到熟悉的处理事件和改变控件的值和状态的概念,我在第八章的中讨论过,当时我们创建了自定义时钟控件。您将看到当 Clok 用户按下 Start Clok 菜单选项时启动计时器的代码,然后当他按下 Stop Clok 菜单选项时停止计时器。有一些逻辑可以防止用户在没有选择项目的情况下保存条目,以及防止用户保存或丢弃没有经过时间的计时器。目前,保存按钮和放弃按钮都只是重置表单。虽然这是丢弃按钮的正确动作,但我们还是给自己留了一个注释,记录用户按下保存按钮时的时间输入。我将在第十二章中介绍这一点。
清单 10-8。 准备我们的 JavaScript 来处理时间输入表单事件
WinJS.UI.Pages.define("/pages/home/home.html", {ready: function (element, options) {this.initializeMenuPointerAnimations();toggleTimerMenuItem.onclick = this.toggleTimerMenuItem_click.bind(this);project.onchange = this.project_change.bind(this);saveTimeButton.onclick = this.saveTimeButton_click.bind(this);discardTimeButton.onclick = this.discardTimeButton_click.bind(this);this.setupTimerRelatedControls();},initializeMenuPointerAnimations: function () {var buttons = WinJS.Utilities.query(".mainMenuItem");buttons.listen("MSPointerDown", this.pointer_down, false);buttons.listen("MSPointerUp", this.pointer_up, false);buttons.listen("MSPointerOut", this.pointer_up, false);},pointer_down: function (e) {WinJS.UI.Animation.pointerDown(e.srcElement);e.preventDefault();},pointer_up: function (e) {WinJS.UI.Animation.pointerUp(e.srcElement);e.preventDefault();},timerIsRunning: false,toggleTimerMenuItem_click: function (e) {this.toggleTimer();},project_change: function (e) {this.enableOrDisableButtons();},discardTimeButton_click: function (e) {this.discard();},saveTimeButton_click: function (e) {this.save();},save: function () {// TODO: save the time entrythis.resetTimer()},discard: function () {this.resetTimer()},toggleTimer: function () {this.timerIsRunning = !this.timerIsRunning;this.setupTimerRelatedControls();},resetTimer: function () {this.timerIsRunning = false;elapsedTimeClock.winControl.reset();project.selectedIndex = 0;timeNotes.value = "";this.setupTimerRelatedControls();},setupTimerRelatedControls: function () {if (this.timerIsRunning) {elapsedTimeClock.winControl.start();timerImage.src = "/img/Clock-Running.png";timerTitle.innerText = "Stop Clok";} else {elapsedTimeClock.winControl.stop();timerImage.src = "/img/Clock-Stopped.png";timerTitle.innerText = "Start Clok";}this.enableOrDisableButtons();},enableOrDisableButtons: function () {if ((project.value !== "")&& (!this.timerIsRunning)&& (elapsedTimeClock.winControl.counterValue > 0)) {saveTimeButton.disabled = false;} else {saveTimeButton.disabled = true;}discardTimeButton.disabled = (this.timerIsRunning)|| (elapsedTimeClock.winControl.counterValue <= 0);},
});
用 JavaScript 添加 CSS 过渡
如果你现在运行 Clok,这个应用可能会像你预期的那样运行。您可以启动和停止计时器,并保存有效的时间条目。现在让我们看看清单 10-9 中的来找到我们需要添加到我们的save
方法中来触发转换的代码。
清单 10-9。 我们更新了保存方法
save: function () {// TODO: save the time entrytimeEntry.style.transition = 'color 5ms ease 0s, '+ 'transform 500ms ease 0s, opacity 500ms ease 0s';timeEntry.style.transform = 'scale3d(0,0,0)';timeEntry.style.opacity = '0';timeEntry.style.color = '#00ff00';timeEntry.style.transformOrigin = "-130px 480px";var self = this;var transitionend = function (e1) {if (e1.propertyName === "transform") {timeEntry.removeEventListener('transitionend', transitionend);self.resetTimer();}};timeEntry.addEventListener('transitionend', transitionend, false);
},
我们在这里添加的第一行代码是为了定义我们的转换。这一行将动画显示对color
、transform
或opacity
CSS 属性的任何更改,在指定的时间内逐渐改变每个属性、5ms
的属性、500ms
的属性、transform
和opacity
的属性。接下来,我们为这些属性中的每一个指定新的值,指示我们的时间输入表单应该收缩和褪色,同时将文本颜色更改为绿色,以表示成功。transformOrigin
属性允许我们指出转换发生的点。在这种情况下,我们已经指出过渡的中心在时间输入表单左上角的左侧 130 像素和下方 480 像素处。这些数字是根据我们之前为菜单选项定义的大小选择的,并将在时间表按钮的顶部设置过渡的原点。
接下来,我们创建一个名为transitionend
的内嵌函数来处理同名事件。正如您可能猜到的,当转换完成时会引发此事件。我们有三个同时发生的转换,color
、transform
和opacity
转换在完成时都会引发这个事件,每个都在不同的时间发生。我们的处理函数忽略了color
和opacity
完成事件,但是当transform
转换完成时,我们的处理函数重置表单并停止监听后续的转换完成事件。因为opacity
转换的持续时间与transform
转换的持续时间相同,所以监听那个转换是否完成是等效的。
注意如果这个转换是我们要添加到 Clok 中的唯一一个转换,我们就不需要像这样担心删除事件监听器。然而,如果不这样做,任何后续的转换也会触发
transitionend
事件处理程序,这可能会导致意想不到的结果。
现在,当我们运行 Clok 并保存我们的时间条目时,我们可以清楚地看到我们的时间条目被保存到我们的时间表中(见图 10-5 )。
图 10-5 。成功完成的时间输入表保存到我们的时间表
这一转变相当顺利,Clok 开始成为一个有用的小应用。不过,我们现在有一个小问题。一旦条目被保存,表单就消失了。如果用户想为另一个项目记录时间,我们可以让他关闭 Clok 并重新启动它,但这将是一个非常糟糕的体验。幸运的是,就像我们将时间输入表单动画化一样容易,我们可以将表单重置为初始状态。清单 10-10 就是这么做的。我添加了一个新的resetTimerStyles
方法来将所有的样式重置回它们的初始值,并清除过渡。然后我从现有的resetTimer
方法中调用这个方法。
清单 10-10。 把东西放回原处
resetTimer: function () {this.timerIsRunning = false;elapsedTimeClock.winControl.reset();project.selectedIndex = 0;timeNotes.value = "";this.resetTimerStyles();this.setupTimerRelatedControls();
},resetTimerStyles: function () {timeEntry.style.transition = 'none';timeEntry.style.transformOrigin = "50% 50%";timeEntry.style.transform = 'scale3d(1,1,1)';timeEntry.style.opacity = '1';timeEntry.style.color = '#ffffff';
},
executeTransition 和 executeAnimation 方法
在上一节中,我们看到了如何通过修改想要制作动画的元素的各种 CSS 样式属性来创建过渡。这非常方便和简单。然而,还有一些事情需要记住。在易访问控制面板中,Windows 8 允许用户禁用不必要的动画(参见图 10-6 )。有些用户可能会关闭动画,因为启用动画时,他们使用的计算机会变慢。其他人这样做可能只是因为他们不想被动画分散注意力。不管是什么原因,如果动画(或过渡)对应用的功能并不重要,你应该尊重用户的选择,不要启动动画。
图 10-6 。禁用不必要的动画
那么我们如何检查这个值呢?Windows 运行时(WinRT)定义了一个我们可以使用的类。Windows.UI.ViewManagement.UISettings
类提供了一种简单的方法来访问一些常见的用户界面设置(参见清单 10-11 )。一旦我们有了这个类的实例,我们可以检查一个名为animationsEnabled
的属性,它直接对应于图 10-6 中的设置。
清单 10-11。 检查用户偏好的例子
var uiSettings = new Windows.UI.ViewManagement.UISettings();
if (uiSettings.animationsEnabled) {// perform animation or transitionmyElement.style.transition = "opacity 500ms ease 0s";timeEntry.style.opacity = "0.5";
}
此外,WinJS 提供了一个函数WinJS.UI.isAnimationEnabled
,它检查该设置,并结合一些其他标准,确定是否应该出现动画。确定isAnimationEnabled
值的标准的描述可以在 MSDN: http://msdn.microsoft.com/en-us/library/windows/apps/hh779793.aspx
上找到。isAnimationEnabled
函数由动画库和ListView
控件在内部使用,它为您提供保持动画一致性所需的信息。虽然纯粹用 CSS 声明动画是不可能的,清单 10-12 给出了一个假设的例子,说明我们如何修改清单 10-9 中的代码,在开始转换之前检查这个函数。
清单 10-12。 假想改变我们的保存方法
save: function () {if (WinJS.UI.isAnimationEnabled()) {// SNIPPED}
},
这种做法的缺点是,每当使用 JavaScript 定义自定义 CSS 过渡或 CSS 动画时,您都必须检查该函数。我提到过动画库在内部检查这个方法,所以我们之前创建的pointerDown
和pointerUp
动画在创建动画时会自动考虑控制面板设置。如果能够为我们声明自动检查isAnimationEnabled
功能的自定义动画和过渡,将会非常方便。
幸运的是,这是可能的。动画库在内部使用两个 WinJS 方法来执行过渡和动画。对isAnimationEnabled
函数的检查发生在这两个方法中,并且它们也被公开,供我们在自己的应用中使用。我们可以使用WinJS.UI.executeTransition
和WinJS.UI.executeAnimation
分别设置一个或多个过渡和动画,在页面中的特定元素上执行。
我们来看一个例子。在上一节中,我们添加了一个转换,以便在用户保存时间条目时向她提供反馈。在这一节中,我们还将为丢弃按钮添加一个过渡。将表格动画条目保存到时间表菜单选项中。当我想到丢弃一些东西,比如一个空水瓶,我会想象自己把它扔进回收站。在 Clok 中我们没有回收站的概念;然而,我们可以创建一个扔掉某物的类比。与保存动画一样,我们将让我们的丢弃动画缩小表单,但不是向菜单选项显示动画,我们只是让它在缩小到背景中时旋转,而不是变成绿色来指示成功,我们将让文本变成红色来指示我们正在删除该条目。用清单 10-13 中突出显示的代码更新home.js
中的discard
方法。
清单 10-13。 一种新的丢弃方法
discard: function () {var self = this;var slideTransition = WinJS.UI.executeTransition(timeEntry,[{property: "transform",delay: 0,duration: 500,timing: "ease",from: "rotate(0deg) scale3d(1,1,1)",to: "rotate(720deg) scale3d(0,0,0)"},{property: "opacity",delay: 0,duration: 500,timing: "ease",from: 1,to: 0},{property: "color",delay: 0,duration: 5,timing: "ease",from: '#ffffff',to: '#ff0000'}]).done(function () { self.resetTimer(); });
},
这里我们使用executeTransition
方法在我们的timeEntry
元素上执行三种不同的转换。
- 我们通过将旋转角度从 0 度转换到 720 度来旋转表单两次。
- 我们正在通过改变它的不透明度来淡化表单。
- 我们将文本颜色从白色改为红色。
然后,一旦转换完成,计时器和表单被重置。结果是,Clok 现在给用户反馈,确认当她按下放弃按钮时,我们已经有意清除了表单(见图 10-7 )。
图 10-7 。丢弃动画
结论
在这一章中,你已经看到了向用户提供可视化、动画反馈的多种方式。虽然过多的动画会分散注意力,但是代表用户已经执行的动作的微妙的动画可以向我们的用户提供信心,我们的应用已经如预期的那样运行了。
我们选择的动画技术包括纯粹用 CSS 定义的简单动画,使用 WinJS 动画库中几个预定义的动画之一,使用 JavaScript 以编程方式修改与元素相关联的 CSS 样式,或者使用 WinJS 中的低级executeTransition
或executeAnimation
方法。虽然在我们的应用中,每一个都有它的位置,但是我们应该意识到用户可能因为这样或那样的原因不喜欢看到不必要的动画,我们应该让这个事实影响我们决定使用哪种动画方法。