HTML5 和 JavaScript 的 Windows8 开发高级教程(五)
十五、使用ListView控件
在这一章中,我描述了ListView控件,这是另一个 WinJS 数据驱动的 UI 控件。与上一章的 FlipView 控件有一个共同的基础,但是ListView显示多个项目,并在如何实现这一点上提供了一些灵活性。在这一章中,我解释了可以使用的不同种类的模板和布局,描述了被调用项和被选择项之间的区别,以及如何处理ListView控件发出的各种事件。我还将向您展示如何使用描述数据源的抽象,这样您就可以编写适用于任何数据源的代码,而不仅仅是那些使用WinJS.Binding.List对象创建的数据源。表 15-1 对本章进行了总结。
何时使用 ListView 控件
ListView是一个非常灵活的控件,对于如何使用它没有真正的限制。简而言之,如果您想向用户呈现多个项目,那么ListView可能是最好的 WinJS UI 控件。但是,您必须确保用户能够容易地找到特定的项目或项目组。ListView非常适合显示项目,包括大型数据集,但是它很容易被冲昏头脑,呈现给用户一大堆可供选择的项目。对于大型数据集,考虑为用户提供搜索或过滤项目的工具,或者实现基于语义缩放控件的布局,我在第十六章的中对此进行了描述。
添加 ListView 示例
我将从描述ListView控件的基本特性开始,然后在这个例子的基础上演示一些更高级的选项和特性。虽然ListView控件只做一件事(向用户呈现多个项目的列表),但是有很多排列和配置选项。首先,我在/js/viewmodel.js文件中定义了一个数据源,如清单 15-1 所示。
清单 15-1 。ListView 示例的 viewmodel.js 文件中的附加内容
`(function () { "use strict";
WinJS.Namespace.define("ViewModel", { data: { images: new WinJS.Binding.List([ { file:img/aster.jpg", name: "Aster"}, { file:img/carnation.jpg", name: "Carnation"}, { file:img/daffodil.jpg", name: "Daffodil"}, { file:img/lily.jpg", name: "Lilly"}, ]),
extraImages: [{ file:img/orchid.jpg", name: "Orchid"},
{ file:img/peony.jpg", name: "Peony"},
{ file:img/primula.jpg", name: "Primula"},
{ file:img/rose.jpg", name: "Rose"},
{ file:img/snowdrop.jpg", name: "Snowdrop" }], ** letters: new WinJS.Binding.List(),**
},
});
** var src = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",** ** "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];** ** src.forEach(function (item, index) {** ** ViewModel.data.letters.push({** ** letter: item,** ** group: index % 3** ** });** ** });**
})();`
我创建了一个新的WinJS.Binding.List对象,包含了字母表中每个字母的对象。每个对象都有一个letter属性,它返回对象对应的字母,还有一个group属性,我用它将对象分配到三个数字组中的一个。在本章的后面,我将在显示项目时使用letter属性的值,在描述将相关项目分组的ListView特性时使用 group 属性。
定义列表视图 HTML
为了演示ListView控件,我在 Visual Studio 项目的pages文件夹中添加了一个名为ListView.html的新文件。你可以在清单 15-2 中看到这个文件的内容。
清单 15-2 。ListView.html 文件的初始内容
`
** ** ** **
** **
** **
`
通过将data-win-control属性设置为WinJS.UI.ListView,将ListView控件应用于div元素。当使用ListView控件时,使用itemDataSource和itemTemplate属性设置数据源和用于显示数据项的模板,就像使用FlipView控件一样。
ListView控件没有我在第十四章的中为FlipView描述的第一个图像问题,所以你可以使用data-win-options属性安全地声明设置itemDataSource和itemTemplate属性(尽管你也可以编程地设置这些值,并且如果你喜欢的话,使用一个函数来生成你的模板元素——细节和例子参见第十四章)。在我的清单中,我使用添加到viewmodel.js文件中的字母相关对象的List作为数据源,使用在ListView.html文件中定义的WinJS.Binding.Template控件作为模板。
定义 CSS
我已经把这个例子的 CSS 放到一个名为/css/listview.css的文件中,你可以在清单 15-3 中看到它的内容。在这个 CSS 中没有新的技术,所有的风格都是简单和标准的。
清单 15-3 。listview.css 文件的内容
#list {width: 500px;height: 500px;} *.listItem {width: 100px;} *.listData {background-color: black; text-align: center; border: solid medium white;font-size: 70pt;} .listTitle {position: absolute; background-color: rgba(255, 255, 255, 0.6); color: black; bottom: 3px;font-size: 20pt; width: 86px; padding-left: 10px; padding-top: 20px;font-weight: bold;} *.invoked {color: red;} *.invoked .listData {font-weight: bold;} *.invoked .listTitle {background-color: transparent} #midPanel, #rightPanel { -ms-flex-pack: start; } #list .win-container {background-color: transparent;}
定义 JavaScript
我已经把这个例子的 JavaScript 放到了一个名为/js/pages/listview.js的文件中,你可以在清单 15-4 中看到这个文件的内容。
清单 15-4 。listview.js 文件的初始内容
`(function () {
WinJS.UI.Pages.define("/pages/ListView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({
layout: "Grid",
groups: false, groupHeaderPosition: "top",
maxRows: 3,
ensureVisible: null,
searchFor: null,
});
** Templates.createControls(midPanel, list, "listView1", proxyObject);** ** Templates.createControls(rightPanel, list, "listView2", proxyObject);** } }); })();`
您会注意到,我对清单中的Templates.createControls方法进行了两次调用。当我为这个例子定义 HTML 时,我为配置控件添加了一个额外的容器元素,如下所示:
`...
...`在这一章中,我需要太多的配置控件来将它们放入模拟器的标准分辨率屏幕上的一个容器中,所以我将元素分成了两个容器,因此,需要对createControls方法进行两次调用。你可以在清单 15-5 中看到我添加到这个例子的controls.js文件中的两组定义控件。
清单 15-5 。ListView 控件的定义对象
`... listView1: [ { type: "select", id: "layout", title: "Layout", values: ["Grid", "List"], useProxy: true }, { type: "toggle", id: "groups", title: "Groups", useProxy: true, value: false }, { type: "select", id: "groupHeaderPosition", title: "Group Position", values: ["top", "left"], labels: ["Top", "Left"], useProxy: true }, { type: "input", id: "maxRows", title: "Max Rows", value: 3, useProxy: true }, { type: "span", id: "invoked", value: "Invoke an Item", title: "Invoked" }, { type: "span", id: "selected", value: "Select an Item", title: "Selected" }],
listView2: [ { type: "select", id: "tapBehavior", title: "tapBehavior", values: ["directSelect", "toggleSelect", "invokeOnly", "none"] }, { type: "select", id: "selectionMode", title: "selectionMode", values: ["multi", "single", "none"] }, { type: "input", id: "ensureVisible", title: "EnsureVisible", value: "", useProxy: true }, { type: "input", id: "searchFor", title: "Search For", value: "", useProxy: true }, { type: "span", id: "itemCount", value: 26, title: "Count" }, { type: "buttons", labels: ["Add Item", "Delete Item"] }], ...`
正如我前面提到的,ListView控件非常灵活,这反映在我演示最重要的特性所需的配置控件的数量上。
最后,我需要确保用户可以从导航条导航到ListView.html文件,所以我对templates.js文件进行了添加,如清单 6 所示。
清单 15-6 。确保可以从导航栏访问 ListView.html 文件
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, { name: "Flyout", icon: "\u0036" }, { name: "Menu", icon: "\u0037" }, { name: "MessageDialog", icon: "\u0038" }, { name: "FlipView", icon: "pictures" }, ** { name: "Listview", icon: "list" },** ]; ...
使用列表视图控件
如果此时运行 app 并通过导航栏导航到ListView.html文件,你会看到如图图 15-1 所示的布局。在左侧面板中是ListView控件,它显示了我添加到viewmodel.js文件中的字母数据源中的项目。另外两个面板包含配置控件,我将使用它们来演示不同的ListView特性。
图 15-1。【ListView.html 文件的布局
在接下来的部分中,我将解释ListView功能的不同方面,并演示使用ListView控件显示数据项的不同方式。
选择布局
ListView可以使用两种不同的布局显示数据项。默认情况下,ListView控件在一个网格中显示来自数据源的项目,如图 1 所示。请注意数据项的显示顺序。网格中的每一列都是从上到下填充的,形成了垂直优先的布局。如果数据源中的项目多于ListView控件占据的屏幕空间,则使用水平滚动。为了使布局对用户更明显,当用户将鼠标移动到ListView控件上或通过触摸交互向左或向右滑动时,会显示一个水平滚动条。
可以使用的另一种布局是垂直列表。您可以使用示例右侧面板中的第一个select元素在网格和列表之间切换,我将它标记为Layout。在清单 15-7 中,你可以看到我添加到/js/pages/listview.js文件中的代码,它将select元素链接到ListView控件。
清单 15-7 。切换 ListView 控件的布局
`(function () {
WinJS.UI.Pages.define("/pages/ListView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ layout: "Grid", groups: false, groupHeaderPosition: "top", maxRows: 3, ensureVisible: null, searchFor: null, });
Templates.createControls(midPanel, list, "listView1", proxyObject); Templates.createControls(rightPanel, list, "listView2", proxyObject);
** proxyObject.bind("layout", function (val) {** ** list.winControl.layout = val == "Grid" ?** ** new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout();** ** });** } }); })();`
ListView控件定义了layout属性,这就是我在清单中所做的更改。您将该属性设置为一个对象——如果您想要网格,则为一个WinJS.UI.GridLayout对象;如果您想要垂直列表,则为一个WinJS.UI.ListLayout对象。
在图 15-2 中可以看到切换到List布局的效果。图中的布局看起来有点奇怪,因为ListView控件的大小适合显示多列。
***图 15-2。*使用列表布局显示元素
这是一个更加传统的垂直列表。我倾向于不像这样单独使用列表布局,但是它在创建语义缩放布局时非常有用,我在第十六章中对此进行了描述。
一个常见的错误是将layout属性设置为字符串形式的对象名称。这是行不通的——您需要使用new关键字来创建一个新对象,并将其赋值,就像我在清单中所做的那样。如果希望以声明方式设置布局,则必须在数据绑定中使用特殊的符号,如下所示:
... data-win-options="{layout: **{type: WinJS.UI.ListLayout}**}" ...
这个符号告诉ListView控件创建一个ListLayout对象的新实例。这是一种笨拙的语法,我倾向于通过在代码中设置布局来避免它。
注意原则上,你可以创建自己的布局对象来实现定制的布局策略,但是很难将
WinJS.UI.Layout对象中定义的基本功能和ListView控件对布局功能的假设分开。
设置网格的最大行数
GridLayout对象定义了maxRows属性,该属性对用于布局数据源中的项目的行数设置了上限。为maxRows属性设置一个值只会限制行数——例如,它不会强制网格占据您指定的行数。实际行数不同的主要原因是GridLayout永远不会使用垂直滚动。因此,除非有足够的垂直空间来完全容纳从模板生成的元素,否则不会添加新行。
为了演示这个特性,在标签为Max Rows的应用布局的中间面板中有一个input元素。在清单 15-8 中,您可以看到我添加到/js/pages/listview.js文件中的代码,该代码将输入该控件的值链接到maxRows属性。
清单 15-8 。向 listview.js 文件添加对 maxRows 特性的支持
`(function () {
WinJS.UI.Pages.define("/pages/ListView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ layout: "Grid", groups: false, groupHeaderPosition: "top", maxRows: 3, ensureVisible: null, searchFor: null, });
Templates.createControls(midPanel, list, "listView1", proxyObject); Templates.createControls(rightPanel, list, "listView2", proxyObject);
proxyObject.bind("layout", function (val) { list.winControl.layout = val == "Grid" ? new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout(); });
** proxyObject.bind("maxRows", function (val) {** ** list.winControl.layout.maxRows = val;** ** });** } }); })();`
这是GridLayout对象的一个特性,您创建它并将其设置为ListView控件的layout属性的值——它不是由ListView本身直接定义的一个特性。这意味着该功能仅在网格布局中起作用,并且您必须确保通过从布局属性返回的对象来设置值。如果你给一个ListLayout对象的maxRows属性赋值,不要担心——这不会有不好的影响,但是不会改变布局。您可以在图 3 中的Max Rows input元素中看到输入值2的效果。
***图 15-3。*为 maxRows 属性设置一个值
显示组
ListView控件能够以组的形式显示项目,其中组的细节通过一种特殊的数据源提供,这种数据源很明显叫做组数据源。正是为了准备这个特性,当我在本章前面设置示例应用时,我为我添加到数据列表中的对象定义了group属性。
当您使用WinJS.Binding.List对象时,创建一个组数据源非常简单——您只需调用createGrouped方法。你可以在清单 15-9 的中看到我对/js/viewmodel.js文件所做的添加,以调用这个方法。
清单 15-9 。从列表对象创建分组数据源
`(function () { "use strict";
WinJS.Namespace.define("ViewModel", { data: { images: new WinJS.Binding.List([ { file:img/aster.jpg", name: "Aster"}, { file:img/carnation.jpg", name: "Carnation"}, { file:img/daffodil.jpg", name: "Daffodil"}, { file:img/lily.jpg", name: "Lilly"}, ]),
extraImages: [{ file:img/orchid.jpg", name: "Orchid"}, { file:img/peony.jpg", name: "Peony"},
{ file:img/primula.jpg", name: "Primula"},
{ file:img/rose.jpg", name: "Rose"},
{ file:img/snowdrop.jpg", name: "Snowdrop" }],
letters: new WinJS.Binding.List(), ** groupedLetters: null,** }, });
var src = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; src.forEach(function (item, index) { ViewModel.data.letters.push({ letter: item, group: index % 3 }); });
** ViewModel.data.groupedLetters = ViewModel.data.letters.createGrouped(** ** function (item) { return item.group.toString(); },** ** function (item) { return "Group " + item.group; },** ** function (g1, g2) { return g1 - g2; }** ** );** })();`
我在名称空间ViewModel.data中定义了一个名为groupedLetters的新属性,并将来自List.createGrouped方法的结果赋给它。createGrouped方法返回一个新的List对象,其中的项目按组组织。createGrouped方法有三个函数,用于提供每个项目的分组信息。
第一个函数返回项目所属组的键。数据源中的每个项目都会调用它,并且您必须返回一个字符串。在我的例子中,我能够返回我为每个数据项定义的group属性的字符串值。
第二个函数为每组中的第一项调用一次。结果是要分配给组的文本描述。我对 group 属性的数值进行了简单的修改,因此我返回了名称Group 1、Group 2等等。请注意,系统会向您传递一个完整的数据项,因此您可以使用您选择的数据项的任何特征来生成组描述。
第三个函数用于对组进行排序。您被传递了两个组的密钥,要求您返回一个数值。返回值0表示这些组具有相同的等级,返回小于零的值表示第一组应该在第二组之前显示,返回大于零的值表示第二组应该在第一组之前显示。因为我的组合键是数字,所以我可以返回一个减去另一个的结果来得到我想要的效果。
应用组数据源
我将在常规数据源和组数据源之间切换,以响应中间面板中标有Groups的ToggleSwitch控件。您可以在清单 10 的中看到我添加到/js/pages/listview.js中的代码,当开关位置改变时,它会做出响应。
清单 15-10 。增加数据源之间切换的支持
`(function () {
WinJS.UI.Pages.define("/pages/ListView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ layout: "Grid", groups: false, groupHeaderPosition: "top", maxRows: 3, ensureVisible: null, searchFor: null, });
Templates.createControls(midPanel, list, "listView1", proxyObject); Templates.createControls(rightPanel, list, "listView2", proxyObject);
proxyObject.bind("layout", function (val) { list.winControl.layout = val == "Grid" ? new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout(); });
proxyObject.bind("maxRows", function (val) { list.winControl.layout.maxRows = val; });
** proxyObject.bind("groups", function (val) {** ** if (val) {** ** var groupDataSource = ViewModel.data.groupedLetters;** ** list.winControl.itemDataSource = groupDataSource.dataSource;** ** list.winControl.groupDataSource = groupDataSource.groups.dataSource;** ** } else {** ** list.winControl.itemDataSource = ViewModel.data.letters.dataSource;** ** list.winControl.groupDataSource = null;** ** }** ** });** } }); })();`
为了显示分组数据,我必须更新两个属性:itemDataSource和groupDataSource。itemDataSource属性用于获取将要显示的项目,而groupDataSource属性用于获取关于项目分组方式的信息。
第一步是设置itemDataSource属性,使其指向分组 List对象的dataSource属性。第二步是将groupDataSource设置为分组 List对象的groups.dataSource属性。
注意这导致了很多混乱,需要强调的是:对于*
itemDataSource和groupDataSource属性,必须使用分组的数据源。*
*按照与常规数据源相同的语法,itemDataSource属性被设置为List.dataSource属性。groupDataSource属性被设置为List.groups.dataSource属性——注意属性路径中添加了groups。设置了这两个属性后,ListView控件将分组显示数据源项,并显示每组的标题,如图 15-4 中的所示。
***图 15-4。*显示分组数据
为了更容易看到这些组,我在导航到ListView.html页面后,通过在JavaScript Console窗口中输入以下语句,临时调整了应用的布局:
rightPanel.style.display = "none"; list.style.width = "800px"
当切换回常规(未分组)数据源时,必须将groupDataSource属性设置为null。如果您不这样做,应用将抛出一个异常,因为数据源缺少将组与单个数据项关联起来所需的结构。
设置割台位置
如图 15-4 中的所示,项目组上方的每组都有一个标题。您可以通过由GroupLayout对象(而不是由ListView控件本身)定义的groupHeaderPosition属性来更改这个位置。中间面板中的select元素可以用来改变组标题的位置,你可以看到我添加到/js/pages/listview.js文件中的代码,当在清单 15-11 中选取一个新的select值时,它会做出响应。
清单 15-11 。增加了改变组头位置的支持
`(function () {
WinJS.UI.Pages.define("/pages/ListView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ layout: "Grid", groups: false, groupHeaderPosition: "top", maxRows: 3, ensureVisible: null, searchFor: null, });
Templates.createControls(midPanel, list, "listView1", proxyObject); Templates.createControls(rightPanel, list, "listView2", proxyObject);
proxyObject.bind("layout", function (val) { list.winControl.layout = val == "Grid" ? new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout(); });
proxyObject.bind("maxRows", function (val) { list.winControl.layout.maxRows = val; });
** proxyObject.bind("groupHeaderPosition", function (val) {** ** list.winControl.layout.groupHeaderPosition = val;** ** });**
proxyObject.bind("groups", function (val) { // ...code removed for brevity... }); } }); })();`
groupHeaderPosition属性支持的值是top(默认值)和left。你可以在图 15-5 的中看到left值如何改变布局。在组的顶部显示标题可以减少网格中的行数,所以当那些行更重要时left设置是有用的,即使网格本身会更宽并且需要更多的水平滚动。
***图 15-5。*在群组左侧显示群组标题
使用组页眉模板
默认情况下,ListView只是将组头显示为一个字符串,但是您可以通过提供一个定制模板来定制外观。但是,需要对数据源进行调整,以便组密钥可以与 WinJS 绑定模板一起使用。您可以在清单 15-12 中看到 UI 对/js/viewmodel.js文件中的组数据所做的更改。
清单 15-12 。更改组数据以支持使用标题模板
... ViewModel.data.groupedLetters = ViewModel.data.letters.createGrouped( function (item) { return item.group.toString(); }, function (item) { //return "Group " + item.group; ** return {** ** title: "Group " + item.group** ** };** }, function (g1, g2) { return g1 - g2; } ); ...
问题是 WinJS 数据绑定系统没有一种机制允许你引用传递给Template控件的render方法的数据对象。(有关如何使用数据绑定模板的详细信息,请参见第八章)。为了解决这个问题,我需要更改我传递给createGrouped方法的第二个函数的结果,以便它返回一个具有我可以在数据绑定中引用的属性的对象。
在清单 15-13 中,你可以看到我添加到ListView.html文件中的模板,以及我使用data-win-options属性中的groupHeaderTemplate属性将模板与控件关联起来的方式。我还添加了一个style元素,它适用于从模板创建的元素。
清单 15-13 。向 ListView.html 文件添加并应用组标题模板
`
** ** ** .groupHead {border: thin solid white;background-color: black;** ** color: white;padding: 5px;font-weight: bold;}** ** ****
`
模板没有什么特别的——它遵循了我在《??》第八章中展示的所有约定,并用于ListView和FlipView控件的项目模板。你可以在图 15-6 中看到结果,我已经在两个位置显示了从模板生成的组标题。
***图六。*使用模板显示群组标题
处理 ListView 事件
ListView控件定义了我在表 15-2 中描述的四个事件。在很大程度上,这些事件与用户交互有关,我将在下面的章节中解释。
处理被调用的项目
当用户通过点击或用鼠标点击选择一个项目时,触发iteminvoked事件。传递给事件处理函数的对象是一个IItemPromise,它是一个常规的Promise,当它被满足时返回一个IItem对象(我在第九章中描述过)。在清单 15-14 中,你可以看到我是如何使用IItemPromise来更新中间面板中标有Invoked的 span 元素的。
清单 15-14 。处理 iteminvoked 事件
... list.addEventListener("**iteminvoked**", function (e) { e.detail.itemPromise.then(function (item) { invoked.innerText = item.data.letter; $('.invoked').removeClass("invoked"); WinJS.Utilities.addClass(e.target, "invoked"); }); }); ...
没有视觉提示来显示哪个项目被调用了,所以,如果你想让用户清楚,你必须自己处理。在这个例子中,我将invoked类应用于表示被调用项目的元素(并且,因为一次只能调用一个项目,所以要确保没有其他元素属于这个类)。invoked类对应于我在本章开始时向您展示的/css/listview.css文件中的样式。我已经重复了清单 15-15 中的样式。
清单 15-15 。定义被调用项目的样式
... *.invoked {color: red;} *.invoked .listData { font-weight: bold;} *.invoked .listTitle {background-color: transparent;} ...
你可以在图 15-7 中看到结果,该图显示了同一项目的正常状态和调用状态。但是,您不必指出被调用的项,尤其是当它与项的选择相冲突时,我将在下一节中对此进行描述。
***图 15-7。*为用户标记被调用的项目
设定点击行为
通过改变tapBehavior属性的值,可以改变ListView响应用户点击项目的方式。这个属性可以设置为WinJS.UI.TapBehavior枚举中的一个值,我已经在表 15-3 中描述过了。
这些值定义了调用和选择项目之间的关系。如果您只想让用户调用项目,那么您应该使用的值是invokeOnly。如果您希望用户能够选择,但不能调用项目,那么您应该使用none值。其他值允许同时调用和选择一个项目。我将在下一节解释项目选择。
您可以通过从应用布局的右侧面板中的select元素中选择一个值来更改tapBehavior属性的值,该元素的标签很明显是tapBehavior。
处理项目选择
用户可以选择项目并调用它们。这可能会导致一些复杂的交互,因此配置ListView非常重要,这样您就可以获得您想要的效果。您可以通过使用我在上一节中提到的tapBehaviour属性和selectionMode属性来实现这一点,后者是使用来自WinJS.UI.SelectionMode枚举的值来设置的。我已经在表 15-4 中描述了这个枚举中的值。
提示您可以通过从标签为
selectionMode的应用布局右侧面板的select元素中选取一个值来更改selectionMode属性的值。
通过监听selectionchanged事件,您可以在选择发生变化时收到通知。当selectionMode设置为multi时,通过监听selectionchanging事件,您可以在用户向选择中添加项目时接收更新。你可以看到我是如何在清单 15-16 中为selectionchanged定义一个处理程序的。
清单 15-16 。处理 selectionchanged 事件
... list.addEventListener("iteminvoked", function (e) { e.detail.itemPromise.then(function (item) { invoked.innerText = item.data.letter; $('.invoked').removeClass("invoked"); ` WinJS.Utilities.addClass(e.target, "invoked");
});
});
list.addEventListener("selectionchanged", function (e) { ** this.winControl.selection.getItems().then(function (items) {** ** var selectionString = "";** ** items.forEach(function (item) {** ** selectionString += item.data.letter + ", ";** ** });** ** selected.innerText = selectionString.slice(0, -2);** ** });** }); ...`
用户选择的项目集可通过 selected 属性获得,该属性返回一个ISelection对象。这个对象定义了一些有用的方法来处理选中的项目,我已经在表 15-5 中描述过了。
在清单中,我使用了getItems方法来获取所选项的集合,以便构建一个可以在视图模型中设置的值,从而在示例的右侧面板中显示选择。getItems方法返回一个由IItem对象组成的数组,每个对象对应一个选中的项目。正如您在第十四章中回忆的那样,IItem通过一个Promise使其内容可用,这就是为什么我必须使用 then 方法来获得每一项的细节(参见第九章以获得Promise对象的更多细节)。ListView对选择的项目进行强调,以便用户可以看到整个选择,如图图 15-8 所示。注意,字母D的项目被选中并且被调用。
***图 15-8。*通过 ListView 控件添加到所选项目的强调
设计 ListView 控件的样式
ListView支持许多类,你可以用它们来设计控件外观的不同方面,如表 15-6 中所总结的。
并非所有这些 CSS 类都是ListView控件独有的——例如,你会注意到win-item类是与我在第十四章中描述的FlipView控件共享的。当应用这些样式时,您需要确保将焦点缩小到已经应用了ListView控件的元素上。在清单 15-17 中,你可以看到我添加到ListView.html文件中的style元素来演示这些风格。
清单 15-17 。使用 CSS 样式化 ListView 选择
`...
.groupHead {border: thin solid white;background-color: black; color: white;padding: 5px;font-weight: bold;}` `** .win-selectioncheckmark {** ** color: red; font-size: 20pt;** ** }**...`
我已经使用了win-selectioncheckmark类来改变选中项目上显示的格子的大小和颜色,你可以在图 15-9 中看到它的效果。
***图 15-9。*使用 CSS 样式化 ListView 控件
以编程方式管理 ListView 控件
您可以使用许多不同的方法和属性来控制ListView的行为。我已经描述了其中的一些,比如通过selection属性可用的add、remove和set方法,以及控制数据项如何显示的itemTemplate和groupHeaderTemplate属性。表 15-7 显示了从代码中驱动ListView控件行为时有用的一些附加方法和属性。
我倾向于不使用indexOfElement和elementFromIndex方法,因为我更喜欢让ListView控件处理其内容的外观和布局。然而,其他方法可能非常有用,尤其是当您从外部 UI 控件驱动ListView时。作为一个例子,我在例子的右边面板中包含了一个input元素,这个元素被标记为Ensure Visible。清单 15-18 显示了我添加到/js/pages/listview.js文件中的代码,当input元素的内容改变时,它调用ensureVisible方法。
清单 15-18 。调用 ensureVisible 方法
... proxyObject.bind("ensureVisible", function (val) { list.winControl.**ensureVisible**(val == null ? 0 : val); }); ...
例如,如果您将 input 元素中的值设置为26,您将看到ListView滚动其内容,以便字母Z可见。
搜索元素
用户通常不会根据索引来考虑数据项,因此作为一个相关的例子,我在右边的面板中定义了另一个标记为Search For的input元素。在清单 15-19 中,你可以看到我添加到/js/pages/listview.js文件中的代码,当一个值被输入到input元素中时,通过定位相应的项目并选择它来响应。这是一个简单的技巧,但却是一个经常需要的技巧,所以我想把它包含在这一章中,供你将来参考。
清单 15-19 。根据内容定位和选择项目
... proxyObject.bind("searchFor", function (val) { if (val != null) { var index = -1; ViewModel.data.letters.forEach(function (item) { if (item.letter == val.toUpperCase()) { index = ViewModel.data.letters.indexOf(item); } }); if (index > -1) { list.winControl.ensureVisible(index); list.winControl.selection.set([index]); } } }); ...
代码的第一部分定位数据源中与用户在input元素中输入的字母相匹配的项目的索引。如果有匹配,我使用索引来设置ListView选择,并确保选中的项目是可见的。在图 15-10 中,你可以看到在Search For input元素中输入字母J会发生什么。
***图 15-10。*寻找字母 J
使用数据源
在我的例子中,ListView控件的数据源是一个WinJS.Binding.List对象。这是一个方便的安排,因为这意味着我可以通过操作List的内容来改变ListView显示的数据。
虽然这很方便,但不太现实。您可能无法通过这样一个有用的辅助渠道修改数据,甚至不知道您正在处理哪种数据源。在这些情况下,您需要依赖由IListDataSource接口定义的功能。该接口定义了所有数据源都必须实现的方法,并且您可以依赖这些由ListView.itemDataSource属性返回的对象定义的方法。我已经在表 15-8 中描述了IListDataSource定义的方法。
我在这个例子的右边面板中定义了两个button元素,标记为Add Item和Delete Item。您可以从清单 15-20 中的按钮看到我添加到/js/pages/listview.js文件中处理click事件的代码。在这段代码中,我使用了来自IListDataSource接口的方法来编辑数据源的内容。
清单 15-20 。使用 IListDataSource 方法编辑数据源的内容
... $(".buttonContainer button").listen("click", function (e) { var ds = list.winControl.itemDataSource; ds.beginEdits(); var promise; if (this.innerText == "Add Item") { promise = ds.insertAtEnd(null, { letter: "A", group: 4 }); } else { promise = ds.remove("1"); } promise.then(function () { ds.endEdits(); }); }); ...
如果点击Add Item按钮,一个新的数据项将被添加到数据源中,并由ListView控件显示——该数据项总是带有字母A,属于组4。如果点击Delete Item按钮,索引1处的元素将被移除。
处理钥匙
如果您仔细查看表中的方法,您会注意到键在标识数据源中的项时起着重要的作用。到目前为止,我还没有担心过键,因为它们是由List对象自动分配的,但是当您直接使用数据源时,它们就会浮出水面。
WinJS.Binding.List对象遵循一个简单的系统,根据条目添加到列表中的顺序生成键。第一项被分配一个键1,第二项被分配一个键2,依此类推。键被表示为字符串值,所以如果你想通过它的索引定位一个键,你必须使用"1"而不是数字值。您可以看到我如何使用键值来响应被点击的Delete Item按钮:
... promise = ds.remove(**"1"**); ...
该方法调用首先删除添加到List中的项目。但是,当数据源的内容改变时,键不会改变,因此再次单击该按钮将会生成错误,因为数据源中没有具有指定键的项。
当使用List对象作为数据源时,这是一个非常常见的错误,因为很容易假设键指的是元素的索引,而不是它被添加的顺序。要删除第一个项目,我需要采取不同的方法,如清单 15-21 所示,它通过位置获取一个项目,然后获取密钥并用它来请求删除。
清单 15-21 。根据项目在数据源中的位置删除项目
... $(".buttonContainer button").listen("click", function (e) { var ds = list.winControl.itemDataSource; ds.beginEdits(); var promise; if (this.innerText == "Add Item") { promise = ds.insertAtEnd(null, { letter: "A", group: 4 }); } else { //promise = ds.remove("1"); ** promise = ds.itemFromIndex(0).then(function (item) {** ** return ds.remove(item.key);** ** });** } promise.then(function () { ds.endEdits(); }); }); ...
由itemFromIndex方法返回的来自Promise的结果是一个IItem对象,我在第十四章中介绍过。这个对象包含了键,我可以将它传递给remove方法。注意,几乎所有由IListDataSource接口定义的方法都返回一个Promise。你需要使用Promise.then方法将操作数据源内容的动作链接在一起——参见第九章了解Promise对象和如何使用then方法的全部细节。
添加没有关键字的项目
点击Add Item按钮向数据源添加一个新项目。我不想担心为我的数据对象生成唯一的键,所以我将insertAtEnd方法的第一个参数设置为null,如下所示:
... promise = ds.insertAtEnd(**null**, { letter: "A", group: 4 }); ...
这告诉数据源实现对象(在我的例子中是List)应该使用它的标准算法生成一个键。
抑制更新事件
每次项目改变时,数据源都会发出事件,这导致ListView更新其布局,以便与数据保持同步。这是一个很棒的功能,但这意味着如果您进行多次编辑,您需要显式禁用这些事件——如果您不这样做,那么ListView将在每次更改后自我更新,这是一个资源密集型操作,可能会导致用户看到不一致或混乱的数据项视图。
您可以使用beginEdits方法禁用事件。我已经在示例中这样做了,尽管我只编辑了一个项目(我发现总是调用beginEdits是一个好习惯,这样我以后更新编辑代码时就不会有问题了)。一旦您调用了这个方法,您就可以自由地对数据源进行彻底的修改,而不必担心不必要的ListView更新。
完成编辑后,必须确保调用endEdits方法。如果您忘记了,数据源不会发送任何事件,并且ListView也不会更新。你需要确保在调用endEdits之前所有的编辑操作都已经完成,这意味着跟踪IListDataSource方法返回的Promise对象,并在适当的时候使用then方法链接方法调用。你可以在清单 15-22 中看到我是如何做到的。
清单 15-22 。使用 then 方法确保 endEdits 方法具有正确的效果
... $(".buttonContainer button").listen("click", function (e) { var ds = list.winControl.itemDataSource; ** ds.beginEdits();** var promise; if (this.innerText == "Add Item") { promise = ds.insertAtEnd(null, { letter: "A", group: 4 }); } else { //promise = ds.remove("1"); promise = ds.itemFromIndex(0).then(function (item) { return ds.remove(item.key); }); } ** promise.then(function () {** ** ds.endEdits();** ** });** }); ...
如果您在编辑操作完成之前调用了endEdits方法,那么您将会在ListView中看到您所做的最后几次编辑的每次更改的更新,这就使调用beginEdits失去了意义。
监听数据源的变化
您可以通过使用createListBinding方法接收数据源变化的通知。这个方法的参数是一个实现由IListNotificationHandler接口定义的方法的对象,我已经在表 15-9 中描述过了。
你倾听事件的方式有点奇怪,最好用一个例子来解释。清单 15-23 显示了我在例子中定义的处理程序。
清单 15-23 。使用带有数据源的通知处理程序
... var handler = { ** countChanged: function (newCount, oldCount) {** ** itemCount.innerText = newCount;** ** }** }; list.winControl.itemDataSource.**createListBinding(handler)**; ...
第一步是创建一个对象,该对象定义与表中的方法相匹配的方法。您可以在清单中看到,我定义了一个名为countChanged的方法,它有两个参数——这与由IListNotificationHandler接口定义的countChanged方法相匹配。
一旦实现了您感兴趣的方法,您就可以将处理程序对象传递给createListBinding方法。从这一点来看,当数据源发生变化时,将调用处理程序方法。在我的例子中,当数据源中的项数改变时,我的countChanged方法将被执行。您可以通过点击Add Item或Delete Item按钮来实现这一点,并查看示例右侧面板中Count标签旁边显示的结果。
总结
在这一章中,我已经向你展示了ListView控件,这是一个丰富而复杂的 UI 控件。布局和模板系统允许您控制项目的布局,我解释了调用和选择项目以及与它们相关的事件之间的区别。在本章的最后,我向您展示了如何使用数据源,而不是直接操作实现对象。这允许您创建适用于任何数据源的代码,而不仅仅是基于WinJS.Binding.List对象的代码。在下一章,我将向你展示如何使用语义缩放,它结合了不同的 WinJS UI 控件来创建一个关键的窗口交互。*
十六、使用SemanticZoom
在这一章中,我描述了 WinJS UI 控件的最后一个,叫做SemanticZoom。该控件代表 Windows 中的一个关键用户交互,并允许用户在数据集中的两个细节级别之间缩放。控件本身相对简单,依靠一对ListView控件来完成所有的艰苦工作(我在第十五章的ListView控件中描述过)。在这一章中,我将向你展示如何使用SemanticZoom,并演示一种可以同时显示两个细节层次的替代方法。表 16-1 提供了本章的总结。
何时使用语义缩放控件
只有当数据可以被有意义地分组时,才应该使用SemanticZoom控件,因为这是提供导航和上下文的基础。我说是有意义的分组,因为以对用户和他们使用数据执行的任务有意义的方式来表示数据是很重要的。将分组应用到数据上很容易,只是为了让使用SemanticZoom变得可行,但是结果会让那些必须处理以无意义的方式组织的数据的用户感到有点困惑。在使用SemanticZoom控件之前,您可能希望阅读整个章节:除了如何使用SemanticZoom的细节,我还演示了一种替代方法,这种方法对于数据分组方式对用户来说不太明显的数据集很有用。
添加 SemanticZoom 示例
SemanticZoom控件相对简单,因为它建立在ListView控件的功能上。对于更大的数据集来说,SemanticZoom控件是最容易演示的,所以我开始对/js/viewmodel.js文件做了一些补充,如清单 16-1 所示。
清单 16-1 。向 viewmodel.js 文件添加数据
`(function () { "use strict";
WinJS.Namespace.define("ViewModel", { data: { images: new WinJS.Binding.List([ { file:img/aster.jpg", name: "Aster"}, { file:img/carnation.jpg", name: "Carnation"}, { file:img/daffodil.jpg", name: "Daffodil"}, { file:img/lily.jpg", name: "Lilly"}, ]),
extraImages: [{ file:img/orchid.jpg", name: "Orchid"}, { file:img/peony.jpg", name: "Peony"}, { file:img/primula.jpg", name: "Primula"}, { file:img/rose.jpg", name: "Rose"}, { file:img/snowdrop.jpg", name: "Snowdrop" }],
letters: new WinJS.Binding.List(), groupedLetters: null, ** names: new WinJS.Binding.List(),** ** groupedNames: null,** }, });
// ...code for previous chapters removed for brevity...
** var namesSrcData = ['Aaliyah', 'Aaron', 'Abigail', 'Abraham', 'Adam', 'Addison',**
** 'Adrian', 'Adriana', 'Aidan', 'Aiden', 'Alex', 'Alexa', 'Alexander', 'Alexandra',**
** 'Alexis', 'Allison', 'Alyssa', 'Amelia', 'Andrew', 'Angel', 'Angelina',**
** 'Anna', 'Anthony', 'Ariana', 'Arianna', 'Ashley', 'Aubrey', 'Austin', 'Ava',**
** 'Avery', 'Ayden', 'Bella', 'Benjamin', 'Blake', 'Brandon', 'Brayden', 'Brian',**
** 'Brianna', 'Brooke', 'Bryan', 'Caleb', 'Cameron', 'Camila', 'Carter', 'Charles',**
** 'Charlotte', 'Chase', 'Chaya', 'Chloe', 'Christian', 'Christopher', 'Claire',**
** 'Connor', 'Daniel', 'David', 'Dominic', 'Dylan', 'Eli', 'Elijah', 'Elizabeth',**
** 'Ella', 'Emily', 'Emma', 'Eric', 'Esther', 'Ethan', 'Eva', 'Evan', 'Evelyn',**
** 'Faith', 'Gabriel', 'Gabriella', 'Gabrielle', 'Gavin', 'Genesis', 'Gianna',**
** 'Giovanni', 'Grace', 'Hailey', 'Hannah', 'Henry', 'Hunter', 'Ian', 'Isaac',**
** 'Isabella', 'Isaiah', 'Jack', 'Jackson', 'Jacob', 'Jacqui', 'Jaden', 'Jake',**
** 'James', 'Jasmine', 'Jason', 'Jayden', 'Jeremiah', 'Jeremy', 'Jessica', 'Joel',**
** 'John', 'Jonathan', 'Jordan', 'Jose', 'Joseph', 'Joshua', 'Josiah', 'Julia',**
** 'Julian', 'Juliana', 'Julianna', 'Justin', 'Kaitlyn', 'Katherine', 'Kayla',**
** 'Kaylee', 'Kevin', 'Khloe', 'Kimberly', 'Kyle', 'Kylie', 'Landon', 'Lauren', 'Layla', 'Leah', 'Leo', 'Liam', 'Lillian', 'Lily', 'Logan', 'London', 'Lucas',**
** 'Luis', 'Luke', 'Mackenzie', 'Madeline', 'Madelyn', 'Madison', 'Makayla', 'Maria',**
** 'Mason', 'Matthew', 'Max', 'Maya', 'Melanie', 'Mia', 'Michelle', 'Miriam', 'Molly',**
** 'Morgan', 'Moshe', 'Naomi', 'Natalia', 'Natalie', 'Nathan', 'Nathaniel', 'Nevaeh',**
** 'Nicholas', 'Nicole', 'Noah', 'Oliver', 'Olivia', 'Owen', 'Paige', 'Patrick',**
** 'Peyton', 'Rachel', 'Rebecca', 'Richard', 'Riley', 'Robert', 'Ryan', 'Samantha',**
** 'Samuel', 'Sara', 'Sarah', 'Savannah', 'Scarlett', 'Sean', 'Sebastian', 'Serenity',**
** 'Sofia', 'Sophia', 'Sophie', 'Stella', 'Steven', 'Sydney', 'Taylor', 'Thomas',**
** 'Tristan', 'Tyler', 'Valentina', 'Victoria', 'Vincent', 'William', 'Wyatt',**
** 'Xavier', 'Zachary', 'Zoe', 'Zoey'];**
** namesSrcData.forEach(function (item, index) {** ** ViewModel.data.names.push({name: item, firstLetter: item[0]** ** });** ** });**
** ViewModel.data.groupedNames = ViewModel.data.names.createGrouped(** ** function (item) { return item.firstLetter; },** ** function (item) { return item; },** ** function (g1, g2) { return g1 < g2 ? -1 : g1 > g2 ? 1 : 0; }** ** );** })();`
我将处理的数据是一个姓名列表。这些是 2011 年纽约州最受欢迎的婴儿名字。
我首先处理名称列表,以填充我分配给ViewModel.data名称空间中的 names 属性的WinJS.Binding.List对象。对于每个名字,我创建一个对象,它有一个name属性和一个firstLetter属性。我将完整的名称分配给 name 属性,并且顾名思义,将名称的第一个字符分配给firstLetter属性(这将使我以后更容易将数据组织成组)。所以,以名称Sophie为例,我在ViewModel.data.names List中创建一个对象,如下所示:
... { name: "Sophie", firstLetter: "S" } ...
我使用ViewModel.data.names List中的数据对象创建分组数据,使用的技术与我在第十五章的中展示的技术相同。正如你将看到的,当使用SemanticZoom控件时,群组扮演了一个重要的角色。在这种情况下,我已经按照项目的首字母对它们进行了分组,因此所有以A开头的名称都在同一个组中,所有以B开头的名称也是如此,依此类推。分组数据可通过ViewModel.data.groupedNames属性获得。
定义 HTML 文件
为了演示SemanticZoom控件,我在 Visual Studio 项目的pages文件夹中创建了一个名为SemanticZoom.html的新文件。你可以在清单 16-2 中看到这个文件的内容。
清单 16-2 。SemanticZoom.html 文件的最初内容
`
这个文件包含了标准的双面板布局,我在本书这一部分的大多数例子中使用了这个布局,还有三个模板,我将用它们来演示SemanticZoom控件的特性。有一点是不包含对SemanticZoom控件本身的任何引用——这是因为通过向您展示底层构建模块并在以后添加控件,我可以更容易地解释该控件的工作原理。
本例的 JavaScript 只包含对Templates.createControl方法的调用,所以我将它包含在 HTML 文件的script元素中。
定义 CSS
在SemanticZoom.html文件中有两个link元素。第一个是我在第十五章中创建的/css/listview.css文件,它包含我为用来显示数据的模板定义的样式——出于同样的目的,我在本章中再次使用这些样式。第二个link元素指的是我添加到 Visual Studio 项目中的一个名为/css/semanticzoom.css的新文件,它包含一些我用于SemanticZoom控件的附加样式。您可以在清单 16-3 的中看到semanticzoom.css文件的内容。这个文件中没有新的技术——我只是设置了将应用SemanticZoom控件的元素的大小,并为SemanticZoom所依赖的底层控件之一定义了一些基本样式(稍后我会解释)。
清单 16-3 。semanticzoom.css 文件的内容
`#semanticZoomer { width: 500px; height: 500px; }
*.zoomedInListItem { width: 150px; }
*.zoomedInListData { background-color: black; text-align: center; border: solid medium white; font-size: 20pt; padding: 10px; }`
完成示例
你可以在清单 16-4 中看到我添加到/js/controls.js文件中的定义对象。SemanticZoom控件相对简单,您可以从我定义的少量配置控件中看到这一点。
清单 16-4 。controls.js 文件的定义对象
... **semanticZoom**: [ { type: "toggle", id: "enableButton", title: "EnableButton", value: true }, { type: "toggle", id: "locked", title: "Locked", value: false }, { type: "toggle", id: "zoomedOut", title: "Zoomed Out", value: false }, { type: "input", id: "zoomFactor", title: "Zoom Factor", value: 0.65 }, ], ...
最后一步是确保用户可以导航到SemanticZoom.html文件,我通过在/js/templates.js文件中添加如清单 16-5 所示的内容来完成。
清单 16-5 。增加导航到 SemanticZoom.html 文件的支持
var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, { name: "Flyout", icon: "\u0036" }, { name: "Menu", icon: "\u0037" }, { name: "MessageDialog", icon: "\u0038" }, { name: "FlipView", icon: "pictures" }, { name: "Listview", icon: "list" }, ** { name: "SemanticZoom", icon: "zoom" },** ];
如果您在此时运行示例应用并导航到SemanticZoom.html页面,您将看到如图图 16-1 所示的应用布局。面板结构和配置控件已经就位,但是左侧面板中没有控件。在接下来的部分中,我将向您展示如何应用SemanticZoom控件。
图 16-1。【SemanticZoom.html 页面的初始布局
了解语义缩放控件
SemanticZoom控件为用户提供了相同数据的两种视图选择——一种是放大的视图,显示分组排列的单个数据项,另一种是缩小的视图,只显示分组,不显示数据项。用户可以使用缩小视图来浏览大型复杂数据集,然后深入了解特定组的详细信息。
向您展示如何应用SemanticZoom的最佳方式是从关注如何创建两个视图开始,这依赖于WinJS.UI.ListView控件的两个实例,我在第十五章中描述过。在接下来的章节中,我将向你展示如何创建底层的ListView控件,然后如何应用SemanticZoom控件来协调它们的行为和外观。
创建放大的列表视图控件
SemanticZoom控件依靠两个ListView控件来表示数据的放大和缩小视图。你可以在清单 16-6 的中看到我是如何定义放大视图的,我已经将它添加到了SemanticZoom.html文件中。
清单 16-6 。定义放大的 ListView 控件
`...
要创建放大视图,将分组数据源的itemDataSource设置为dataSource,将groupDataSource属性设置为groups.dataSource对象。这将设置ListView,使数据项成组显示,并带有组标题。(这与我在第十五章的中描述ListView控件时使用的技术相同。)
您用常规模板设置了itemTemplate和groupHeaderTemplate来显示项目和组标题。这些是我在SemanticZoom.html文件中定义的模板,我在清单 16-7 中再次列出了它们。
清单 16-7 。放大的 ListView 控件的项目和显示模板
`...
如果您运行该示例并在此时导航到SemanticZoom.html文件,您将能够看到添加和配置ListView控件的效果,如图图 16-2 所示。我仍然没有SemanticZoom控件,但是数据的两个视图中的一个已经就位。
***图 16-2。*将放大的视图添加到 SemanticZoom.html 文件
创建缩小的 ListView 控件
下一步是添加将显示缩小视图的ListView控件,向用户显示组列表,而不显示这些组中的任何数据项。你可以在清单 16-8 的中看到我对SemanticZoom.html文件添加的ListView控件。
清单 16-8 。为缩小视图添加 ListView 控件
`...
这个ListView将只显示组,这意味着我必须使用组数据源作为itemDataSource属性的值。这是一个巧妙的技巧,因为它允许ListView控件显示缩小的数据,而不需要理解它正在处理的数据的结构——这是SemanticZoom控件,我将很快添加到示例中,它提供上下文并关联数据的两个视图。
***图 16-3。*两个 ListView 控件都显示在应用布局中
如果你现在运行这个例子,你将会在应用布局中看到两个ListView控件,如图 16-3 中的所示。
根据您的设备分辨率,布局中可能没有足够的空间来显示两个ListView控件,因此其中一个控件可能会溢出屏幕底部。即便如此,这也是一个机会,可以确保在应用SemanticZoom控件并开始管理控件的可见性之前,它们已经被正确配置。
应用语义缩放控件
通过将data-win-control属性设置为WinJS.UI.SemanticZoom,将SemanticZoom控件应用于div元素,其中div元素包含显示数据的两个视图的ListView控件。
当您设置了ListView控件并以您想要的方式显示数据时,您可以添加SemanticZoom控件。你可以在清单 16-9 的中的SemanticZoom.html文件中看到我是如何做的。
注意
ListView控件在SemanticZoom div元素中声明的顺序很重要——放大视图必须出现在缩小视图之前。
清单 16-9 。应用语义缩放控件
`...
应用SemanticZoom控件的结果是只有一个ListView元素,最初,这是放大的视图。如果你将鼠标移到SemanticZoom控件上,你会看到滚动条上方出现一个小按钮。在图 16-4 中,你可以看到放大的视图,我高亮显示了这个按钮以便更容易看到(直到你知道它在那里,它才那么明显)。
***图 16-4。*放大视图和缩小按钮。
点击该按钮将使SemanticZoom控件显示向缩小视图转换的动画,允许用户浏览组级别的数据,如图 16-5 中的所示。
**图 16-5。**语义缩放控件显示的缩小视图
如果你点击一个组,那么SemanticZoom控件将动画显示转换回放大的视图,显示你选择的组中的项目。
在语义缩放视图之间导航
我向您展示了缩小按钮,因为在没有提示的情况下很难注意到它,但是有几种不同的方式可以在由SemanticZoom控件显示的两个视图之间导航。如果你有带滚轮的鼠标,你可以按住Control键,向上移动鼠标滚轮放大,向下移动缩小。如果你喜欢使用键盘,那么你可以使用Control和加号键(+)放大,使用Control和减号键(-)缩小。
如果您是触控用户,那么您可以使用捏合/缩放触控手势来放大和缩小。我将在第十七章中详细讨论触摸手势,但是如果你选择了我在图 16-6 中突出显示的按钮,Visual Studio 模拟器将允许你执行捏/缩放手势。
***图 16-6。*捏手势模拟按钮并显示
当您选择收缩/缩放手势按钮时,光标将变为图中所示的两个空心圆圈,每个圆圈代表一根手指。按住鼠标按钮,模拟用手指触摸屏幕–光标会发生变化,圆圈被填满,如图的最后一帧所示。使用鼠标滚轮将模拟的手指移近或移远,创建收缩/缩放手势。将手指并拢使SemanticZoom控件缩小,将手指分开使其放大。
配置语义缩放控件
SemanticZoom控件支持我在表 16-2 中描述的配置属性。与其他 WinJS UI 控件相比,这是一个很小的属性集,因为SemanticZoom中的复杂性被委托给它所依赖的ListView控件。
我已经在示例的右面板中定义了配置控件来演示所有四个SemanticZoom属性。locked和zoomedOut属性是不言而喻的,但是我将在接下来的章节中解释另外两个属性。
启用和禁用缩小按钮
属性控制我在图 16-4 中展示的缩小按钮的可见性。我在示例的右面板中加入了一个ToggleSwitch,它改变了SemanticZoom控件上的enableButton属性。
该属性的默认值是true,这意味着SemanticZoom控件将显示按钮。将属性设置为false会阻止按钮显示,但是用户仍然可以使用我在上一节中描述的其他技术在视图之间导航。如果您想防止用户在视图之间切换,那么将locked属性设置为true。
设置缩放系数
正如您已经注意到的,SemanticZoom控件使用动画在视图之间切换,而zoomFactor属性决定动画的戏剧性。该值可以在0.2和0.85之间,默认为0.6。我无法在打印页面上演示动画,但是较小的值会产生更生动的缩放效果。我更喜欢一个更微妙的动画,这意味着如果我改变默认值,它通常是一个值0.8,它创建了一个效果,清楚地表明一个过渡,而不是太引人注目。
处理 SemanticZoom 控件事件
SemanticZoom控件定义了一个事件,当控件在缩放级别之间切换时会发出该事件。我在表 16-3 中描述了这一事件。
您可以在清单 16-10 中看到我是如何处理这个事件的。当事件被触发时,我更新了视图模型中的一个属性,这使得标记为Zoomed Out的ToggleSwitch配置控件与SemanticZoom的状态保持一致。
清单 16-10 。处理 SemanticZoom 事件
`...
...`传递给 handler 函数的Event对象的detail属性在SemanticZoom为zoomedOut时设置为true,在放大时设置为false。
提示你通过直接处理控件的事件来响应用户与放大的
ListView的交互。关于项目被调用或选择时ListView用来发送通知的事件的详细信息,请参见第十五章。
设置 SemanticZoom 控件的样式
一个SemanticZoom的大部分样式是通过底层ListView控件使用的模板完成的(我在第十五章中描述过)。然而,有两个类可以用来直接设计这个控件的样式,如表 16-4 所述。
我不会在我的项目中使用这些风格。如果我想应用常规样式,那么我的目标是包含SemanticZoom控件的父元素(在我的例子中,这是具有semanticZoomContainer的id的div元素,我通常在我的所有内容中有一个等价的容器元素)。如果我想设计一个更具体的布局部分,那么我会瞄准ListView控件,或者通常依靠项目和组标题模板来获得我想要的效果。
语义缩放控件的替代
在我看来,SemanticZoom控件呈现的交互有一个缺陷,那就是当显示放大视图时,缩小视图呈现的整体上下文丢失了。当可以从数据中推断出上下文时,这不是问题,我的姓名数据就是这种情况。你可以在图 16-7 中看到我的意思。
***图 16-7。*数据中群体的性质非常明显
我只需要向您展示一两个组,就可以清楚地看到数据是按字母顺序分组的,并且根据所显示的组,SemanticZoom显示的数据接近数据源的 50%。在这些情况下,SemanticZoom控件是完美的,因为用户需要知道的一切都显示在布局中,或者可以很容易地从布局中推断出来。
注以下部分展示了如何将两个
ListView控件连接在一起。然而,很难仔细阅读所有对放大和缩小视图的引用。我的建议是在 Visual Studio 中遵循这些部分,以便您可以看到每个更改的效果——它将为描述性文本提供上下文。
有时这种方法不起作用,而您希望同时显示两个视图,以便整体上下文和细节同时可见。我发现自己经常遇到这个问题,我通过用两个ListView控件和一些事件处理程序代码替换SemanticZoom控件来解决这个问题。
创建列表上下文示例
为了演示两个ListView控件的排列,我在名为ListContext.html的示例项目的pages目录中添加了一个新文件,其内容如清单 16-11 所示。
清单 16-11 。ListContext.html 文件的内容
`
<div id="zoomedIn" data-win-control="WinJS.UI.ListView" data-win-options="{ itemDataSource: ViewModel.data.groupedNames.dataSource, groupDataSource: ViewModel.data.groupedNames.groups.dataSource, itemTemplate: contextZoomedInItemTemplate, groupHeaderTemplate: contextGroupHeaderTemplate}">
`这个文件包含两个ListView元素,它们使用我为SemanticZoom控件定义的相同数据源。与SemanticZoom的例子一样,我使用了一个ListView来分组显示数据项(创建放大视图),使用了一个ListView来只显示分组本身(创建缩小视图)。
定义 CSS
我在这个例子中重用了semanticzoom.css文件,这样就不必改进我在模板中使用的样式。我还添加了/css/listcontext.css文件来去掉ListView对象,并定义了一些我将在本章后面使用的附加样式。您可以在清单 16-12 中看到listcontext.css文件的内容。这个文件中没有新的技术,所有的样式都很简单,并且使用标准的 CSS。
清单 16-12 。listcontext.css 文件的内容
`#contextContainer { height: 100%; display: -ms-flexbox; -ms-flex-direction: row; -ms-flex-align: center; -ms-flex-pack: center; }
#contextContainer div[data-win-control="WinJS.UI.ListView"] { border: thick solid white; height: 650px; padding: 20px; margin: 10px; }
#zoomedOut { width: 200px;} #zoomedIn { width: 900px; padding: 20px;} #zoomedIn .win-groupheader {display: none;}
*.contextListItem {width: 170px;} *.contextListData { background-color: black; text-align: center; border: solid medium white; font-size: 20pt;}
*.highlighted { color: #4cff00; font-weight: bold;} *.notHighlighted { color: white; font-weight: normal; -ms-transition-delay: 100ms; -ms-transition-duration: 500ms;}`
定义 JavaScript 代码
这个例子的 JavaScript 代码在/js/pages/listcontext.js文件中。首先,这个文件只包含一个空的自执行函数,但是我将在完成这个示例的过程中添加代码。你可以在清单 16-13 中看到这个文件的初始内容。
清单 16-13 。listcontext.js 文件的初始内容
`(function () {
WinJS.UI.Pages.define("/pages/ListContext.html", { ready: function () { // ...code will go here... } });
})();`
这个例子没有定义对象,因为我不需要演示任何特定的 UI 控件特性。这意味着我需要采取的唯一额外步骤是确保用户可以通过导航条导航到ListContext.html文件,我已经对清单 16-14 中的文件做了添加。
清单 16-14 。通过导航栏启用 ListContext.html 文件导航
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, { name: "Flyout", icon: "\u0036" }, { name: "Menu", icon: "\u0037" }, { name: "MessageDialog", icon: "\u0038" }, { name: "FlipView", icon: "pictures" }, { name: "Listview", icon: "list" }, { name: "SemanticZoom", icon: "zoom" }, ** { name: "ListContext", icon: "list" },** ]; ...
如果运行该示例并导航到ListContext.html文件,您将看到如图图 16-8 所示的布局。两个ListView控件已经就位并填充了数据,但是它们还没有链接在一起,所以当您调用单个项目时,它们之间没有交互。
图 16-8。【ListContext.html 文件的布局
为列表上下文示例添加事件处理程序代码
本例的构建模块已经就绪——剩下的是纯粹的 JavaScript 来创建两个ListView控件之间的关系,模拟SemanticZoom控件的基本行为,但确保用户可以看到组的上下文和单个元素的细节。在接下来的部分中,我将所需的代码添加到/js/pages/listcontext.js文件中,并解释每次添加是如何构建ListView控件之间交互的关键部分的。
从缩小的 ListView 控件中选择组
我想做的第一件事是通过在放大视图中显示相应的组来响应在缩小视图中调用的项目。你可以在清单 16-15 中看到我是如何做到这一点的,这里我添加了一个函数来处理ListContext.js文件中的iteminvoked事件。
清单 16-15 。在缩小视图中处理调用的项目
`(function () {
WinJS.UI.Pages.define("/pages/ListContext.html", { ready: function () {
zoomedOut.addEventListener("iteminvoked", function (e) { e.detail.itemPromise.then(function (item) { var invokedGroup = item.key; zoomedIn.winControl.groupDataSource.itemFromKey(invokedGroup) .then(function (item) { var index = item.firstItemIndexHint; zoomedIn.winControl.indexOfFirstVisible = index; }); }); }); } });
})();`
正如你在第十五章中回忆的那样,当用户点击一个项目时,就会触发iteminvoked事件。传递给处理函数的Event对象的detail属性是一个Promise对象,它在项目完成时返回项目。我使用then方法获取项目,并使用key属性获取被调用项目的键。
属性告诉我用户调用了哪个组。为了在放大的视图中显示这个组,我在分组的数据源上使用了itemFromKey方法。这给了我另一个Promise,它在完成时返回一个项目。不同之处在于,从分组数据源返回的项包含一个firstItemIndexHint属性,该属性返回组中第一项的索引。我通过设置indexOfFirstVisible属性的值来确保用户在缩小视图中调用的组显示在放大视图中,这将导致ListView跳转到数据中的正确位置。
这种添加的结果是调用缩小的ListView中的项目将导致在放大的ListView中向用户显示相应的组。你可以在图 16-9 的缩小视图中看到调用J组的效果。
***图 16-9。*在缩小的 ListView 控件中调用一个组的效果
响应放大的 ListView 控件中的滚动
我现在想设置互补关系,以便在放大的视图中滚动内容。我在ListContext.js文件中为放大的ListView控件上的scroll事件添加了一个处理函数,如清单 16-16 所示
清单 16-16 。为放大的 ListView 控件添加滚动事件处理程序
`(function () {
WinJS.UI.Pages.define("/pages/ListContext.html", { ready: function () {
zoomedOut.addEventListener("iteminvoked", function (e) { e.detail.itemPromise.then(function (item) { var invokedGroup = item.key; zoomedIn.winControl.groupDataSource.itemFromKey(invokedGroup) .then(function (item) { var index = item.firstItemIndexHint; zoomedIn.winControl.indexOfFirstVisible = index; }); }); });
** zoomedIn.addEventListener("scroll", function (e) { var firstIndex = zoomedIn.winControl.indexOfFirstVisible;**
** zoomedIn.winControl.itemDataSource.itemFromIndex(firstIndex)**
** .then(function (item) {**
** zoomedOut.winControl.itemDataSource.itemFromKey(item.groupKey)**
** .then(function (item) {**
** zoomedOut.winControl.ensureVisible(item.index);**
** });**
** });**
** }, true);**
}
});
})();`
要从一个ListView控件接收scroll事件,你必须将addEventListener方法的可选第三个参数设置为true,这样事件就会从包含在生成事件的ListView中的元素(称为视窗)中冒出来:
`... zoomedIn.addEventListener("scroll", function (e) { //...function statements go here...
}, true); ...`
如果省略该参数,该值默认为false,并且该事件不会触发您的处理函数。在我的代码中,我通过读取图 16 中indexOfFirstVisible属性的值来响应scroll事件——找出哪个元素在放大的ListView中位于最左侧。
我使用这个元素的索引,通过调用数据源上的itemFromIndex方法来获取数据项本身——这是另一个在满足Promise时返回数据项的方法。
一旦有了条目,我就使用groupKey属性(在分组数据源中的条目上定义)来标识最左边的组,并确保相应的条目在缩小的ListView中可见。结果是两个ListView项目现在同步了。如果您在缩小视图中调用一个项目,相应的组将显示在放大视图中。同样,当您滚动放大视图时,代表当前显示组的项目总是可见的。
处理最后一项
我还没有完全达到我想要的效果。我编写的代码确保了与放大的ListView中的第一个组相对应的缩小项目是可见的,但是它没有很好地处理数据源中的最后几个组。你可以在图 16-10 中看到这个问题,它显示了当我滚动放大的ListView来显示最终的元素组时的效果。
***图 16-10。*最后几组内容有问题
如图 16-所示,缩小后的ListView没有显示与放大视图中最后几组相对应的项目。为了解决这个问题,我需要添加一个事件处理程序代码来专门检查显示中的最后一组,如清单 16-17 中的所示。
清单 16-17 。确保最后一组得到正确处理
`(function () {
WinJS.UI.Pages.define("/pages/ListContext.html", { ready: function () {
zoomedOut.addEventListener("iteminvoked", function (e) { e.detail.itemPromise.then(function (item) { var invokedGroup = item.key; zoomedIn.winControl.groupDataSource.itemFromKey(invokedGroup) .then(function (item) { var index = item.firstItemIndexHint; zoomedIn.winControl.indexOfFirstVisible = index; }); }); });
** zoomedIn.addEventListener("scroll", function (e) {**
** var firstIndex = zoomedIn.winControl.indexOfFirstVisible;**
** var lastIndex = zoomedIn.winControl.indexOfLastVisible; zoomedIn.winControl.itemDataSource.getCount().then(function (count) {**
** var targetIndex = lastIndex == count - 1 ? lastIndex : firstIndex;**
** zoomedIn.winControl.itemDataSource.itemFromIndex(targetIndex)** ** .then(function (item) {** ** zoomedOut.winControl.itemDataSource.itemFromKey(item.groupKey)** ** .then(function (item) {** ** zoomedOut.winControl.ensureVisible(item.index);** ** });** ** });** ** });** ** }, true);** } });
})();`
与上一个例子的不同之处在于,我通过使用ListView控件的count方法和indexOfLastVisible属性来检查数据源中的最后一项是否可见,这两者我都在第十五章的中描述过。如果最后一个数据项可见,那么我确保缩小视图显示最后一组组。您可以在图 16-11 的中看到这种变化的结果,其中您可以看到将放大视图滚动到数据集的末尾会导致缩小控件显示最后几组。
***图 16-11。*确保最后的数据项被正确处理
在缩小的控件中强调过渡
当放大视图滚动时,缩小视图是否保持最新对于用户来说不是很明显。用户的注意力会集中在正在滚动的ListView上,他们不会总是注意到其他地方的细微变化。
为了帮助吸引用户的注意力并强调两个ListView控件之间的双向关系,当放大视图中显示的组发生变化时,我将为缩小的ListView添加一个简短的颜色高亮。
为此,我将使用来自/css/listContext.css文件的两个样式,这是我在本章前面创建示例时添加的,我在清单 16-18 中再次展示了这两个样式。这些样式使用 CSS3 过渡,使样式中定义的值逐渐应用。
清单 16-18 。缩小视图中高亮过渡的样式
`... *.highlighted { color: #4cff00; font-weight: bold; }
*.notHighlighted { color: white; font-weight: normal; -ms-transition-delay: 100ms; -ms-transition-duration: 500ms; } ...`
应用了highlighted类的元素将有绿色和粗体文本。notHighlighted类逆转这些改变,但是在 100 毫秒的延迟之后,在 500 毫秒的方向上进行。为了应用这些样式,我对清单 16-19 中的文件进行了修改。
清单 16-19 。应用 CSS 样式来强调列表视图控件中的项目
`... zoomedIn.addEventListener("scroll", function (e) {
var firstIndex = zoomedIn.winControl.indexOfFirstVisible; var lastIndex = zoomedIn.winControl.indexOfLastVisible;
zoomedIn.winControl.itemDataSource.getCount().then(function (count) { var targetIndex = lastIndex == count - 1 ? lastIndex : firstIndex;
** var promises = {** ** hightlightItem: zoomedIn.winControl.itemDataSource.itemFromIndex(firstIndex),** ** visibleItem: zoomedIn.winControl.itemDataSource.itemFromIndex(targetIndex)** ** };**
** WinJS.Promise.join(promises).then(function (results) {**
zoomedOut.winControl.itemDataSource.itemFromKey(results.visibleItem.groupKey)
.then(function (item) { zoomedOut.winControl.ensureVisible(item.index);
});
** zoomedOut.winControl.itemDataSource.itemFromKey(results.hightlightItem.groupKey)** ** .then(function (item) {** ** var elem = zoomedOut.winControl.elementFromIndex(item.index);** ** $('*.highlighted').removeClass("highlighted")** ** .removeClass("notHighlighted");** ** WinJS.Utilities.addClass(elem, "highlighted");** ** WinJS.Utilities.addClass(elem, "notHighlighted");** ** });**
}); }); }, true); ...`
我使用了WinJS.Promise.join方法的一个特性,它允许您传递一个属性值为Promise对象的对象。由join方法返回的Promise产生的结果包含相同的属性名,但是每个属性名的值是由您在对象中传递的相应的Promise产生的结果——这是使用数组索引的一个很好的替代方法。
否则,添加的代码很简单,当放大视图显示的组发生变化时,我应用 CSS 类来突出显示缩小视图中的项目。这种效果如此之快,以至于我无法轻松地在截图中展示给你,但如果你启动示例应用并滚动浏览放大的内容,你会看到在缩小的视图中出现强调的闪光。
总结
在本章中,我向您展示了SemanticZoom控件,它在整个 Windows 用户体验中扮演着重要的角色,它将两个ListView控件结合在一起,允许用户从一个控件缩放到另一个控件,以在两个不同的细节级别上查看相同的数据。
我还向您展示了一种替代方法,其中整体上下文和详细视图同时显示。当您正在处理的数据在没有更广泛的上下文的情况下有意义时,您应该使用标准的SemanticZoom控件——在我看来,这意味着数据中应用组的方式和在内容中的相对位置是不言而喻的。对于其他数据集,您应该考虑使用一种方法来确保广泛的上下文和精细的细节并排显示。在下一章,我将描述 Windows 应用处理输入事件和触摸手势的方式。
十七、使用指针和手势
到目前为止,在本书中,我一直依赖标准的 DOM 事件,如click和moveover来响应用户输入。对于简单的交互来说,这是一种可行的方法,但是需要不同的技术来充分利用 Windows 应用,特别是支持触摸手势,这使得用户可以轻松地表达复杂的命令。在本章中,我将向您展示如何确定 Windows 8 设备支持哪些输入技术,解释 Windows 如何使用一个通用的指针系统来表示这些输入,以及如何识别触摸手势。表 17-1 对本章进行了总结。
创建示例项目
我创建了一个名为AppInput的项目来演示 Windows 应用可以支持的不同事件和手势。我将在自己的内容页面中展示每个主要功能,因此我创建了一个熟悉的带有NavBar的主内容页面的应用结构,这将允许用户在应用中导航。在清单 17-1 中,您可以看到default.html文件的内容,它将作为示例应用的母版页。
清单 17-1 。default.html 文件的内容
`
AppInputSelect a page from the NavBar
这个文件包含给用户的初始消息和导航条命令,这些命令允许用户导航到我将在本章中添加的五个内容页面。这是一个比我用于 UI 控件更简单的应用结构,例如,我不需要从定义对象生成元素。
定义 CSS
所有内容页面的 CSS 都可以在/css/default.css文件中找到,其内容可以在清单 17-2 中看到。这个文件中没有新的技术,我把它包括进来只是为了让你能看到这个示例应用的每个方面。
清单 17-2 。/css/default.css 文件的内容
`body { background-color: #5A8463; display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: center; -ms-flex-pack: center;}
.container { display: -ms-flexbox; -ms-flex-direction: row; -ms-flex-align: stretch; -ms-flex-pack: center; }
.panel { border: medium white solid; margin: 10px; padding: 20px; display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch; -ms-flex-pack: center; text-align: center;}
.sectionHeader { font-size: 30pt; text-align: center; padding-bottom: 10px;}
.coloredRect { background-color: black; color: white; width: 300px; height: 300px; margin: 20px; font-size: 40pt; display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch; -ms-flex-pack: center; }
#eventList { width: 500px; height: 500px;}
.eventDisplay { background-color: #5A8463;} .pointerDetail, .eventDetail, .primaryDetail { display: inline-block; width: 250px; font-size: 20pt; text-align: left;} .pointerDetail {width: 100px;} .primaryDetail {width: 75px;}
input.cinput {width: 75px;display: inline-block;margin-left: 20px;font-size: 18pt;} .imageRect {width: 600px;height: 80vh;}
#capabilitiesContainer div.panel {-ms-flex-pack: start;} .capabilityTitle {text-align: right; width: 250px; } span.capabilityResult { text-align: left; font-weight: bold; width: 80px; } div.capability {font-size: 20pt;width: 350px;} div.capability > * {display: inline-block;padding-bottom: 10px;}`
定义 JavaScript
/js/default.js文件包含应用的导航代码,使用了您现在熟悉的相同模式。这是一个稍微简化的版本,因为我在这个应用中使用的唯一数据绑定是在模板中,所以当我加载新内容页面时,我不必调用WinJS.Binding.processAll方法。与我的其他示例页面一样,导航代码从 Visual Studio 项目的pages文件夹中加载内容文件。您可以在清单 17-3 中看到default.js文件的内容。
清单 17-3 。/js/default.js 文件的内容
`(function () { "use strict";
var app = WinJS.Application;
WinJS.Navigation.addEventListener("navigating", function (e) {
WinJS.UI.Animation.exitPage(contentTarget.children).then(function () { WinJS.Utilities.empty(contentTarget); WinJS.UI.Pages.render(e.detail.location, contentTarget) .then(function () { return WinJS.UI.Animation.enterPage(contentTarget.children) }); }); });
app.onactivated = function (eventObject) { WinJS.UI.processAll().then(function () { navbar.addEventListener("click", function (e) { var navTarget = "pages/" + e.target.winControl.id + ".html"; WinJS.Navigation.navigate(navTarget); }); }) };
app.start(); })();`
图 17-1 显示了应用的基本布局,如果您此时运行应用并调出导航条,您将会看到这个布局。在接下来的部分中,我将向应用添加内容页面,以演示应用支持指针和手势的不同方式。
***图 17-1。*app 的初始布局
确定设备的输入能力
本章的起点是计算用户设备支持何种输入形式的技术。您可以通过使用Windows.Devices.Input名称空间中的对象来获取这些信息,该名称空间提供了关于设备对键盘、鼠标和触摸交互的支持的信息。
确定哪些输入功能可用对于定制应用呈现的用户体验非常有用。例如,如果没有键盘,您可能不想显示允许用户配置键盘快捷键的设置。(题外话,我在第二十章中向你展示了如何管理应用设置并呈现给用户)。
为了演示如何确定设备输入功能,我在示例 Visual Studio 项目的 pages 文件夹中添加了一个名为DeviceCapabilities.html的新文件。你可以在清单 17-4 中看到这个文件的内容。
清单 17-4 。DeviceCapabilities.html 文件的内容
`
}]); } });function generateCapabilityPanel(name, capabilities) { panelTemplate.winControl.render({section: name}).then(function (panelElem) { WinJS.Utilities.addClass(panelElem, "panel"); capabilities.forEach(function (capability) { capabilityTemplate.winControl.render(capability) .then(function (capabilityTemplate) { WinJS.Utilities.addClass(capabilityTemplate, "capability"); panelElem.appendChild(capabilityTemplate); }); }); capabilitiesContainer.appendChild(panelElem); }); }
`
我已经使用WinJS.Binding.Template功能生成了一个显示设备功能的布局。generateCapabilityPanel函数接受一个标题和一组功能,并在 DOM 中生成元素来显示它们。我采用这种方法,这样我就不必列出大量的 HTML 标记,因为我想展示这种形式的模板绑定。
当我调用Template.render方法时,我接收到一个Promise对象,当从模板中生成新元素时,该对象被满足。传递给then函数的对象是已经创建的顶级元素(这是因为我省略了render方法的第二个参数,它指定了模板元素将要插入的容器元素——如果我指定了一个元素,那么这个容器元素将被传递给then函数)。
我使用这种方法是因为Template控件在从模板创建元素时去掉了class属性的值。为了解决这个问题,我使用了WinJS.Utilities.addClass方法,并使用 DOM appendChild方法将模板元素插入到文档中。这些都与设备功能没有直接关系,但是我忍不住展示了使用 WinJS 模板的另一种方法。
确定键盘功能
您可以通过创建一个新的Windows.Devices.Input.KeyboardCapabilities对象来获取有关用户设备键盘功能的信息。该对象只定义一个属性,该属性指示设备上是否有键盘。我已经在表 17-2 中总结了这个属性,以便您在以后返回本章时可以轻松找到它,并且不想阅读文本来查找属性名称。
在清单 17-5 中,您可以看到我是如何创建KeyboardCapabilities对象并读取属性的值的。注意,这个属性返回的是1而不是true,因此为了得到一个true / false值,我必须将属性值与数字文字值1进行比较。
清单 17-5 。检测硬件键盘的存在
... var kbd = new input.KeyboardCapabilities(); generateCapabilityPanel("Keyboard", [{ name: "Keyboard Present", value: kbd.keyboardPresent == 1 }]); ...
keyboardPresent属性与硬件键盘相关。没有硬件键盘的设备仍然支持通过软件键盘输入文本,当合适的 HTML 元素(如input或textarea元素)获得焦点时,软件键盘会自动显示文本。
如果此时运行应用,并使用Capabilities NavBar 命令导航,您将会看到如图图 17-2 所示的内容。图 17 显示了在我的开发 PC 上运行的应用,正如你所料,它有一个键盘。
***图 17-2。*确定设备是否有键盘
确定鼠标功能
您可以通过Windows.Devices.Input.MouseCapabilities对象获得关于鼠标的信息,该对象定义了表 17-3 中所示的属性。
注意不要因为SwapButtons属性返回1就认为用户是左撇子。如果你有一个可以从左手和右手配置中受益的界面,那么把SwapButtons值作为一个提示,但是要求用户明确地确认他们想要一个替代的配置。在清单 17-6 中,你可以看到我是如何确定和显示由DeviceCapabilities.html文件的脚本元素中的示例应用中的MouseCapabilities对象定义的属性值的。
清单 17-6 。确定鼠标的存在和功能
`...
...`
我建议在对鼠标做假设时要谨慎。虽然大多数用户以标准的方式使用鼠标,但是惊人数量的用户重新配置鼠标的操作方式,以便在某些情况下重新映射按钮或执行宏。重新配置鼠标设备的软件质量变化很大,好的软件通过重新映射操作系统的硬件功能工作得很好。写得很差的代码(到处都有)使用了各种令人讨厌的黑客手段,你从用户那里得到的输入并不总是与 Windows 报告的功能相对应。我的建议是不要假设用户将能够使用垂直滚轮,例如,当verticalWheelPresent属性返回1时,它很可能完全被重新映射到一些其他函数,因此您应该对如何在您的应用中导航内容保持灵活。在图 17-3 中,你可以看到在我的开发 PC 上运行应用并选择Capabilities导航条命令的结果。
提示Visual Studio 模拟器将总是报告一个不带滚轮的 2 键鼠标,而不管连接到运行模拟器的 PC 的硬件如何。
***图 17-3。*确定当前设备的鼠标功能
确定触摸能力
最后一类输入是触摸,包括触摸屏和数字化平板电脑在内的各种设备类型。您可以使用Windows.Devices.Input.TouchCapabilities对象查看设备是否支持触摸,该对象定义了表 17-4 中所示的属性。
如果设备有多个触摸面,那么contacts属性返回能力最差的一个支持的接触点的数量。你可以看到我是如何使用清单 17-7 中的TouchCapabilities对象的,它显示了我对DeviceCapabilities.html文件的script元素的添加。
注意我发现当
TouchPresent属性描述的设备有更多接触点时,它经常返回1。
清单 17-7 。使用 TouchCapabilities 对象
`...
...`
你可以在图 17-4 中看到这段代码生成的结果。我已经在我的开发 PC 上运行了示例应用,我在 PC 上连接了一个便宜的数字化平板。
***图 17-4。*显示设备触摸功能的详细信息
处理指针事件
您可以在 Windows 应用中愉快地使用标准的 DOM 事件,如click和mouseover。负责运行 JavaScript Windows 应用的 Internet Explorer 10 将生成这些事件,而不管用于生成它们的输入设备是什么。这意味着,例如,当点击一个button元素时,它将触发一个click事件,而不管用户是用鼠标、触摸屏上的手指还是数字化仪上的笔进行交互。这些事件由 IE10 生成,以提供与 web 应用的兼容性,您可以在您的应用代码中非常安全地使用它们,正如我在本书的示例应用中所做的那样。
然而,如果你想使用 Windows 触摸手势,那么你需要使用MSPointer事件。这些事件与 web 应用开发中的标准 DOM 事件相对应,但是它们具有额外的属性,这些属性提供了所使用的输入类型的详细信息。在表 17-5 中,我列出了MSPointer事件,并描述了它们被触发的情况。没有叫MSPointer的事件——这个名字指的是事件名称以MSPointer—MSPointerDown、MSPointerUp等开始。
提示
MSPointer事件与 DOM 兼容性事件一起生成。您可以毫无问题地监听像MSPointerMove和mousemove这样的混合事件,尽管有时在MSPointer事件和被触发的标准 DOM 事件之间会有一点延迟。
对这些事件的描述必然是模糊的,因为它们涉及广泛的交互类型。当用户点击鼠标时,当手指或手写笔触摸屏幕时,或者使用不太常见的设备进行其他交互时,指针可以触摸一个元素。为了演示这些事件是如何工作的,我在示例 Visual Studio 项目的pages文件夹中添加了一个名为PointerEvents.html的文件,您可以在清单 17-8 中看到。
清单 17-8 。PointerEvents.html 文件的内容
`
WinJS.UI.Pages.define("/pages/PointerEvents.html", { ready: function () { var eventTypes = [ "MSPointerUp", "MSPointerDown","MSPointerOut", "MSPointerOver","MSPointerCancel","MSPointerHover", /*"MSPointerMove" */ "MSGotPointerCapture", "MSLostPointerCapture"];
eventTypes.forEach(function (eventType) { targetElem.addEventListener(eventType, function (e) {
eventList.unshift(e);
}), true;
});
}
});
这个页面的布局由一个简单的彩色块组成,您可以与它交互以生成事件。我通过将事件添加到一个WinJS.Binding.List对象来处理它们,该对象是一个ListView控件的数据源(你可以在第八章的中了解到WinJS.Binding.List对象,在第十五章的中了解到ListView UI 控件)。你可以在图 17-5 中看到结果,显示了简单交互后的内容。
***图 17-5。*响应 MSPointer 事件
您可以像注册常规 DOM 事件一样注册您对指针事件的兴趣——但是您必须注意使用正确的大写。例如,事件名称是MSPointerDown,而不是mspointerdown、MsPointerDown或任何其他排列。你可以在清单 17-9 中看到我是如何设置我的处理函数的。我已经注释掉了MSPointerMove事件,因为任何交互都会产生许多这样的事件,很难发现其他类型的事件。
清单 17-9 。注册一个函数来处理 MSPointer 事件
`... var eventTypes = [ "MSPointerUp", "MSPointerDown","MSPointerOut", "MSPointerOver","MSPointerCancel","MSPointerHover", ** /"MSPointerMove" / "MSGotPointerCapture", "MSLostPointerCapture"];
eventTypes.forEach(function (eventType) { targetElem.addEventListener(eventType, function (e) { eventList.unshift(e); }), true; }); ...`
MSPointer事件并不直接等同于 HTML DOM 中的事件,但在很大程度上,它们非常相似,并且以与仅使用鼠标的交互一致的方式触发。例如,当用户触摸屏幕上的元素时,就会触发MSPointerDown和MSPointerUp事件,无论用户使用的是鼠标还是触摸屏,都是如此。
有两件事很麻烦。MSPointerHover事件是指当一个点在一个元素上移动而没有接触到屏幕时被触发。这听起来很合理,但我无法在真实的硬件上触发这个事件——尽管在应用模拟器中触发它很容易(只需选择模拟器窗口右边缘的Basic Touch Mode按钮,并将鼠标指针移动到元素上)。
我也不能触发MSPointerCancel事件。当设备中止交互时会触发此事件,微软给出的例子是当同时触摸点的数量超过触摸屏或数字化仪处理它们的能力时。我已经在我能找到的所有硬件上测试了清单 17-8 中的代码,但是还不能触发这个事件。
获取指针信息
一个MSPointer事件的处理函数被传递一个MSPointerEvent对象,它实现了一个常规 DOM Event的所有方法和属性,但是增加了一些内容。许多附加功能被系统用来计算多个事件是如何形成一个手势的,但是有一些属性可能更有用,我已经在表 17-6 中描述了这些属性。
你可以看到我是如何通过我的ListView模板显示这些属性的,如清单 17-10 所示。
清单 17-10 。在 ListView 模板中显示选中的 MSPointerEvent 值
`...
通过选择不同的输入模式,您可以在模拟器中生成不同类型的事件。模拟器窗口右侧的按钮允许您在鼠标和触摸输入之间移动。
导致MSPointer事件被触发的指针类型可通过pointerType属性获得。该属性返回一个数值,您可以将其与由MSPointerEvent对象定义的此类值的枚举进行比较。在这个例子中,我使用了一个绑定转换器将数值转换成有意义的文本,如清单 17-11 中的所示。
清单 17-11 。确定 MSPointer 事件的类型
... var pointerTypeConverter = WinJS.Binding.converter(function (typeCode) { switch (typeCode) { case MSPointerEvent.MSPOINTER_TYPE_MOUSE: return "Mouse"; case MSPointerEvent.MSPOINTER_TYPE_PEN: return "Pen"; case MSPointerEvent.MSPOINTER_TYPE_TOUCH: return "Touch"; default: return "Unknown"; } }); ...
值MSPOINTER_TYPE_MOUSE、MSPOINTER_TYPE_PEN和MSPOINTER_TYPE_TOUCH对应于pointerType属性返回的值。
提示来自数字化平板的事件通常被报告为
MSPOINTER_TYPE_MOUSE事件,而不是MSPOINTER_TYPE_PEN事件。这取决于数字化仪硬件是如何被识别的——我测试过的许多输入设备对系统来说就像鼠标一样,大概是为了更广泛的兼容性。
我在模板中显示的第三个字段指示事件是否由输入设备上的主要点生成。这与多点触摸设备有关,其中第一个接触点(通常是触摸屏幕的第一个手指)被认为是主要点。响应其他手指的移动或接触而触发的事件将为isPrimary属性返回false。
处理手势
手势是以特定顺序接收的一系列事件。因此,例如,一个点击手势由一个MSPointerDown事件组成,在某个时刻,后面跟着一个MSPointerUp事件。我说在某些时候是因为手势交互是复杂的——用户的手指可能会在指针被按下然后释放的时刻之间移动,指针可能会移动到你正在收听的事件所在的元素之外,等等。微软已经包含了一些有用的工具,可以更容易地处理手势,而不必处理它们派生的单个事件。在接下来的部分中,我将向您展示如何在用户执行手势时接收通知,以及您可以在应用中响应手势的一些不同方式。
处理手势可能相当复杂,所以我将从最简单的手势开始,逐步增加到更复杂的。为了演示基础知识,我在 Visual Studio 项目的pages目录中添加了一个名为Gestures.html的文件,你可以在清单 17-12 中看到。
清单 17-12 。Gestures.html 文件的内容
`
return "Start"; } else if (detail == MSGestureEvent.MSGESTURE_FLAG_END) { return "End"; } else if (detail == MSGestureEvent.MSGESTURE_FLAG_CANCEL) { return "Cancel"; } else { return ""; } });WinJS.UI.Pages.define("/pages/Gestures.html", { ready: function () {
var eventTypes = ["MSPointerDown", "MSGestureTap", "MSGestureHold"];
var ges = new MSGesture(); ges.target = targetElem;
eventTypes.forEach(function (eventType) { targetElem.addEventListener(eventType, function (e) { if (e.type == "MSPointerDown") { ges.addPointer(e.pointerId); } else { eventList.unshift(e); } }, false); }); } });
该内容遵循我用于指针事件的相同模式。有一个彩色的矩形,我监听它的事件——但是在这个例子中有一些关键的不同。
最基本的手势是tap和hold,分别用MSGestureTap和MSGestureHold事件表示。为了从一个元素接收这些事件,我必须创建一个MSGesture对象,并告诉它我希望它对哪个元素进行操作,如清单 17-13 所示。
清单 17-13 。创建 MSGesture 对象
... var ges = new MSGesture(); ges.target = targetElem; ...
MSGesture对象内置于 Internet Explorer 10 中,因此您不需要使用名称空间来引用它。使用target属性设置想要接收手势事件的元素——在本例中,我指定了作为我的彩色矩形的div元素。
提示
MSGesture对象只处理单一元素。如果您想要接收多个元素的手势事件,那么您需要为每个元素创建MSGesture对象。
MSGesture对象解除了您跟踪单个MSPointer事件的责任,但是您需要告诉它,通过addPointer方法传递来自MSPointerDown事件的细节,一个新的手势可能正在开始,该方法接受由MSPointerEvent对象的pointerId属性返回的值,如清单 17-14 所示。
清单 17-14 。开始一个手势
... ges.addPointer(e.pointerId); ...
此时,您不需要做任何其他事情——MSGesture对象将跟踪来自元素的事件,并在手势出现时生成事件。手势事件的处理函数被传递一个MSGestureEvent对象,该对象包含关于手势的信息。点击手势没有额外的细节,但是保持手势可以导致多个MSGestureHold事件被触发。您可以通过读取detail属性并将其与MSGestureEvent对象枚举的值进行比较来确定这些事件的重要性,如表 17-7 中所述。
在例子中,我使用一个ListView和一个非常简单的项目模板来响应事件的基本手势和细节作为列表。我已经定义了一个绑定转换器,这样我就可以读取detail值并显示一个有意义的字符串,如清单 17-15 中的所示。
清单 17-15 。通过读取细节属性确定手势事件的重要性
... var holdConverter = WinJS.Binding.converter(function (detail) { if (detail == MSGestureEvent.MSGESTURE_FLAG_BEGIN) { return "Start"; } else if (detail == MSGestureEvent.MSGESTURE_FLAG_END) { return "End"; } else if (detail == MSGestureEvent.MSGESTURE_FLAG_CANCEL) { return "Cancel"; } else { return ""; } }); ...
你可以在图 17-6 中看到这些值是如何显示的,以及手势事件的类型。
***图 17-6。*响应顶部和保持手势
表演基本手势
为了测试这个例子,您需要知道如何执行手势。在介绍每个手势时,我将向您展示如何使用鼠标在触摸屏上创建它,以及如何使用鼠标在 Visual Studio 模拟器中模拟触摸。
要使用鼠标执行点击手势,只需在元素上单击鼠标按钮并立即释放——对于鼠标使用,执行点击手势与生成click事件并触发MSGestureTap事件是一样的。要执行保持手势,单击鼠标按钮并按住它——几秒钟后会触发MSGestureHold事件。
在触摸屏上,用一个手指按下并立即释放元素以执行点击手势,然后按住(即,不要将手指从屏幕上移开)以执行保持手势。
在模拟器中,使用模拟器窗口右边缘的按钮选择Basic Touch Mode,如图 17-7 中的所示。光标变为代表手指(显示为带有十字光标的大圆)。您的光标现在是一个手指——按下鼠标按钮模拟用手指触摸屏幕,松开按钮模拟移开手指。如果你想回到模拟器中常规的鼠标交互,那么选择Mouse Mode,它就在Basic Touch Mode按钮的正上方。
***图 17-7。*在 Visual Studio 模拟器中选择基本触摸模式
搬运操作
操作是更复杂的手势,允许用户缩放、旋转、平移和滑动元素。为了演示操作手势,我在 Visual Studio 项目中添加了一个名为Manipulations.html的文件。你可以在清单 17-16 中看到这个文件的内容。
清单 17-16 。Manipulations.html 文件的内容
`
function filterGesture(e) { var matrix = new MSCSSMatrix(e.target.style.transform); switch (e.target.id) { case "rotate": return matrix.rotate(e.rotation * 180 / Math.PI); break; case "scale": return matrix.scale(e.scale); break; case "pan": return matrix.translate(e.translationX, e.translationY) break; }; }var ids = ["rotate", "scale", "pan"]; var elems = []; var gestures = [];
ids.forEach(function (id) { elems[id] = document.getElementById(id); gestures[id] = new MSGesture(); gestures[id].target = elems[id]; eventTypes.forEach(function (eventType) { elems[id].addEventListener(eventType, handleGestureEvent); }); }); } });
对于这个例子,我已经创建了三个目标元素,每个元素都用我将在接下来的小节中应用的操作进行了标记。你可以在图 17-8 中看到初始布局。
***图 17-8。*可以应用操作手势的元素
表演操纵手势
这个示例演示的操作手势是旋转、缩放和平移,在深入研究示例代码之前,我将向您展示如何执行每个手势。您需要在示例中的相应元素上执行每个手势。
注意您可以使用触摸或鼠标进行平移动作,但旋转和缩放手势仅适用于触摸。
旋转元素
要旋转一个元素,用两个手指触摸屏幕,并围绕一个中心点做圆周运动。要在模拟器中执行该手势,从模拟器窗口的右边选择Rotation Touch Mode按钮。光标将变为两个圆圈,代表手势的两个手指。将光标放在元素上,并按住鼠标按钮。向上滚动垂直鼠标滚轮执行逆时针旋转,向下滚动垂直鼠标滚轮执行顺时针旋转。释放鼠标按钮以完成手势。在图 17-9 中可以看到Rotation Touch Mode按钮、模拟光标和效果。
提示不按鼠标键滚动鼠标滚轮,改变模拟手指的初始位置。
***图 17-9。*选择旋转触摸模式,旋转一个元素
缩放元素
要缩放一个元素(也称为*捏/缩放手势)*将两个手指放在显示屏上,将它们分开以放大元素。一起移动手指会缩小元素。要在模拟器中模拟该手势,选择Pinch/Zoom Touch Mode按钮。光标将变为代表两个手指。在元素上按住鼠标按钮以开始手势,并使用鼠标滚轮调整接触点–向上滚动滚轮会将接触点分开,向下滚动滚轮会将它们一起移动。释放鼠标按钮以完成手势。在图 17-10 中可以看到Pinch/Zoom Touch Mode按钮、模拟光标和缩放手势的效果。
提示两个接触点都需要在元素内才能发起手势。在不按下按钮的情况下使用鼠标滚轮来调整接触点之间的距离,以使它们合适。
***图 17-10。*选择挤压/缩放触摸模式并缩放一个元素
平移元素
平移手势是唯一可以使用鼠标执行的操作:只需在元素上按住鼠标按钮并移动鼠标,元素就会跟随鼠标指针移动。手势的工作方式与触摸非常相似——触摸元素,然后移动手指在屏幕上移动元素。要模拟触摸手势,选择Basic Touch Mode并按下鼠标按钮,模拟将手指放在屏幕上。
处理操纵手势事件
使用手势时,您需要为每个想要变换的元素创建一个MSGesture对象,并使用target元素来关联它所应用的元素。在清单 17-17 中,您可以看到我是如何在示例中做到这一点的,在数组上使用了forEach方法,这样我就可以用相同的方式设置所有的元素,并表达对相同事件集的兴趣。
清单 17-17 。设置 MSGesture 对象并监听操作手势事件
`... var eventTypes = ["MSPointerDown", "MSGestureStart", "MSGestureEnd", "MSGestureChange"]; ... var ids = ["rotate", "scale", "pan"]; var elems = []; var gestures = [];
ids.forEach(function (id) { elems[id] = document.getElementById(id); gestures[id] = new MSGesture(); gestures[id].target = elems[id]; eventTypes.forEach(function (eventType) { elems[id].addEventListener(eventType, handleGestureEvent); }); }); ...`
操纵手势有它们自己的一套事件,我在表 17-8 中描述了这些事件。当用户开始执行操作手势时触发MSGestureStart事件,当手势完成时触发MSGestureEnd事件。当用户移动指针时,使用MSGestureChange事件发送关于手势的更新。
系统不会区分不同的手势。这些事件的处理函数被传递给一个包含附加属性的MSGestureEvent对象,该属性包含用户所做的旋转、缩放和平移的详细信息。由你来决定你想对这些运动的哪些方面做出反应。我在表 17-9 中总结了这些特性。
这种方法的好处是用户可以同时执行多个手势。您可以选择要读取的属性值,并忽略那些表示您不感兴趣的手势的属性值。这是我在示例中采用的方法,我只想为布局中的每个元素支持一种手势。
使用 CSS3 转换响应操作
你可以在应用中以任何有意义的方式响应操作手势,但如果你想让用户直接操作布局中的元素,那么最简单的方法是使用 CSS3 转换,这在 Internet Explorer 10 中是受支持的。你可以在例子中看到我是如何使用清单 17-18 中的函数来完成的。
清单 17-18 。处理操作手势事件的细节
... function filterGesture(e) { var matrix = new **MSCSSMatrix**(e.target.style.transform); switch (e.target.id) { case "rotate": return **matrix.rotate**(e.rotation * 180 / Math.PI); break; case "scale": return **matrix.scale**(e.scale); break; case "pan": return **matrix.translate**(e.translationX, e.translationY) break; }; } ...
表示布局中元素的每个 DOM 对象都有一个style.transform属性,您可以将它用作MSCSSMatrix对象的构造函数参数。传递给事件处理函数的每个MSGestureEvent对象包含自上次事件以来每次操作的变化量,而MSCSSMatrix对象使得通过rotate、scale和translate方法应用这些变化变得容易。
提示
CSSMatrix对象和元素style.transform属性是 CSS3 过渡和转换特性的一部分,我将在第十八章的中详细介绍。
唯一不方便的是,MSGestureEvent.rotation值是用弧度表示的,而 CSS3 转换是用度数表示的——您可以在清单中看到我是如何从一种转换到另一种的。你必须记住将rotate、scale或translate方法的结果设置为用户正在操作的元素的transform属性,如清单 17-19 所示。
清单 17-19 。将更新后的变换应用于被操纵的元素
function handleGestureEvent(e) { if (e.type == "MSPointerDown") { gestures[e.target.id].addPointer(e.pointerId); } else { ** e.target.style.transform = filterGesture(e);** } }
你可以在图 17-11 中看到操纵所有三个元素的结果。
***图 17-11。*操纵示例中的元素
使用内容缩放
内容缩放允许你缩放一个元素的内容,而不是元素本身。你可以在图 17-8 中看到,我放大了其中一个元素,以至于它溢出了初始边界——这是一个巧妙的技巧,但有时你只是想让用户在原位缩放内容,这就是内容缩放允许的。为了演示这个特性,我在示例 Visual Studio 项目的pages文件夹中添加了一个名为CSSGestures.html的新文件。你可以在清单 17-20 中看到这个文件的内容。
清单 17-20 。CSSGestures.html 文件的内容
`
** #contentZoom {** ** overflow: scroll;** ** -ms-content-zooming: zoom;** ** -ms-content-zoom-limit-min: 50%;** ** -ms-content-zoom-limit-max: 200%;** ** -ms-overflow-style: -ms-autohiding-scrollbar;** ** -ms-content-zoom-snap-points: snapList(50%, 75%, 100%, 200%);** ** -ms-content-zoom-snap-type: mandatory;** ** }**使用 CSS 配置内容缩放功能,本例中没有script块。我依赖于一个名为aster.jpg的图像文件,我已经将它添加到 Visual Studio 项目的images目录中——你可以在这个例子中使用任何图像,我使用的图像包含在本章的源代码下载中,可以从Apress.com获得(并且是我在第十四章的中用来演示WinJS.UI.FlipView控件的图像之一)。
本章这一节的内容非常简单。图像以相当大的格式显示。用户可以触摸屏幕并做出缩放手势来缩放图像。在图 17-12 中可以看到布局的原始状态和放大后的内容。该手势与标准缩放手势的主要区别在于,图像已在其容器的边界内缩放,但容器保持不变。
***图 17-12。*使用内容缩放手势
控制内容缩放手势的 CSS 属性在表 17-10 中进行了描述,在接下来的章节中,我将向您展示如何使用它们,并解释支持值的范围。
启用内容缩放功能
启用内容缩放手势需要两个属性。首先,您必须将 CSS overflow属性设置为scroll。该属性并不特定于内容缩放,但它是启用该功能所必需的。第二个属性是–ms-content-zooming,必须设置为zoom。结合这些属性可以启用元素的内容缩放功能。
你必须将这些属性应用到一个有内容的元素上,如清单 17-21 所示。您可以看到内容是一个img元素,它包含在一个 div 中。这是内容缩放手势应用到的div元素。
清单 17-21 。对包含内容的元素应用内容缩放功能
`
** #contentZoom** { ** overflow: scroll; ** ** -ms-content-zooming: zoom;** /* ...other properties removed for brevity... */ }应用缩放限制
您可以使用-ms-content-zoom-limit-min和ms-content-zoom-limit-max属性来限制内容的缩放量。这些以原始大小的百分比表示。用户可以在执行捏合/缩放手势时将内容缩放到这些限制之外,但当手势结束时,它会迅速恢复到该限制。在这个例子中,我设置了 50%的最小比例和 200%的最大比例,如清单 17-22 中的所示。设置这些属性时,不要忘记%符号。
清单 17-22 。应用缩放限制
... -ms-content-zoom-limit-min: 50%; -ms-content-zoom-limit-max: 200%; ...
提示
-ms-content-zoom-limit便利属性允许您在一条语句中指定最小值和最大值。
设置缩放内容样式
-ms-overflow-style属性允许您配置内容缩放后的显示方式。该属性支持的值如表 17-11 所示。
我倾向于使用-ms-autohiding-scrollbar值,它可以确保用户意识到他们可以在元素的内容周围平移,但是使用滚动条,滚动条覆盖在内容的顶部,并且只在用户与内容交互时显示。相比之下,由scrollbar值应用的滚动条增加了元素的大小,并且总是被显示。你可以在图 17-13 中看到不同之处。左图显示了-ms-autohiding-scrollbar值的效果,右图显示了scrollbar值。
***图 17-13。*使用内容缩放功能时滚动条的不同样式
限制缩放级别的范围
您可以通过应用-ms-content-zoom-snap-points属性来限制缩放级别的范围。用户在执行收缩/缩放手势时可以缩放到任何级别,但是内容将会靠齐该属性指定的最接近的缩放级别。有两种方法指定捕捉点,如表 17-12 所述。
以编程方式使用内容缩放
虽然 CSS 用于设置内容缩放,但它还有一些编程特性。当用户改变内容的比例时,MSContentZoom事件被触发,您可以使用msContentZoomFactor属性获取或设置缩放因子。清单 17-23 显示了对CSSGestures.html文件的一些添加,以演示事件和属性的使用。
清单 17-23 。程序化内容缩放功能
`
#contentZoom { overflow: scroll; -ms-content-zooming: zoom; -ms-content-zoom-limit-min: 50%; -ms-content-zoom-limit-max: 200%; -ms-overflow-style: -ms-autohiding-scrollbar; -ms-content-zoom-snap-points: snapList(50%, 75%, 100%, 200%); -ms-content-zoom-snap-type: mandatory; } **** zoomFactor.addEventListener("change", function (e) {**
** contentZoom.msContentZoomFactor = zoomFactor.value;**
** });**
** }**
** }); **
**
Zoom Factor:
** ** ** **我在布局中添加了一个input元素,当MSContentZoom事件被触发时,这个元素会被更新。当输入元素的值改变时,我通过更新msContentZoomFactor属性来处理change事件。在图 17-14 中可以看到修改后的布局。
注意
msContentZoomFactor是直接在HTMLElement对象上定义的,而不是由 WinJS 定义的。这意味着您不需要使用winControl来访问属性——您可以直接从document.getElementById方法返回的对象中获取或设置值。
***图 17-14。*在 JavaScript 中使用内容缩放功能
总结
在这一章中,我向您展示了如何确定设备上可用的输入形式,以及如何使用MSPointer事件来响应用户交互。这些事件提供了对常规 DOM 事件的重要增强,因为它们包含了输入机制的细节,可以用来识别手势。我向您展示了手势系统的工作原理,并演示了简单的手势和操作。在本章的最后,我演示了如何使用内容缩放功能来创建另一种效果。这个特性依赖于 CSS——我将在下一章回到这个主题,在那里我将描述 WinJS 动画系统。