课程内容
Ø Panarama控件
Groceries是一个简易的购物清单应用程序,我们可以用它来一步一步建立自定义的购物清单。根据个人的喜好,我们可以命名并添加尽可能多的购物页面。能够方便地添加记录,这是本应用程序的特点,比如,批量添加、选择最喜欢的商品以及选择最近购买的商品等等。
Groceries应用展示了Panorama控件,这是Windows Phone平台上具有标志意义的用户控件,它被广泛地应用于手机上的“hub”界面(例如人脉、图片等等)。粗略地说,Panorama控件的行为与Pivot很类似,它允许在一个页面的不同部分之间进行水平切换。Panorama的与众不同之处就在于它的外观和动态切换。
Panorama的核心理念就是让用户感觉是在浏览一幅很长的水平放置的油画。该控件向用户给出了一些视觉上的元素,指引用户进行水平切换。比如,应用程序的显示的标题要比屏幕的尺寸大(除非标题实在太短),每个Section的大小要比屏幕的尺寸略窄,所以下一个Section的左边界部分就可以在这个界面中显示出来。Panorama支持水平回滚,在最后一个Section继续向右切换,就会回到第一个Section。同样,在第一个Section向左切换,就会导航到最后一个Section页面。
图27.1展示了Groceries应用程序中Panorama控件的用法。第一个Section包含了整个购物清单,最后一个Section包含了购物车(用户已经购买的商品)。在这两个部分之间,可以动态添加多个Section,存放用户希望购买的商品清单,并且展示这些商品是否已经放入购物车。
图27.1 Groceries应用程序中的Panorama控件,展示了购物清单。
虽然长的水平画卷的方式是Panorama控件一贯的界面风格,但这五个Section中的背景图片并不是完全连续的。事实上,这个机制要更加复杂一点。Panorama控件包括三个不同的层,每个层的移动速度不一样,以达到一个视差的效果。背景平移的速度最慢,其次是标题,平移速度最快的是内容,它以普通的滚动/切换的速度进行平移。图27.2展示了访问图27.1中的每个Section时,屏幕所展示的页面内容。
图27.2 访问图27.1中Panorama的每个Section时,展示的页面内容。
在应用程序中,我们应该如何选择使用Panorama或者是Pivot控件?
主要的考虑因素是应用程序想要呈现给用户的视觉外观。与Pivot相比,具有背景画面的Panorama控件可以提供更具吸引力、更加有趣的用户界面。即使在Pivot中使用背景图片,它也不能达到Panorama的效果,主要原因是Panorama提供的视差平移效果。另外,Panorama在同一个Section中支持更加好的水平滚动,这使得宽度可变的Section更容易实现。
在其他方面,Pivot也有胜过Panorama的优势。Pivot展示每个Section更加真实的状态,在内容或者记录数目较多时,Pivot的性能更加出色,原因有三点:外观和过渡更加简洁,采用内容的延时加载机制,为延时加载与卸载提供API。在Pivot中,仍然可以使用应用程序栏(或者程序状态栏),而在Panorama中一般不适用它们。因此,如果我们想要展示一些基于页面的行为,那么,具有应用程序栏的Pivot应该是一个更好的选择。
Groceries应用程序其实应用更适合使用Pivot,而不是Panorama,因为每个页面只是同一个数据集的不同过滤页面而已。一个典型的Panorama中的Section要比Groceries应用中的更加丰富与生动,具有更加多的缩略图(就像我们在Marketplace应用程序中所看到的那样)。但是,通过使用Panorama,Groceries给用户留下了深刻的印象,使用起来也更加有趣。
The Panorama Control
在阅读前一章“Pivot”控件之后,Panorama控件看上去应该是比较熟悉了。Panorama控件位于Microsoft.Phone.Controls二进制集的Microsoft.Phone.Controls命名空间中,它是一个item控件,与PanoramaItem这个content控件配合使用。
虽然Panorama控件的行为要比Pivot复杂得多,但是它提供的API更加少。与Pivot一样,Panorama具有Title 和 TitleTemplate属性,其中,HeaderTemplate属性用来自定义控件中的header。一般情况下,没有必要使用这个属性,因为控件已经提供了很好的外观和感受。因为Panorama的Title是一个类型对象,所以把它设置为logo而非文本,是可以接受的。著名的Facebook应用程序就是这样做的。
PanoramaItem具有Header属性,但是与PivotItem不同,它也为自定义不同外观的Header提供了HeaderTemplate属性(当然,我们可以直接把Heade设置为用户自定义的UI元素,而不需要HeaderTemplate属性)。同时,PanoramaItem具有一个屏幕方向的属性,它可以在内容不合适时,指示合理的滚动方向。该属性的默认值是Vertical,将它设置为Horizontal时,可以使得单个Panorama Item的横向展开宽度大于整个屏幕的宽度。注意,如果我们想要Panorama Item进行垂直的滚动,就必须加入scroll viewer控件。在水平的Panorama Item中,我们不会想着使用scroll viewer控件,Panorama自动会处理。每个水平的Panorama Item具有一个最大的宽度,那就是两个屏幕的大小(960像素)。
Horizontal Panorama Items and Their Headers
系统内置应用中的Panorama控件, Panorama Item在水平状态并且比屏幕要宽时,它的标题的平移速度要比内容的平移速度慢(这就确保了在查看Panorama Item页面时,我们只能看到标题的部分内容)。但是,Panorama控件并不提供这个行为。无论它的宽度有多大,每个Panorama Item的标题移动速度和内容的移动速度相同。
对于Panorama Item中记录的布局来说,我们可以自行设置。虽然Panorama中会使用一些方形图片和文字,但并没有特殊的控件会自动完成这些布局的设置。我们应该使用通用的Panel控件,例如Grid或者Wrap。
The Main Page
Groceries应用程序的主页面如图27.2所示,只有它使用了Panorama。主页面提供了导航到其他四个页面的链接:添加记录页面、编辑记录页面、设置页面和说明页面。这些页面的代码说明在这里省略。
如果应用程序使用了Panorama,一般只使用一个,而且一般也只是用在应用程序的第一页。在一个应用程序中使用多个Panorama,会给熟悉Windows Phone体验的用户带来使用上的困惑。
The User Interface
控件的XML命名空间再次添加了对panorama的引用。
本页面只使用竖屏模式,这也正是我们对每个具有panorama控件的页面所期望的行为。
本页面填充了白色的前景色,这正是考虑到了在light主题和dark主题下,应用程序的外观保持一致。因为背景图片没有改变,所以我们不想让文字的颜色变为黑色。
Panorama的背景和其他元素中的Background属性类似。虽然设计指导中建议我们使用纯色的画刷或者图片画刷,但我们可以把它设置为任何的画刷。该列表利用图片画刷将背景设置为background.jpg。
确保Panorama应用程序在dark 和 light两种主题模式下测试通过!
这对于任何Panorama应用程序都是必须做的一项测试,因为我们经常在设计Panorama时犯错,那就是设置一个固定的背景图片。如果背景从不改变,那么我们就需要确保内容的颜色也从不发生改变。
为了防止图片拉伸,确保我们使用的Panorama背景图片的高度为800像素。另外,为了保证我们应用程序的性能,图片的宽度不应该大于1024像素,并且图片的类型应该是JPEG格式的。Groceries使用了一张分辨率为1024x800的 JPEG图片。在我决定写这个应用程序时,带着我妻子的具备拍摄Panorama图片功能的新相机去一个附近的杂货店拍摄了图片。而这之后,我意识到最好的背景图片其实并不是Panorama类型的。图27.3显示了应用程序的背景图片文件。
图27.3 应用程序的背景图片文件
使用超大分辨率的背景图片会导致背景切换迟滞的问题。事实上,背景切换的速度取决于Panorama Item数量,因为Panorama保证在你切换到最后一页时,才会看到背景图片的结尾。在Groceries应用中,标题“groceries”和背景图片的宽度导致标题与背景图片基本上以相同的速度切换,为了获得更加丰富的视差效果,我们可以改变其中任何一个元素的宽度。
为了获得最好的效果,Panorama应用中的背景图片的Build Action属性应该设置为Resource,而并不是Content。实际应用中很少使用资源文件,本应用是其中之一,原因在于同步和异步加载/解码之间的差异。如果图片比较大,并且作为content文件,Panorama就有可能在背景图片显示之间出现。如果作为resource文件,Panorama就会在图片准备好以后加载。resource文件的同步加载机制,通常被认人们诟病,但在这里却保证了应用程序的友好体验。虽然增加了Panorama显示需要的时间,但人们还是不希望图片背景在Panorama之后加载。其实,我们可以使用活动的UI元素作为Panorama的背景!Panorama控件的创建者,本书的技术编辑,微软员工Dave Relyea共享了这个成果,你可以在这里看到:http://bit.ly/panoramaxaml。
由于Panorama是水平切换的,因此在背景右边沿与左边沿的连接处,会出现一条“缝隙”,除非我们使用指定的美工设计(如游戏Hub)或者是纯色的背景(如人脉Hub)。缝隙存在也没有问题,只要用户习惯就好,这也有助于提示用户控件将要回滚(我们可以在图片和Marketplace Hub中看到这条缝隙)。但是,Groceries使用的背景图片边沿有一些阴影,使得切换过程更加的平滑。如图27.4所示。
图27.4 背景图片的阴影使得Panorama控件从最后切换到开始页面的过渡更加平滑。
即使选择使用美工设计的图片,1个像素宽度的背景色缝隙在页面回滚过程中也偶尔会被用户看到。我们仍然可以通过设置一个新Panorama控制模板来解决这个问题。它可以是默认模板的一份拷贝,其background边界具有负边距,如下:
<Border x:Name=”background” Background=”{TemplateBinding Background}”CacheMode=”BitmapCache” Margin=”-1,0”/>
Panorama包含了两个始终显示的Item:等待购买的所有物品的清单和购物车清单。中间的一些页面通过代码来动态添加。
“list” 这个Panorama Item的Header是用户自定义的,在通常的标题文本边上,它有三个按钮:一个用来添加新购物清单,一个用来进行参数设置,还有一个是帮助,详见图27.2。一般来说,这些应该设置为应用程序栏的按钮,但因为在Panorama的设计指导中,指明了最好不要使用应用程序栏,所以就把它们放在这个区域中去了。
“购物车”清单也具有一个自定义的Header,我们在它的文本旁边加入了一个“删除”按钮。其他的Panorama Item(主要通过代码添加)只包含了一个list box,但“购物车”清单包含了一个Grid控件,用来在list box背后加入一个明显的“购物车”图标。
按钮的使用贯穿了整个应用,它们具有自定义的“SimpleButtonStyle”风格。在这种风格中,每个按钮具有新的控件模板,移除了按钮的border、padding和其他行为,所以我们看到的只是按钮的文字内容(它同时还加入了本书中使用的标题效果)。
如果每个按钮采用默认的样式(调整了按钮的布局,使得它们都能够显示在界面上),那么它们的效果如图27.5所示。在这里使用按钮控件的原因是:按钮的单击事件只有在用户的单击动作下触发,而非平移动作。这就使得用户可以在无意中点击按钮时,也可以对Panorama进行平移。如果使用MouseLeftButtonUp事件来检测用户对UI元素的点击,那么在UI元素上的平移操作将会触发原来点击行为的事件。
图27.5 填充了按钮的Groceries应用界面,它可以很容易地检测用户非平移的点击。
在Panorama和Pivot控件中,避免使用原始的鼠标事件,如MouseLeftButtonDown、MouseMove和MouseLeftButtonUp!因为整个控件的平移受用户手势的控制,对于这些事件中任何附加的用户逻辑来说,它就必须处理用户的平移手势。我们可以寻找其他不会被平移手势触发的事件来替代,比如按键的单击事件或者list box的SelectionChanged事件等等。
The Code-Behind
本应用程序维护着一个购物清单列表(Settings.AvailableItems),该列表记录的是用户加入的每个清单。根据每个商品的属性,Panorama中的每个list box正是这个列表进行条件过滤后的视图。这些列表具有静态的属性,比如FilteredLists.Need(整个“List”列表中的商品)和FilteredLists.InCart(购物车列表中的商品)。与前一章中依靠填写表格的页面进行过滤的方式不同,这些列表由内部逻辑实现过滤。这就使得主页面可以对每个list box使用数据绑定。这些列表以及代表每件商品的Item类型会在接下来的“Supporting Data Types”一节中介绍。
RefreshAisles负责动态地往第一个Panorama item页和最后一个Panorama item页之间的页面填写信息。每个动态的页面由自定义的AislePanoramaItem控件(继承自PanoramaItem)来封装。该控件会在下一节中介绍。Panorama item只添加用户自定义的页面,该页面中的商品最终有可能会被添加到购物车。
对每一个动态的Panorama item页使用各自的过滤集,这种方法的效率并不高,因为每个FilteredObservableCollection(它的实现会在稍后讲解)必须通过传入的可用Item列表来迭代。如果Item列表非常大的话,有可能需要选择一个新的策略。
本应用程序证明了如何来实现Panorama item的动态卸载,在动态页面中的所有商品均放入购物车以后,就会触发该行为。但是,与Pivot类似,Panorama并不对它的Item移除进行优雅的处理。其存在的问题有两点:寻找移除Panorama item的合适时间点,以及它对视差效果的影响。
为了消除疑惑,在用户切换视图以后,才会将一个空的Panorama item移除。所以,这就需要由代码在Panorama的SelectionChanged事件处理中进行检查。在该事件处理过程中,前一个显示页以唯一的页面存放在RemovedItems集合中。因为立即移除的效果会与平移过渡的效果类似,而平移动作会触发SelectionChanged事件,所以处理程序使用DispatcherTimer在之后的半秒钟内进行移除操作。实际上,这种处理效果非常好。唯一存在的问题就是:由于背景的移动,而且标题基于整个Panorama的宽度,如果移除一个Item的话,会减少整个Panorama的宽度,这就会导致背景和标题的突然抖动。除非这种情况发生时,我们停留在Panorama的第一页。很遗憾,我们没有办法避免这种情况的出现,除非不移除Panorama item。
Storyboards 被用来模拟商品加入/移出购物车的效果。对购物车列表做的改变发生在Completed事件处理当中,这是通过设置Item的Status属性为InCart或者Need来完成的。这会在列表发生改变后,触发一个属性更改的通知,由于采用了数据绑定,这两个列表会自动完成更新。与Pivot Item不一样,将Panorama Item的Visibility设置为Collapsed以后,可以成功隐藏真个Item。即通过更改Panorama Item的可见性,而不是添加或者删除Item。但是,隐藏Panorama Item同删除操作一样,都存在抖动的情况。
Panorama无法通过编程来设置当前的Panorama Item! 本应用程序明显遗漏了一个设置,该设置用于记录当前的Panorama Item,使得程序下一次启动或者激活时,当前的Panorama Item能够恢复。但是,Panorama的SelectedIndex和SelectedItem属性是只读的,所以尽管我们可以保存它们两个中任何一个的值,但却在程序启动或者激活时,无法恢复Panorama Item。
Panorama具有一个可读写的DefaultItem属性,它可以立即改变屏幕显示的Panorama item,但是可能和你所期望的不大一样。它会移动Item,使得DefaultItem变成虚拟Canvas上的第一个Section,如图27.6所示。这意味着标题与当前的默认Item对齐,背景图片的缝隙立刻移到了Item的左边。在Groceries应用程序中,背景图片的缝隙出现在除购物车和所有商品页面中的话,会给用户带来疑惑。因此,DefaultItem属性并不适合让用户回归到他们注销的页面。
The AislePanoramaItem Control
AislePanoramaItem可以作为一个用户控件的形式加入到Visual Studio工程中,但是它的基类从UserControl变成了PanoramaItem。这样做是为了使得AislePanoramaItem这个子类可以像用户控件一样,获得方便的XAML支持。 这个Panorama Item和主页面上的第一个Panorama Item很类似,但是在Item模板中没有编辑按钮。就像应用程序中RefreshAisles方法所做的那样,这种方便的打包方式使得它可以很容易被主页面重用。
Supporting Data Types
Status枚举定义如下:
public enum Status
{
Need, // In the current shopping list (but not in the cart yet)
InCart, // In the cart
Unused // Added at some point in the past, but not currently used
}
IsFavorite属性在“添加”页面和“编辑”页面中使用,帮助用户管理商品输入。
属性更改的通知使得过滤集合可以保证商品出现在正确的列表分类中。它们使得单个商品信息保持最近的更新。比如,在Item的IsFavorite状态发生改变以后,“添加”页面使用了一些值转换器来显示或者隐藏按钮。
AvailableItems设置用来保存列表中的所有商品信息。
本应用中使用的过滤列表并没有被程序保存,而是由单个列表在程序运行时进行初始化。
每个FilteredObservableCollection在ReadOnlyObservableCollection中被封装,这样做是为了避免用户直接尝试修改数据集合。
该类的构造函数中有两个参数:一个源数据集和一个返回单条记录是否属于过滤列表的回调函数。这使得每个实例都可以使用不同的过滤器,就和FilteredLists静态类中一样。该类中使用的记录类型必须实现INotifyPropertyChanged接口,因为该类在监视源数据集的添加和删除操作的同时,也要跟踪每条记录的属性更改(这是Groceries应用程序的需求,因为类似Status或者IsFavorite这些属性的改变必须立即反应在显示的列表当中)。
本文转自施炯博客园博客,原文链接:http://www.cnblogs.com/dearsj001/archive/2012/08/15/101App4WP7_Groceries.html,如需转载请自行联系原作者