Windows Phone 7 ViewModel 样式视频播放器






4.95/5 (81投票s)
一次设计师/开发者协作,展示了一个 Windows Phone 7 ViewModel 风格的视频播放器示例
Windows Phone 7 “ViewModel 风格”视频播放器
在本教程中,Michael Washington 和 Alan Beasley 合作演示了如何创建一个使用 **ViewModel Style** 模式的 **Windows Phone 7** 视频播放器。
**ViewModel Style** 模式允许程序员创建一个没有 UI(用户界面)的应用程序。程序员只需创建 **ViewModel** 和 **Model**。然后,一位完全没有编程能力的**设计师**就可以在 **Microsoft Expression Blend 4**(或更高版本)中创建 **View**(UI)。如果您不熟悉 **ViewModel Style** 模式,建议您阅读 Silverlight ViewModel:一个(过度)简化的解释 以获得入门知识。
Michael Washington:创建“ViewModel”(程序)
创建视频播放器其实非常容易。我唯一做的不同之处在于,我不在 **View** 的代码隐藏文件中编写代码。这样,Alan Beasley 就可以随意修改 **View**(UI)了。
要创建 **Windows Phone 7** 应用程序,您需要从以下网址安装开发人员工具:http://developer.windowsphone.com。
首先,我们在 **Visual Studio** 中创建一个 **Silverlight for Windows Phone Application** 项目。
使用 MVVM Light 在 Windows Phone 7 中调用 ICommands
当您不使用代码隐藏时,您需要使用 **ICommands** 并通过行为(behaviors)来调用它们。**Windows Phone 7** 在撰写本文时,不支持调用 **ICommands**,因此我们将使用 Laurent Bugnion 的 **MVVM Light**,其地址为:http://mvvmlight.codeplex.com/(感谢 Sacha Barber 和 Laurent Bugnion 提醒我在遇到困难时查看 MVVM Light 或 Cinch)。
该软件包包含多个项目,请解压 **GalaSoft.MvvmLight.Binaries.V3.zip**(或更高版本)(注意:有关安装完整软件包的更多帮助,请参阅 http://www.galasoft.ch/mvvm/installing/)。
接下来,我们选择 **添加引用...**
我们从 **\Program Files\Laurent Bugnion (GalaSoft)\Mvvm Light Toolkit\Binaries\WPF4** 添加程序集。
滑动触发器 (Flick Trigger)
手势对 **Windows Phone 7** 应用程序非常重要,因此接下来我们要从 http://aimeegurl.com/2010/03/18/panoramic-navigation-on-windows-phone-7-with-no-code 下载 Jocelyn Mae Villaraza 的 **WPBehaviorsLibrary**。
我们下载该库,打开它,复制 **GestureTriggers.cs** 和 **StateBehaviors.cs** 文件,然后将它们放入我们的项目中。
其他支持文件
我们还创建了一个 **DelegateCommand.cs** 文件,该文件也支持“命令”。该文件在 此链接 的教程中进行了介绍。
**Videos.cs** 文件是一个简单的类,用于存储视频。
public class Videos { public string VideoName { get; set; } public string VideoURL { get; set; } }
ViewModel
技术上我们只需要再创建一个文件,即 **ViewModel**(**MainViewModel.cs**)。然而,我们还将逐步介绍实现视频播放器 **View** 的一些步骤,以便您能很好地了解 **View** 是如何从 **ViewModel** 创建的。
我们创建一个名为 **MainViewModel.cs** 的文件并输入以下代码。
public class MainViewModel : INotifyPropertyChanged { private MediaElement MyMediaElement; private DispatcherTimer progressTimer; public MainViewModel() { // Set the command property MediaOpenedCommand = new DelegateCommand(MediaOpened, CanMediaOpened); PlayVideoCommand = new DelegateCommand(PlayVideo, CanPlayVideo); StopVideoCommand = new DelegateCommand(StopVideo, CanStopVideo); PauseVideoCommand = new DelegateCommand(PauseVideo, CanPauseVideo); SetSeekControlCommand = new DelegateCommand(SetSeekControl, CanSetSeekControl); SetVideoCommand = new DelegateCommand(SetVideo, CanSetVideo); // Call the Model to get the collection of Videos GetListOfVideos(); }
这设置了一些我们将需要的全局变量:**MyMediaElement**、**progressTimer**。在类的构造函数中,它设置了一些我们将需要的 **ICommands**。最后,它调用 **GetListOfVideos()** 方法。
**ICommands** 的代码和 **ViewModel** 的 **Properties** 直接取自:SilverlightMVVMVideoPlay.aspx 和 AdvancedMVVMVideo.aspx,并在这些文章中进行了详细介绍。
例如,**MediaOpenedCommand** 是 **View** 设置 **MediaElement** 的地方,并将其存储在 **MyMediaElement** 全局变量中,然后启动一个 **DispatcherTimer** 来跟踪视频进度并更新进度控件。
#region MediaOpenedCommand public ICommand MediaOpenedCommand { get; set; } public void MediaOpened(object param) { // Play Video MediaElement parmMediaElement = (MediaElement)param; MyMediaElement = parmMediaElement; this.progressTimer = new DispatcherTimer(); this.progressTimer.Interval = TimeSpan.FromSeconds(1); this.progressTimer.Tick += new EventHandler(this.ProgressTimer_Tick); SetCurrentPosition(); // Play the video PlayVideo(null); } private bool CanMediaOpened(object param) { return true; } #endregion
与之前的 Silverlight 视频播放器文章相比,主要的改动是删除了 Web 服务代码,而是简单地创建了一个视频集合供 **GetListOfVideos()** 方法使用。
#region GetListOfVideos private void GetListOfVideos() { SilverlightVideoList = new ObservableCollection(); SilverlightVideoList.Add(new Videos { VideoName = "Introduction", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule1-SL4LOB_01_01_Introduction/Silverlight4-SL4BusinessModule1-SL4LOB_01_01_Introduction_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "RIAServices", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule2-SL4LOB_02_01_RIAServices/Silverlight4-SL4BusinessModule2-SL4LOB_02_01_RIAServices_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Editing Entities", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule2-SL4LOB_02_02_EditingEntities/Silverlight4-SL4BusinessModule2-SL4LOB_02_02_EditingEntities_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Showing Events", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule2-SL4LOB_02_03_ShowingEvents/Silverlight4-SL4BusinessModule2-SL4LOB_02_03_ShowingEvents_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Authentication", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule3-SL4LOB_03_01_Authentication/Silverlight4-SL4BusinessModule3-SL4LOB_03_01_Authentication_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "MVVM", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule3-SL4LOB_03_02_MVVM/Silverlight4-SL4BusinessModule3-SL4LOB_03_02_MVVM_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Validation", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule3-SL4LOB_03_03_Validation/Silverlight4-SL4BusinessModule3-SL4LOB_03_03_Validation_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "ImplicitStyles", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule3-SL4LOB_03_04_ImplicitStyles/Silverlight4-SL4BusinessModule3-SL4LOB_03_04_ImplicitStyles_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "RichTextBox", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule3-SL4LOB_03_05_RichTextBox/Silverlight4-SL4BusinessModule3-SL4LOB_03_05_RichTextBox_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Webcam", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule4-SL4LOB_04_01_Webcam/Silverlight4-SL4BusinessModule4-SL4LOB_04_01_Webcam_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Drop", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule4-SL4LOB_04_02_Drop/Silverlight4-SL4BusinessModule4-SL4LOB_04_02_Drop_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Grouping", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule5-SL4LOB_05_01_Grouping/Silverlight4-SL4BusinessModule5-SL4LOB_05_01_Grouping_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "FluidUI", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule5-SL4LOB_05_02_FluidUI/Silverlight4-SL4BusinessModule5-SL4LOB_05_02_FluidUI_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "RightMouseClick", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule5-SL4LOB_05_03_RightMouseClick/Silverlight4-SL4BusinessModule5-SL4LOB_05_03_RightMouseClick_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Printing", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule6-SL4LOB_06_01_Printing/Silverlight4-SL4BusinessModule6-SL4LOB_06_01_Printing_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "MultipagePrinting", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule6-SL4LOB_06_02_MultipagePrinting/Silverlight4-SL4BusinessModule6-SL4LOB_06_02_MultipagePrinting_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "OOB", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule7-SL4LOB_07_01_OOB/Silverlight4-SL4BusinessModule7-SL4LOB_07_01_OOB_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Toasts", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule7-SL4LOB_07_02_Toasts/Silverlight4-SL4BusinessModule7-SL4LOB_07_02_Toasts_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Window Placement", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule7-SL4LOB_07_03_WindowPlacement/Silverlight4-SL4BusinessModule7-SL4LOB_07_03_WindowPlacement_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Elevated Trust", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule7-SL4LOB_07_04_ElevatedTrust/Silverlight4-SL4BusinessModule7-SL4LOB_07_04_ElevatedTrust_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Custom Chrome", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule8-SL4LOB_08_01_CustomChrome/Silverlight4-SL4BusinessModule8-SL4LOB_08_01_CustomChrome_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Window Closing Event", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule8-SL4LOB_08_02_WindowClosingEvent/Silverlight4-SL4BusinessModule8-SL4LOB_08_02_WindowClosingEvent_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "OOB Silent Install", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule8-SL4LOB_08_03_OOBSilentInstall/Silverlight4-SL4BusinessModule8-SL4LOB_08_03_OOBSilentInstall_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "Xap Signing", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule8-SL4LOB_08_04_XapSigning/Silverlight4-SL4BusinessModule8-SL4LOB_08_04_XapSigning_kit.wmv" }); SilverlightVideoList.Add(new Videos { VideoName = "MEF", VideoURL = @"http://ecn.channel9.msdn.com/o9/learn/videos/Silverlight4-SL4BusinessModule8-SL4LOB_08_05_MEF/Silverlight4-SL4BusinessModule8-SL4LOB_08_05_MEF_kit.wmv" }); SetVideo(SilverlightVideoList[0]); SelectedVideoInListProperty = 0; } #endregion
它们通过 **SilverlightVideoList** 集合暴露给 **View**。
#region SilverlightVideoList private ObservableCollection_SilverlightVideoList; public ObservableCollection SilverlightVideoList { get { return _SilverlightVideoList; } private set { if (SilverlightVideoList == value) { return; } _SilverlightVideoList = value; this.NotifyPropertyChanged("SilverlightVideoList"); } } #endregion
View(开发者版本)
我通常会创建一个 **View**,以便 Alan Beasley 可以看到 **ViewModel** 的期望是如何连接的。这个 **View** 会被丢弃,所以我不打算让它看起来很漂亮。我创建 **MainPage.xaml**...
我在 **Expression Blend 4** 中打开项目,然后在 **Objects and Timeline** 窗口中单击 **LayoutRoot**,在 **Properties** 中,单击 **DataContext** 旁边的 **New** 按钮...
...并将它设置为 **MainViewModel**。
然后,当我单击 **Data** 选项卡时,我可以看到 **ViewModel** 提供的所有 **Properties**、**Collections** 和 **Commands**。
我在:SilverlightMVVMVideoPlay.aspx 和 AdvancedMVVMVideo.aspx 中介绍了如何连接控件。
但是,我没有使用 **InvokeCommandAction** 行为,而是使用了 **MVVM Light Toolkit** 中的 **EventToCommand** 行为。
使用手势
因为这是一个 **Windows Phone 7** 应用程序,我们希望添加对手势的支持。这与简单地用手指(使用实际手机时)或鼠标(使用 **Windows Phone 7** 模拟器时)单击某个东西不同。手势允许您在用户朝特定方向移动时指定一个操作。
在我的示例中,我连接了手势:当您(用鼠标指针或手指)按住视频并向右滑动时,它会播放视频。向左滑动时,它会停止视频。
以下是连接 **FlickRight_Play**手势的步骤:
我抓取一个 **EventToCommand** 行为,并将其拖到 **Objects and Timeline** 窗口中的 **mediaElemnt** 上。
在行为的 **Properties** 中,我单击 **TriggerType** 旁边的 **New** 按钮……
我选择 **FlickGestureTrigger**。
我在 **Direction** 下拉菜单中选择一个方向。
我单击 **Command** 旁边的 **Advanced options**。
然后我将 Command 绑定到 **PlayVideoCommand**。
运行项目
一切连接好后,按 **F5** 运行项目。如果您连接了实际的手机,请选择 **Windows Phone 7 Device**;否则,请选择 **Emulator**。
模拟器将加载。
此应用程序以横向模式显示,因此您需要单击旋转按钮。
应用程序将启动并播放第一个视频。要更改视频,您需要单击列表框,按住鼠标按钮向上或向下拖动,然后单击所选项以更改视频(当使用真实手机上的手指操作时,效果要好得多)。
您还可以单击视频,按住鼠标按钮,然后向右滑动以播放视频,向左滑动以停止视频。
卓越的设计师/开发者工作流程
Michael Washington
我通常会创建一个 **View**,以便 **Alan Beasley** 可以看到 **ViewModel** 的期望是如何连接的。然而,他可以完全删除 **View** 中的所有内容并从头开始。
令人惊奇的是,他能够在没有任何代码的情况下完成项目,而无需我“修复任何问题”。项目需要回到我这里的情况只有在添加新功能时才会发生。
例如,在第一个版本之后,他要求我们有一个视频列表可供选择。我将一个集合添加到 **ViewModel** 并将其发送给他。他只需要在他的项目版本中替换 **ViewModel**。无需打扰他可能已经开始对 **View** 所做的任何更改。
当每个人都能专注于他们最擅长的事情时,任何项目都会得到极大的增强。我的部分变得非常容易:我查看项目的当前需求,并仅使用 **Properties**、**Collections** 和 **ICommands**(以及支持它们的任何代码)来实现这些需求。
实际的用户界面和应用程序的“流程”已超出我的控制范围,我惊奇地看着它有时会发生剧烈变化。我有时会提出一两个想法,但我只是“旁观者”,看着通常会成为“我的”项目。我有时不得不提醒自己,唯一编写代码的人是我!然而,就像电影的作家和导演必须接受演员是观众最终看到的内容一样,UI 是最终用户看到和关心的内容。除非代码不起作用,否则没有人关心代码。
多年来,应用程序一直受到限制,因为程序员必须实现某些功能。这就像一个导演需要在电影的每个场景中扮演一个角色。有些导演是好的演员,这样可行,但对于大多数项目来说,UI 最适合交给那些最擅长创建 UI 的人。
Alan Beasley:创建“View”(用户界面)
在开始为 **Windows 7 Phone** 设计任何内容之前,您都应该阅读 Windows Phone UI 设计和交互指南。微软发布了这些“指南”,以期为 **Windows 7 Phone** 开发生成一致的“外观和感觉”。虽然这些只是“指南”,而不是“规则”,但我建议您阅读它们,并有一个非常好的理由/论据说明您为什么想偏离它们。因为微软和整个 Silverlight 社区都希望 **Windows 7 Phone** 能够成功发布。Silverlight 社区中没有人希望看到各种设计的大杂烩,整体看起来像一团乱麻。微软在其“Metro”风格上付出了很多努力,以生成 **Windows 7 Phone** 的一致风格。所以,在您疯狂使用大量“花哨”图形之前,请三思!
那么,这些 UI 设计 指南是什么呢?嗯,重点是涵盖图标、文本样式、布局、导航、交互、配色方案等的一致“Metro”风格。但我不会重复所有 指南,我强烈建议您自己阅读它们(至少直到您发现一个拼写错误,我向您保证有一个)。我将在我叙述 Defwebserver(Michael 的)视频播放器的样式时,讨论相关方面的 指南。那么,让我们开始吧……
下载 Michael 制作的未样式化的播放器,并在 **Blend 4** 中打开。您可能需要按以下方式更新您的安装:
1、卸载 Expression Blend 并重新安装(Microsoft Expression Blend® 4 Release Candidate (RC)
3、安装 Microsoft® Expression Blend® Add-in Preview 2 for Windows® Phone
4、安装 Microsoft® Expression Blend® Software Development Kit (SDK) Preview 2 for Windows Phone
在 **Phone Emulator** 中运行应用程序,然后通过单击下图中的图标切换到 **Landscape** 模式。
最终(需要一些时间)**Video Player** 将启动并显示如下图像。
现在,它“功能齐全”,但不太美观,因为 Michael 只在屏幕上放置了必要的元素来使应用程序正常工作。准备交给设计师使其应用程序更具视觉吸引力(由于我们没有设计师,我得自己动手……哈哈)
那么,我们有什么?一个标题区域,文本为“**My Video Player**”,一个显示“**Introduction**”以及更多当前隐藏的视频的 **ListBox**,但最吸引人的是屏幕中央下方的 **Player Controls**。**Player Controls** 由一个**进度条**、**进度显示**、**播放**、**暂停**、**停止**按钮和一个**音量滑块**组成。一个基本视频播放器所需的一切……目前无法为视频播放器添加**快进**或**快退**控件,尽管 Victor Gaudioso 在出色的视频教程中展示了如何为 MediaElement 添加 快进功能。我们无法添加此功能的原因是我们无法设置 Windows Phone 中视频流的位置。原因我不知道,因为我不是程序员!但我相信微软将在不久的将来解决这个问题。所以目前我们将忘记**快进**和**快退**,只**样式化**我们拥有的。
在 **Silverlight** 中为 **Windows 7 Phone** 开发时可用的默认控件,不是用于开发普通 **Silverlight** 应用程序的标准控件。微软专门为手机开发了这些控件,因为显然触摸屏界面和普通鼠标交互界面会有一些差异。最明显的是按钮,默认情况下它们是矩形且尺寸较大。它们还有一个凸起区域,如下图所示。
按钮有凸起的原因与“**触摸目标**”区域有关,设计师需要考虑触摸屏的用户交互。即用户的**手指有多粗**?如果控件太靠近,用户是否会意外触摸到错误的按钮?微软建议每个触摸目标的最小尺寸为 **9mm**(**34 像素**),相距很近的触摸目标之间的间距为 **2mm**(**8 像素**),而这些触摸目标本身可能只有 **7mm**(**26 像素**)大小。“**Z** 顺序”在触摸目标方面也起着作用,控件在布局 **Z 顺序**中越高(越近),它将优先于重叠控件的“**触摸目标**”区域。我们可以在上图中看到,**Play** 按钮的“触摸目标”与**Pause** 按钮重叠,在触摸屏环境中它们肯定会相互干扰。(糟糕的开发者!)但这不是开发者的责任,除非他们同时也是设计师!所以我们需要考虑交互式控件的最小尺寸,以及是否需要为这些控件提供“**TouchTargetOverhang**”。“**TouchTargetOverhang**”可以在整个应用程序的 **App.xaml** 中调整,默认情况下在所有侧面设置为 **12 像素**。如下图所示。
“**PhoneTouchTargetOverhang**”绑定到按钮**控件模板**中“**ButtonBackground**”**Border** 元素的 **Margin** 属性。但请记住,此“**TouchTargetOverhang**”仅在处理交互式控件的**最小**尺寸时需要。因此,如果按钮大于 **9mm**(**34 像素**),则可能不需要“**Overhang**”。用户测试是检查设计布局是否有效的唯一真正方法,但由于我们目前都没有真机,我建议在“**触摸目标**”尺寸方面我们都采取谨慎的态度。
那么,让我们开始为 **Player Controls** 添加样式,看看效果如何。首先,让我们检查一下 Michael 在 **Expression Blend 4** 的 **Objects and Timeline** 中为我们提供了什么。
目前,我们有一个名为“**PlayerControls**”的 **Grid**,其中包含一个显示视频**进度显示**(持续时间)的 **TextBlock**。以及一个包含其余控件的 **Canvas**。这些控件是一个**Slider**(音量)和 **3个 Button**,分别命名为 **Play**、**Pause** 和 **Stop**。如果您进一步展开树形结构,您会看到 Michael 已将**行为**附加到元素上,这些行为会连接到他在“**View Model**”中编写的代码。您可以在文章的 Michael 部分找到更多关于这方面的信息。但对我来说,作为设计师,这意味着我可以完全删除这里的所有内容,并从头开始。使用这些**行为**连接到他的代码(完全不写任何代码!!!)。
但是,为了避免文章过长或重复 Michael 的内容,我将使用我们现有的内容……
修改 Player Controls 的布局
选择 **Canvas** 元素,右键单击并选择 **Change Layout Type > StackPanel**。
选择 **StackPanel**,将 **Orientation** 改为 **Horizontal**,**Reset** **Margins**,并将 **Horizontal** 和 **Vertical Alignments** 都设置为 **Stretch**。
现在选择 **StackPanel** 中的所有内容,**Reset** **Margins** 并将 **Horizontal** 和 **Vertical Alignments** 设置为 **Stretch**。
**StackPanel** 现在应该看起来像下图所示。
现在,在 **Objects and Timeline** 中将 **StackPanel** 的顺序更改为 **StopButton**、**PlayButton**、**PauseButton**、**Slider**。如下图所示。
**Artboard** 现在应该像下图一样显示控件。
这与 Michael 之前在 **Canvas** 中的布局并没有太大区别,只是现在它是动态的,并且会自动更新。编辑控件更容易……
现在选择 **ProgressBar** 和 **ProgressDisplay**,然后选择 **Group Into > Grid**。
这会导致 **Artboard** 变成下图所示的样子,但别担心!我们可以解决这个问题……
我现在想将 **PlayerControls** 分成 **2 行**。第一行包含 **ProgressBar** 和 **ProgressDisplay**,第二行包含**按钮**和音量**滑块**。
因此,选择名为“**PlayerControls**”的 **Grid** 元素,并使用 **Selection Tool** 放置一个**行分隔符**,如下图所示。
接下来,选择包含 **ProgressBar** 和 **ProgressDisplay** 的 **Grid**,然后选择 **Group Into > Border**。
将此 **Border** 重命名为“**ProgressInfo**”,**Reset** **Margins** 使其仅占据 **PlayerControl Grid** 的顶行,并确保 **Horizontal** 和 **Vertical Alignment** 设置为 **Stretch**。
现在选择 **StackPanel**,选择 **Group Into > Border**,并将 **Border** 重命名为“**NavigationAndVolume**”。**Reset** **Margins** 使其仅占据 **PlayerControl Grid** 的底行,并确保 **Horizontal** 和 **Vertical Alignment** 设置为 **Stretch**。
一切顺利的话,**Artboard** 应该看起来像下图所示。
这看起来开始好转了,几乎可以开始考虑**样式化**控件/组件了。但关于 **ProgressBar** 和 **ProgressDisplay** 还有一件事需要查看。目前 **ProgressDisplay** 是不可见的,这是因为没有要显示的内容(设计时没有视频信息),并且 **ProgressBar** 的右侧有一个**边距**,可以防止它覆盖 **ProgressDisplay**。但这在运行时对我们不起作用,因为我们无法确定 **ProgressDisplay** 需要多少空间。因为这将取决于我们播放的每个视频的长度……
因此,选择包含 **ProgressBar** 和 **ProgressDisplay** 的 **Grid**,并插入一个**列分隔符**,如下图所示。
将 **ProgressBar** 设置为仅填充**第一个 Grid 列**,将 **ProgressDisplay** 设置为仅适合**第二个 Grid 列**。确保两者元素的 **Margins** 都设置为 **0**(**Reset**),并且 **Horizontal** 和 **Vertical Alignments** 设置为 **Stretch**。如下图所示。
您应该会注意到 **ProgressBar** 没有填充**第一列**的所有空间(取决于您将**列分隔符**放在何处),**ProgressDisplay** 也是如此。这是因为 Michael 为这些元素设置了固定的**宽度**(糟糕的开发者!!!)。但和以前一样,我只是开玩笑!所以这不是他的责任,除了测试这些元素的功能,而不是设计它们……
因此,选择 **ProgressBar** 并将**宽度**设置为 **Auto**。对 **ProgressDisplay** 也执行相同的操作……
您应该注意到 **ProgressDisplay** 默认宽度为 **0** 像素,这是因为它在设计时当前没有输入显示。
**ProgressBar** 现在填充了**Grid** 的**第一个列**的所有空间(从一端到另一端),如下图所示。
但是为了给它一些呼吸空间,我们需要设置其**左侧**和**右侧**的**边距**。我们可以对此元素进行设置,或者可能在父 **Grid** 元素上进行设置。我将选择结合**两者**,仅在 **ProgressBar** 的**右侧**设置一个**边距**。(原因很快就会明了……)
因此,在选中 **ProgressBar** 的情况下,仅在**右侧**设置 **10 像素**的**边距**。如下图所示。
现在是聪明的部分,利用默认应用于**按钮**的“**PhoneTouchTargetOverhang**”。
选择 **ProgressBar** 和 **ProgressDisplay** 的父 **Grid** 元素,然后在 **Advanced options** 中选择 **Local Resource > PhoneTouchTargetOverhang**。
**ProgressBar** 和 **ProgressDisplay Grid** 的所有侧面都将放置 **12 像素**的**边距**,并且如果此属性发生更改,它将像**按钮**一样进行更新。**Artboard** 现在看起来可能像下图所示。
(这是因为**顶部**和**底部边距**有效地使**ProgressBar** 的高度为零。但别担心,因为父 **Grid** 名为 **PlayerControls** 具有固定的**高度**)。
因此,选择名为 **PlayerControls** 的 **Grid** 并将**高度**设置为 **Auto**。这应该足够扩展 **PlayerControls** 的**高度**以显示 **ProgressBar**,如下图所示。
现在关于**布局**就先这样,我们可以在**样式化**元素/组件时进行调整。
样式化控件元素
边框元素
正如我们在上一部分中所做的,您可能想知道我为什么要添加一些 **Border** 元素。现在我将向您展示原因!
我读过一些关于**边框**和**矩形**的文章,但它们都没有真正讨论或正确显示它们之间的区别。简单来说,**Rectangle** 比 **Border** 更基础,**Rectangle** 相对于 **Border** 的唯一优势是 **X** 和 **Y** 的**圆角半径**可以不同,仅此而已!但是 **Border** 可以做一些 **Rectangle** 做不到的巧妙事情……
选择名为 **ProgressInfo** 的 **Border** 元素,并在所有侧面设置 **BorderThickness** 为 **3**,如下图所示。
(设置不同的边缘厚度是**Rectangle** 做不到的第一件事……)
**Artboard** 可能没有改变,因为 **Border** 还没有应用 **Background** 或 **BorderBrush**。
因此,选择 **BorderBrush** 并单击 **Brush resources**,如下图所示。
在可用的“**Local Brush Resources**”中,选择 **PhoneAccentBrush**,如下图所示。
这些**本地画笔资源**是**Windows Phone 主题**的一部分,**PhoneAccentBrush** 基本上是 指南中讨论的 **4 种主题颜色**之一。实际上有 **5 种主题颜色**,但有一种是为**手机**制造商保留的。我将使用这种**蓝色主题**颜色为我的所有控件设置样式,并每次都引用这个**资源**。这样,当**主题颜色**改变时,我的控件也会改变……
**Artboard** 现在应该看起来像下图所示。
(我像上面图片一样,按**F9**隐藏/切换 **Blend** 设计手柄以示清晰)
现在我想在**边框**的**前 2 个角**上添加一个**圆角**,但不在**边框**的**后 2 个角**上。
因此,在选中 **ProgressInfo** **Border** 的情况下,将 **CornerRadius** 设置为 **20,20,0,0**,如下图所示。
(为每个角指定不同的半径是 **Rectangle** 做不到的第二件事……)
因此 **Artboard** 现在看起来像下图所示。
现在,对名为 **NavigationAndVolume** 的 **Border** 元素重复此过程,但将**顶部** **BorderThickness** 设置为 **0**,将 **CornerRadius** 设置为 **0,0,20,20**。
如果一切顺利,您应该会得到与下图相同的结果。
正如我之前所说,您无法用 **Rectangle** 做到的!
现在,为了给 **Player Controls** 增加一些实质内容,我想为**边框**元素添加一个半透明的背景。
因此,选择 **ProgressInfo Border** 元素,并为 **Background** 设置一个**渐变画笔**。
将两个**渐变停止点**都更改为**黑色**,将第一个**渐变停止点**的**Alpha**设置为 **30%**,第二个**渐变停止点**设置为 **80%**。
对名为 **NavigationAndVolume** 的**边框**重复此操作,反转**渐变**,使其看起来像下图所示。
(为了这张图片的清晰度,我暂时将一个灰白色**矩形**放在**Player Controls** 后面)。
样式化 ProgressBar 和 ProgressDisplay
选择 **ProgressBar**,并给它一个例如 **18 像素**的高度,使其更粗一点。请注意,它有尖角,这与**边框**元素的圆角不太匹配,所以让我们用一些**样式**来修复它。
因此,在仍然选中 **ProgressBar** 的情况下,选择 **Edit Template > Edit a Copy**。将此新**样式**命名为 **PhoneProgressBarStyle** 或类似名称,然后按 **OK**。
**ProgressBar** 是一个相当复杂的控件,但我们只关心基本组件,即 **ProgressBarTrack** 和 **DeterminateRoot**(及其子 **ProgressBarIndicator**)。这实际上很好,因为如果我选择 **Visual State Manager**(**VSM**)并进入“**Indeterminate**”**状态**。Expression Blend 4 会抛出一个异常(崩溃),原因我不知道,但这似乎只与**Phone**相关……(我可能将在未来的教程中深入介绍 **ProgressBar**)。
所以,我们先来看 **ProgressBarTrack**,这是视频播放时进度指示器沿其移动的轨道。我们目前看不到它,因为它被 **ProgressBarIndicator** 覆盖(遮挡)了。因此,暂时隐藏 **ProgressBarIndicator**,方法是单击其旁边的**眼睛**图标,以便我们可以处理 **ProgressBarTrack**。**ProgressBarTrack** 非常微弱,因为 **Opacity** 只设置为 **10%**,这在大多数情况下可能没问题,但为了在本教程中清晰起见,我想使其更清晰(更明显)。还要考虑移动设备可能使用的环境,如明亮的阳光。因此,我对自己更改 **ProgressBarTrack**“**默认**”设置的理由感到有些合理,但只有真正的用户测试才能证明或否定这个想法……
选中 **ProgressBarTrack**,将元素 **Opacity** 更改为 **30%**,并将 **CornerRadius** 设置为 **5**,使其看起来像下图所示。
(上图隐藏了 **ProgressBarIndicator**)。
现在看起来还可以,但我其实不希望所有角都有半径。但因为这是一个 **Border** 元素,我可以单独更改每个**圆角**。太好了,但有一个问题……**ProgressBarIndicator** 是一个**矩形**,位于一个**网格**中!正如我之前所说,一个**矩形**无法为每个**圆角**设置不同的半径。而且,当 **ProgressBarIndicator** 沿着 **ProgressBarTrack** 移动时,它必须完全匹配,否则会显得很傻。所以我们需要删除**矩形**并用一个**边框**替换它?您可能会说。是的,我们可以,但请注意 **ProgressBarIndicator** 是 **ProgressBar** 的一个**控件部分**(由其旁边的微小图标表示)。因此,在**控件模板**中是**强制**的!所以我们可以替换它,但它必须命名为与现有元素相同的名称。那么,让我们开始吧!
选择 **ProgressBarTrack** 并将 **CornerRadius** 设置为 **8,0,0,0**,这应该与下图所示相同。
(上图隐藏了 **ProgressBarIndicator**)。
现在去 **ProgressBarIndicator Rectangle**,记下名称,然后**删除**它。
在它原来的位置插入一个 **Border** 元素,并将其命名为 **ProgressBarIndicator**,就像魔法一样,它旁边出现了一个小图标,表示它是**控件模板**的**控件部分**。
在 **Layout** 属性中,确保 **Width** 和 **Height** 设置为 **Auto**,**Horizontal Alignment** 设置为 **Left**,**Vertical Alignment** 设置为 **Stretch**。同时确保 **Margins** 设置为 **0**。
现在删除 **BorderBrush** 并将 **Background** 设置为 **Template Binding > Foreground**。
最后将 **CornerRadius** 设置为 **8,0,0,0**(与 **ProgressBarTrack** 相同)。
**ProgressBar** 的内容就这些了,取消隐藏任何隐藏的元素,然后退出**控件模板**,这样我们就可以来看 **ProgressDisplay** 了……
现在,我们显然无法在设计时看到 **ProgressDisplay** 的内容,所以我们将盲目地设计需要做的事情,并祈祷……**ProgressDisplay** 只是文本,可能不会像**手机**的**主题**那样被更改,但**主题**附带了一些预定义的**字体**和**字体大小**。所以,我将**绑定**到这些属性来演示其功能……
选择 **ProgressDisplay** 元素,在 **Text** 属性中,单击 **Advanced options** 并选择 **Local Resource > PhoneFontSizeNormal**。
现在在**字体系列**的**Advanced options** 中,选择 **Local Resource > PhoneFontFamilySemiBold**。
最后运行应用程序(**F5**)并查看结果,如下图所示。
**ProgressBar** 看起来还不错,正如预期的那样,但 **ProgressDisplay** 工作不正常。因为它没有足够的空间来正确显示……这是因为我们无法确定 **ProgressDisplay** 每次使用时需要多少空间,并且我们需要为其分配可变大小,而不是其在父 **Grid** 中当前存在的“**默认**”比例大小。您可能得到的结果与当前屏幕截图略有不同,这取决于您最初放置**Grid 列分隔符**的位置。
因此,选择父 **Grid** 元素,并通过双击“**Padlock**”图标将“**Star**”大小图标更改为“**Auto**”大小,如下图所示。
这不会改变**Artboard** 中**Grid** 分隔符的位置,因为 **Blend** 会为这个**自动调整大小的 Grid 列**设置一个**最小宽度**。
所以,转到 **XAML** 并删除**最小宽度**声明(在下图蓝色高亮显示处)。
**Grid** 分隔符现在应该会吸附到**Grid** 的右端,但当有内容显示时它会自动增长。
关于 **ProgressBar** 和 **ProgressDisplay** 要做的最后一件事是确保它们引用 **PhoneAccentBrush**,从而引用**主题**。
因此,选择 **ProgressBar** 并将**前景**颜色设置为 **PhoneAccentBrush**,然后对 **ProgressDisplay** 也这样做,如下图所示。
再次运行应用程序(**F5**),以检查一切是否按计划进行……
对我来说看起来还可以,但并不完美!因为 **ProgressBar** 比 **ProgressDisplay** 文本略高,但让我们继续处理**按钮**和**滑块**……
样式化 Stop、Play 和 Pause 按钮
现在,我们首先应该做的是让所有元素都可见,因为音量**滑块**被部分遮挡了。这是因为包含所有元素的**Grid** 没有正确地自动调整大小。**PlayerControls Grid** 的**宽度**设置为 **Stretch**,但我们在**左右两侧**应用了**边距**,这限制了**宽度**。因此,我们为 **PlayerControls** 设置了固定大小,因为**手机**屏幕的总宽度(至少在**横向**模式下)始终是 **800 像素**。
因此,选择 **PlayerControls Grid**,将**Horizontal Alignment** 更改为 **Center**,并**重置** **Margins**,如下图所示。
现在将 **Vertical Alignment** 设置为 **Bottom**,并在**底部**应用大约 **32 像素**的**边距**,如下图所示。
这应该会调整 **Player Controls** 的大小并定位,如下图所示。
音量**滑块**现在完全可见,即使它紧贴**边框**的边缘,但一切都会按时完成……(实际上这只是 **Blend 4** 的刷新问题,并且并不罕见——不幸的是!)
选择 **StopButton** 元素并选择 **Edit Template > Edit a Copy**。
给这个新的**样式**起一个名字,比如“**PhoneButtonStyleStop**”,然后按 **OK**。
现在,如果我们查看 **Objects and Timeline**,如下图所示。
我们可以看到这个按钮**控件模板**与普通的 **Silverlight** 按钮略有不同。首先,我们没有 **ContentPresenter**,取而代之的是 **foregroundContainer**,我可以告诉您它包含 **ContentPresenter**。**foregroundContainer** 的父 **Border** 元素(**ButtonBackground**)代表构成按钮的视觉元素的边界(限制)。父 **Grid** 代表构成按钮的整个“**触摸目标**”。看看 **Grid** 元素,并注意到它应用了一个**背景画笔**,**Alpha** 为 **0%**。**Grid** 需要这个“透明”**画笔**才能“**IsHitTestVisible**”并成为用户交互的“**触摸目标**”区域。**ButtonBackground** 应用了一个**边距**,该边距绑定到“**TouchTargetOverhang**”**资源**,并且是目前提供按钮视觉元素与**Player Controls** 的**边框**之间的间距/填充。
现在,我不想在 **StopButton** 中使用**文本**,因为它有点粗俗,并且需要为每种语言进行翻译。(麻烦事!)因此,我将使用媒体领域中“**停止**”的全球公认符号/图标。一个方块!有几种方法可以做到这一点……两种方法都将使用 **Rectangle** 元素,但**布局**将不同……最明显的方法是为**Rectangle** 定义固定的**宽度**和**高度**,但这将不会随按钮的大小**缩放**。因此,相反,我将设置 **Rectangle** 的尺寸为按钮可见区域(不包括“**触摸目标**凸起”)的全部尺寸,并对 **Rectangle** 应用 **ScaleTransform** 使其与按钮成比例。但是,如果我更改按钮的比例,那么“方块”可能会变形为“矩形”,但我可以接受这一点,因为我看不出我需要从根本上改变按钮的比例,但我更有可能改变按钮的大小。
首先,让我们移除“**ContentPresenter**”,可以这样说,通过折叠 **foregroundContainer**。我不想删除它,因为我的老板(虚构的)可能不同意我使用符号/图标的决定,并要求恢复文本。
因此,选择 **foregroundContainer**,并在 **Properties** 选项卡中,将 **Visibility** 设置为 **Collapsed**。
现在,在我们继续在名为 **ButtonBackground** 的**边框**元素中插入一个**矩形**之前。我们需要记住,一个**边框**只能有一个子元素,而这个子元素当前是 **foregroundContainer**。而且,由于我希望**矩形**与**ButtonBackground**成比例**缩放**,因此**矩形**必须位于**边框**元素内部。
因此,在仍然选中 **foregroundContainer** 元素的情况下,选择 **Group Into > Grid**。(确保**边距**被**重置**,并且**对齐方式**设置为 **Stretch**)。
(检查**Grid** 的**可见性**是否为**可见**,并且**ContentPresenter**(**foregroundContainer**)是否仍然为**折叠**)。
选择新创建的**Grid**,插入一个**Rectangle**,**边距**为 **0**,将**对齐方式**设置为 **Stretch**,并将**宽度**和**高度**设置为**自动**。
将**Rectangle** 重命名为“**StopIcon**”,并设置 **X** 轴的**比例变换**(**RenderTransform**)为 **0.35**,**Y** 轴为 **0.6**。
(现在,当按钮尺寸改变时(**缩放**),**StopIcon** 也会随之改变)。
**StopIcon** 可能不是一个完美的“方块”,但不用担心,因为我们还没有确定整体按钮的确切尺寸!
现在,为了确保 **StopIcon** 始终与**边框**的颜色匹配,请选择 **Fill** 并选择 **Template Binding > BorderBrush**。
删除 **StopIcon** 的**描边**,**StopButton** 应该看起来像下图所示。
现在选择 **ButtonBackground** 并设置一个**圆角**为 **0,0,0,8**,如下图所示。
最后(使用“**BreadCrumbBar**”),转到 **StopButton** 的**样式**。
将 **BorderBrush** 更改为名为 **PhoneAccentBrush** 的**画笔资源**,如下图所示。
如果一切顺利,您应该会得到与下图相同的结果。
(**StopButton** 的颜色现在**绑定**到手机的**主题**)。
接下来,我们应该考虑按钮的大小,正如我之前所说,指南建议“**触摸目标**”的最小尺寸为 **9mm**(**34 像素**)。但我们不应将此尺寸视为“标准”尺寸,它是“最小”尺寸!而且,这可能只在空间有限时才相关,因为屏幕上同时有很多“**触摸目标**”。即显示键盘界面时。还应考虑无障碍性,以及谁可以使用此界面。我很幸运(算是)身体健全,但绝不希望不必要地排除某个用户群体使用产品或界面。出于这个原因,我认为控件(按钮)应该尽可能大,而不影响**Player Controls** 的设计、外观和风格。
因此,选择 **StopButton**,将**宽度**设置为 **120 像素**,**高度**设置为 **80 像素**。
对 **PlayButton** 和 **PauseButton** 重复此过程,使它们的大小相同。
现在,按照处理 **StopButton** 的方式,通过**样式化** **Play** 和 **Pause** 按钮。但不要在**边框**元素的任何角上设置半径!或者,您可以从这里下载 **按钮样式**。(**Play** 和 **Pause 图标**也包含在内……)
(上图显示了**已样式化**的**停止**、**播放**和**暂停按钮**)。
样式化音量滑块
您可能已经注意到,**音量滑块**与标准的**Silverlight 滑块**截然不同,并且**Thumb** 组件尺寸稍大。这显然是因为**手机**的“**触摸目标**”考虑因素,所以**Thumb** 足够大,易于控制。因此,虽然我们可以将其设置为任意大小,但请记住,交互对于设计**手机**至关重要。
右键单击并选择 **Group Into > Border**,如下图所示。
确保**滑块**的**边距**被**重置**,选择**边框**元素并将其重命名为“**VolumeSlider**”。
现在单击 **Advanced options** 并选择 **Local Resource > PhoneDefaultBorderThickness**。
这将所有侧面的**边框厚度**设置为 **3**,并自动与其他按钮**边框厚度**一起更新。
(我们也可以将外部**边框**元素设置为此**资源**,稍后可以这样做,但我们可能仍想尝试这些元素的各种厚度……)
将 **CornerRadius** 设置为 **0,0,8,0**,如下图所示。
现在将 **BorderBrush** 设置为 **PhoneAccentBrush 资源**,并匹配按钮的颜色(以及**主题**)。
接下来,将**边距**设置为 **Local Resource > PhoneTouchTargetOverhang**,如下图所示。
最后将**宽度**设置为 **160 像素**,**高度**设置为 **Auto**,如下图所示。
**Player Controls** 现在应该看起来像下图所示。
接下来,我们需要对**滑块**进行一些工作,使其与其他**Player Controls** 一致。
因此,选择 **Slider**,右键单击并选择 **Edit Template > Edit a Copy**。
给这个新的**样式**起一个名字,比如“**PhoneSliderStyleVolume**”,然后按 **OK**。
在 **Objects and Timeline** 中展开 **HorizontalTemplate** 以显示所有元素,如下图所示。
选择 **HorizontalTrack** 并将**可见性**设置为 **Collapsed**。
现在选择 **HorizontalThumb** 并将**高度**设置为 **Auto**,如下图所示。
在 **Artboard** 中,**Thumb** 现在将是父**边框**元素的全**高度**,以及它在父**子分割 Grid** 中所占部分的**全宽度**。
(请注意,**Thumb** 所占的**子分割 Grid** 的部分是**锁定的**。因此,**Thumb 宽度**由父**Grid** 设置)。
在仍然选中 **HorizontalThumb** 的情况下,选择 **Edit Template > Edit Current** 以访问 **HorizontalThumb** 的视觉元素。
选择 **ButtonBackground** 并为**左侧**和**右侧**设置 **8 像素**的**边距**。
这将使 **HorizontalThumb** 的“**触摸目标**”变小,更难选择,我们可以通过在父**Grid** 的**背景**上设置**填充/画笔**(**Alpha** 为 **0%**)来解决。
因此,我们缩小了 **HorizontalThumb**,希望使其更优雅,但这将意味着**Thumb** 无法到达**滑块**的末端。运行应用程序看看……当将**滑块**移动到 **100%** 时,我们可能还会遇到一些奇怪的行为(我遇到了)。我们可以通过在父**边框**内的**滑块**上设置**左右两侧**的**边距**为 **-8** 来修复**滑块**无法到达末端的问题,但奇怪的行为仍然存在……(我不敢肯定您也会遇到此问题)。但还有另一种方法可以缩小 **HorizontalThumb**,同时保持更大的“**触摸目标**”区域并且不会导致奇怪的**滑块**行为。
因此,删除您刚刚应用的所有**边距**以及**Grid** 元素的**背景画笔**。
退出 **HorizontalThumb Template** 并选择 **HorizontalTemplate Grid**,如下图所示。
现在转到 **XAML** 并将第二个**列宽度**更改为 **18 像素**,如下图所示。
(**HorizontalThumb** 现在更纤细、更优雅,但也更难选择)。
因此,请回到 **HorizontalThumb Template**,选择 **Grid** 元素并插入一个新的**Rectangle**。
将此 **Rectangle** 重命名为“**ThumbTouchTarget**”,并将**对齐方式**设置为 **Stretch**,**顶部**和**底部**的**边距**为 **0**,但**左侧**和**右侧**为 **-8**。
现在删除**描边**,将**填充**设置为任何颜色,并将**Alpha** 设置为 **0%**。
这将使 **HorizontalThumb** 的“**触摸目标**”区域变大,但仅在其左侧。因为 **Slider** 模板的 **HorizontalTemplate** 的**Z 顺序**设置得并不完全符合我们的要求。
因此,回到**Thumb Template** 之外,在 **HorizontalTemplate** 中,选择 **HorizontalThumb** 并将其拖到 **HorizontalTrackLargeChangeIncreaseRepeatButton** 下方,如下图所示。
这会改变 **HorizontalThumb** 的**Z 顺序**,使其有效地位于顶部,并且比 **HorizontalTemplate** 中的所有其他元素都可选择。这可能不是最干净的解决方案,但缩小**Thumb**、保持“**触摸目标**”同时避免任何奇怪的行为,是我目前能想到的最佳修复……
现在转到**滑块**的**样式**,并将**前景画笔**设置为 **PhoneAccentBrush**。(就像我们对其他控件所做的那样)。
我们几乎完成了**滑块**的工作,但现在我们需要让**滑块**更明显地表明它用于控制**音量**。我计划通过两种方式实现:首先,在**音量滑块**的下半部分放置一个半透明的图层,其次,在该部分引入一个**扬声器图标**。
因此,回到**Slider Control Template**,选择 **HorizontalTemplate**,然后拖出一个**Rectangle** 来填充**子分割 Grid** 的第一个部分,如下图所示。
确保**边距**被**重置**,**对齐方式**设置为 **Stretch**,并**移除描边**。
然后将**填充**设置为 **Template Binding > Foreground**,如下图所示。
现在,通过将其拖到 **HorizontalTrack** 的后面,将**矩形**移回**Z 顺序**,如下图所示。
最后,将元素**不透明度**更改为 **50%**,使 **Artboard** 看起来像下图所示。
现在从这里下载 Speaker Icon 并将其添加到项目中。
打开 **Speaker.xaml** 页面,选择并**复制** **Path** 元素,然后返回 **MainPage.xaml**。
回到 **Slider Template**,在选中 **HorizontalTemplate** 的情况下,**粘贴** **Path** 元素。
将**Path** 重命名为“**SpeakerIcon**”,**重置** **边距**,并将**填充****Template Bind** 到**样式**的**前景**。
接下来,在“高级选项”的“宽度”下,将“模板绑定”至“滑块”的“值”,如下图所示。
对“高度”执行完全相同的操作,再次将其“模板绑定”至“滑块”的“值”。
这将把“宽度”和“高度”都设置为0.5,如下图所示。
现在,通过为X轴和Y轴设置40的“缩放变换”来“缩放”SpeakerIcon。
在“画板”中,SpeakerIcon可能看起来像下图,并且不在“水平模板网格”的正确“细分”区域中。
首先,通过将SpeakerIcon放置在Rectangle前面,将其移回到Z顺序,如下图所示。
(我更改Z顺序没有其他原因,纯粹是为了保持良好组织。我只是更喜欢将可选择/可交互的项放在前面)。
最后,将SpeakerIcon移动到“水平模板网格”的第一个“列”中,如下图所示。
(确保“边距”已“重置”)。
在座的一些有经验的程序员可能会说,我不应该将“宽度”和“高度”绑定到SpeakerIcon的“缩放”,因为这会影响应用程序的性能。因为它每次更改“音量”时都会导致“布局”重绘。你们可能是对的,但首先这是一个演示应用程序,其次这是我唯一能让它工作的方式。设置固定的“宽度”和“高度”,同时将“缩放变换”绑定到SpeakerIcon不起作用,我的有限的“自定义表达式”尝试也无效。
关于“音量滑块”,我最后想做的是更改初始(开始)“音量”,目前设置为0.5(半音量)。但我们无法在“音量滑块”中做到这一点,因为“滑块”的“值”绑定到MediaElement的“音量”属性。
因此,转到MediaElement,在“属性”选项卡的“媒体”部分,将“音量”更改为0.8,如下图所示。
运行应用程序以查看我们的“播放器控件样式”的结果,如下图所示。
现在看起来可以接受,并且可能是Windows Phone的简单干净界面的OK。它并不完全与Windows 7 Phone的默认样式“保持一致”,但这些控件是“视频播放器”的一部分,而不是“手机”本身。因此,我认为我有自由对其进行一些样式设计,并为“视频播放器”添加一些个性和身份。
样式化播放列表和ListBox
现在,让我们考虑如何选择和选中要播放的视频,以及如何在播放器和可能的视频选择之间进行导航。微软正在推广使用一种称为“Pivot Control”(以及“Panorama”应用程序)的东西,用于Web和Windows 7 Phone。基本上,它是一种以非常视觉化的方式导航大量数据的方法。手机上的主要导航旨在利用Pivot Control,并允许用户快速轻松地深入/钻取数据以找到所需信息。Pivot Control的操作可以想象成垂直圆柱体上的页面和一个中心枢轴,允许用户旋转圆柱体以在页面之间切换。虽然页面数量几乎是无限的,但建议使用最小数量(例如,最多4个应该是您的设计最大值),并且它们会循环。用户将能够通过触摸手势在页面之间“滑动”,因此“滑动”手势在Pivot Control中不应/不能被覆盖。
这是一个非常简单的视频/媒体播放器演示应用程序,我不会实现一个真正功能齐全的Pivot Control。而是一个“精简版 Pivot Control”,它只会切换“视频播放器”和“播放列表”。Michael已经预装了ListBox(播放列表)供选择视频,我非常乐意使用它而不是从头开始。所以让我们开始看看ListBox(播放列表),看看我们有什么...
选择ListBox并将其拖出,放大一点,如下图所示。
我们可以看到一列标题(可用的视频流),你可能会说的第一件事,如果你阅读了指南... 是所有标题都以大写字母开头,而指南规定“列表标题和项”不应大写。在指南中可以找到关于什么应该大写、什么不应该大写的完整列表。对于从流内容动态生成的列表,很难说该怎么做。因为没有100%可靠的方法来解析它们以确保正确的大写(或不),因为将它们全部重新格式化为小写在处理名称和首字母缩略词时是错误的... 所以我将这个问题留给微软,等待指示,并保持列表项不变。
现在,在我开始设置“Pivot Control”之前,我需要删除/更正LayoutRoot Grid的一个小错误。
所以,选择LayoutRoot元素,并使操作手柄可见,查看左上角。如下图所示。
上图中的Grid Row Divider设置为Auto,因此Collapsed。
选择“画板”中的Row Divider,并将其拖到标题区域的底部。或者转到XAML并设置Height为64,如下图所示。
如果拖动了Row Divider,则通过单击其旁边的图标两次,将Row从“Auto”更改为“Fixed”大小。接下来,在“布局”部分的“高级属性”中,将Height更改为64。
(直接编辑XAML确实更容易...)
现在选择名为TitleGrid的Grid,并将其移动到LayoutRoot的第一个Row中,重置Margins并确保Alignments设置为Stretch。
接下来选择名为ContentGrid的Grid,并重置Margins,使其占用LayoutRoot的第二个Row。
现在应该可以修复一般的Layout了,所以我们可以开始处理Playlist ListBox...
所以,选择ListBox,将其重命名为“PlayList”并将其拖到LayoutRoot上。使其看起来像下图。
现在定位PlayList ListBox,使其完全填充LayoutRoot的第二个Row,如下图所示。
接下来考虑我们可以做什么来样式化PlayList ListBox...
但在我们开始之前,我想考虑一下Phone ListBox与标准ListBox有何不同。首先,ScrollBar要小得多,并且不易选择。而且它不需要,因为微软已经将“滑动”手势集成到了Phone ListBox中,用于上下滚动。这样就省去了我们担心为ListBox添加“滑动”手势的麻烦(谢谢微软!)。接下来要考虑的是“触摸”界面,这个ListBox没有MouseOver State。为什么会有呢?触摸屏不知道手指悬停在屏幕上,在尝试使用Behaviors时也应记住这一点。因为你可能想要使用的一些Behaviors(即使只是在模拟器中)也不会作为Event Triggers与MouseEnter和MouseLeave一起工作。指南还规定“动态元素应避免使用阴影”,目前我看到Michael在玩,给ListBox项添加了阴影(坏开发者!)哈哈。
但是,关于无聊的指南内容就到此为止,让我们开始样式化ListBox,看看我们能做些什么让它更具吸引力。如果我们想保持在指南之内,可能无法做太多...
所以,选择PlayList ListBox,右键单击并选择“编辑附加模板” > “编辑生成项” > “编辑当前”。
在“对象和时间轴”中,展开TextBlock,选择DropShadowEffect并删除它。
现在选择TextBlock,并在“文本”属性中重置“字体大小”。因为这可能是设置“字体大小”的最后一个地方。(坏开发者!)哈哈。
(Michael只是在随便玩,设置了一个可用的Font Size,但它实际上不应该在这里设置 - 他基本上是期望我从头开始,而不是使用他现有的测试样本)。
在“画板”中,“文本大小”已减小,但我们稍后会处理...
所以,退出“编辑生成项模板”。
现在右键单击,并选择“编辑附加模板” > “编辑生成项容器” > “编辑副本”。
将此新Style命名为PhoneListBoxItemStyle,然后按OK。
现在我们已经生成了一个Style用于ListBox Items,使用BreadCrumbBar转到“项容器样式”的Style。
在Style中,转到“文本”属性,并将“字体大小”设置为“本地资源” > “PhoneFontSizeLarge”。
这是“文本大小”应该设置和修改的地方。(如果需要...)
现在回到“生成项容器模板”(ItemContainerStyle)。
选择State Manager以查看ListBox的所有可能States。
您应该会注意到,这个ListBox具有正常ListBox的所有States,但正如我之前所说。MouseOver State在“触摸”界面上是不可用的,无论是“手机”还是可能的Windows触摸界面显示器。因此,即使我们有一些可用的选项,我们总是需要考虑用户和应用程序的预期环境。例如,“双击”触摸手势在所有情况下(例如这里)可能不受支持,这意味着我们使用单击来表示选择。这意味着“Focused”并不是一个真正有效或可用的State,因为我们无法使某物处于“Focus”状态而又不“Selected”。很容易想当然地认为我们习惯于在正常的PC情况下进行交互。因为“双击”和键盘交互对我们大多数人来说是天生的,而且我们需要时间来适应“触摸屏”界面的不同考虑因素,特别是当我们目前只有一个模拟器可以使用时...
因此,我们真正能用的是“Selected”和“Disabled”States,它们几乎已经为我们设置好了。我们实际上不应该玩弄“LayoutStates”(Loaded和Unloaded),因为这可能会干扰并削弱“Pivot Control”,并且会使本教程更长...所以我们只做一个小的调整然后继续。(尽管我可能会在最终项目下载中做更多,但只做我之前教程中涵盖的内容:ListBox Additional Templates)。
在“Base”State下,单击“Selected”State旁的小黑圈,以显示此State(由Eye图标表示),但不设置任何Keyframes。
在“对象和时间轴”中,选择Highlight元素,并设置Top Margin为8,以便更好地将蓝色的“Highlight”条居中在文本上。
这就是我在这里想做的,显然还有更多的事情可以做。但请记住我之前关于与“Metro”样式保持一致的“外观和感觉”的说法。这个教程肯定很长,这里的内容太多了...
设置Pivot Control和手势
为了让播放列表“滑入”自右侧,而播放器“滑出”至左侧,我们将使用Visual States。
(所以,退出您当前所在的任何Templates,确保您可以看到LayoutRoot及其3个子元素(TitleGrid、ContentGrid和Playlist))。
首先,将ContentGrid重命名为“Player”。
现在,展开Player Grid以显示MediaElement和Michael设置的3个“EventToCommand”。
现在,为了测试Michael在代码“View Model”中添加的“滑动”手势,他已将其连接到视频播放器的播放和停止功能。为了确保它们在显示视频的屏幕区域上工作,他已将其附加到MediaElement。现在我不想让这些“滑动”手势控制视频,因为这会覆盖“Pivot Control”的功能,并且严重违反指南。所以删除FlickRight_Play和FlickLeft_Stop EventToCommand,但不要删除SetMediaOpened EventToCommand,因为我们仍然需要它。
值得记住的是,这种“行为”是设计者可以设置并且应该设置的。开发人员为您提供了一个可以绑定的挂钩框架。而且,由于设计者可以在Blend中做到这一点,因此他们有责任使用开发人员提供的功能,并通过“点按”进行连接。所以,如果您是新设计师,请通过查看这些Behaviors来研究UI元素是如何“连接”的,因为它们非常强大,并且是Blend作为设计者生命的血液。在我编辑时,Michael刚刚告诉我一个强大的新Behavior/功能DataStore。
所以请记住,设计师们:您的开发人员会/应该非常乐意帮助您完成连接的更多技术方面,并且他们只会/应该施加一个条件:不要一遍又一遍地问他们同样的问题!-学习!(附注:对于编码人员:对无知的人要友善,但对于愚蠢的人随便您...(除了我!)哈哈)
现在选择LayoutRoot元素,转到“Assets”选项卡并选择“Behaviors”。
接下来,选择“GoToNextState”Behavior并将其拖到LayoutRoot上。
既然我们在这里,也请将“GoToPreviousState”Behavior拖到LayoutRoot上。
通过将这些Behaviors添加到LayoutRoot,当与LayoutRoot(整个屏幕)进行交互时,Behaviors将被Triggered/Fired。现在我们需要设置一些States供这些Behaviors进行切换/交互...
(真正的“Pivot Control”应该能够循环,而且不容易做到。所以这里不会这样做,只是一个基本的页面切换,比讨论的指南中的“Panorama”更像。据我所知,一个真正的预定义“Pivot Control”应该很快就会出现)。
所以,转到States选项卡,然后单击“Add state group”图标,如下图所示。
这将向项目添加一个“VisualStateGroup”,我们将称之为“ShowOrHidePlayList”。
现在单击“Add state”图标两次,为这个StateGroup添加2个States。
将这2个新的States重命名为“ShowPlayer”和“ShowPlayList”,如下图所示。
现在,确保您处于“Base”State下,并在“对象和时间轴”中选择PlayList ListBox。
在Properties选项卡的“Transform”部分,设置X轴的Translate为800。
在“画板”中,这将像下图一样,将PlayList ListBox沿X轴移动。
现在,在States Manager(VSM)中,选择PlayList ListBox,选择“ShowPlayList”State,并将Transform Translate X轴改回0。
接下来,在“对象和时间轴”中选择“Player”Grid,并设置X轴的Transform Translate为-800。
此时Artboard应该看起来像下图。
现在,设置“ShowOrHidePlayList”State Group的Transition的Duration为1秒,以及您喜欢的任何EasingFunction。我使用了“Back InOut”,如下图所示。
现在您可以运行(F5)应用程序,查看结果,并希望看到Player和PlayList使用鼠标单击滑动进出。但我们不想使用鼠标单击进行此操作,我们希望使用“滑动”手势在屏幕/页面之间切换。
(注意:我们实际上并不是以切换(交替)方式触发触发器,只是不断循环 - 我认为...)
所以,选择我们在LayoutRoot上设置的“GoToNextState”Behavior,并在Properties选项卡中找到TriggerType并选择“New”。
在弹出窗口中,选择“FlickGestureTrigger”并按OK。
现在,在Properties选项卡的“Trigger”窗格中,将Direction更改为“Left”,如下图所示。
现在选择“GoToPreviousState”并重复整个过程,但这次将方向设置为“Right”。
当应用程序现在运行时,只有“左”或“右”的“滑动”才会改变Player或PlayList。- 搞定!!!
但这并不是完整的“Pivot Control”,因为我们需要考虑Player和PlayList的标题区域。这应该与Player和PlayList一起更改,以显示我们处于“Pivot Control”的哪个部分/页面。
所以,选择TitleGrid Grid并查看Background颜色。注意它是深Grey色,这可能是Michael在设置视频播放器的通用组件时设置的。虽然这可能没问题,但它给了我一个讨论设计Windows 7 Phone的另一个考虑因素的机会。那就是电池续航!显然,显示除Black以外的任何颜色都会消耗宝贵的电池电量,而使用White显然/可能最糟糕。此外,这是一个视频播放器,它可能比其他任何东西都更快地消耗电池...所以我的个人意见是,将Title区域设置为Black以减少电池消耗,并且不分散应用程序的焦点,即播放视频...(尽管我保留在背景颜色方面改变主意的权利!)
所以,在仍然选择TitleGrid的情况下,将Background更改为Brush资源PhoneBackgroundBrush。(确保您回到“Base”State)。
现在,展开TitleGrid,选择TextBlockListTitle,并将Foreground Brush设置为PhoneForgroundBrush。
现在将TextBlockListTitle的Text更改为“video player”,全部小写,因为指南规定“页面标题”应全部小写。然而,在我们的情况下,情况并非如此简单。因为指南还规定“应用程序标题”应全部大写。但我们在视频播放器中实际上没有足够的空间来同时显示应用程序标题和页面标题。因为它会影响视频显示的观看区域,而这绝对不是我们想要的...所以,我决定将我们的“标题文本”视为“页面标题”,因此全部小写。
接下来,将Font Size设置为 Local Resource > PhoneFontSizeLarge.
并将Font设置为Local Resource > PhoneFontFamilyNormal。
Font实际上不是Theme的一部分,所以更改Theme不会影响Font,但只要Font与PhoneFontFamily Resource绑定/关联,我们就可以在一个地方进行更改。
现在,确保TextBlockListTitle是Left和Top Aligned,Left边距为20。
接下来,将TextBlockListTitle重命名为“TextBlockPlayerTitle”,然后使用Copy和Paste复制它。
将复制的TextBlock重命名为“TextBlockPlayListTitle”,并将文本更改为“video playlist”。(全部小写)。
在Base State中,为TextBlockPlayListTitle设置X轴的Translate为800。
在ShowPlayList State中,将Translate改回0,选择“TextBlockPlayerTitle”并将X轴的Translate设置为-800。
这将使页面标题随Player和PlayList一起更改。这很好,但我们可以做得更多...例如,在Player视图中显示正在播放的视频/流。以及提供其他/替代导航到Player和Playlist屏幕/页面之间的Flick手势。指南显示,通过在屏幕右侧(Panorama)显示下一页的提示,这是一个好主意。但这对于视频播放器来说并不真正适用,因为屏幕的整个部分都应该用于显示/播放视频。现在我们可以显示视频播放器屏幕/页面右上角的播放列表标题的一部分。但这在屏幕之间切换时会有点混乱,而且不如显示图标这样的替代方法清晰。所以,废话不多说,让我们开始吧...
现在,确保您处于Base State下,选择“TextBlockPlayerTitle”并选择Group Into > StackPanel。
将StackPanel重命名为“PlayerTitleAndInfo”,将Orientation更改为Horizontal,并确保Margins为0,但Left边距为20。(Horizontal Alignment应为Left,VerticalAlignment应为Stretch)。
展开PlayerTitleAndInfo以显示TextBlockPlayerTitle,并使用Copy和Paste复制它。
将此副本重命名为“TextBlockSpacer”,并将Text设置为“ - ”。(“空格”、“减号”、“空格”)。
再次复制此元素,并将新元素重命名为“TextBlockPlayerInfo”,如下图所示。
TextBlockPlayerInfo用于显示当前选定的视频标题,为此,我们需要Data Bind到PlayList ListBox的SelectedValue。但是,为了做到这一点,我们需要将PlayList ListBox的SelectedValue的格式从Object转换为String。所以我们需要去找开发者(Michael),说:先生,我能有一个ValueConverter吗?只要您的开发者心情好,只需一分钟就能为您做好。所以下载VideoToVideoTitleConverter并将其添加到Project。
选择TextBlockPlayerInfo,在Text属性的“高级选项”中,选择“Data Binding...”。
在弹出窗口中,选择Element Property选项卡,选择PlayList,选择SelectedItem属性,然后选择VideoToVideoTitleConverter,最后按OK。
现在将Font更改为Local ResourcePhoneFontFamilyLight。
在“画板”中,标题区域应该看起来像下图。
运行应用程序以查看结果,当我们使用“滑动”手势在Player和Playlist页面之间切换时,我们遇到了一个小问题。TextBlockPlayerTitle移动了,但TextBlockSpacer和TextBlockPlayerInfo在切换页面时没有移动。所以,转到State Manager(VSM),在PlayList State中删除附加到TextBlockPlayerTitle的RenderTransform。取而代之的是,选择父StackPanel(PlayerTitleAndInfo)并将PlayerTitleAndInfo StackPanel的Translate设置为-800。这样应该可以解决问题...
因此,我们现在在屏幕顶部显示选定的视频,对于短标题来说这很好,但我们需要考虑长标题会发生什么。我可以告诉您,它会溢出到PlayList页面/屏幕上。所以我们需要确保这种情况不会发生,最简单的办法是为父StackPanel设置一个Fixed大小。只要视频播放器始终处于Landscape模式,这就可以了,但如果以后视频播放器需要以Portrait模式工作,它将不起作用。(因为它会再次溢出到PlayList页面)。所以,我们暂时将StackPanel保留为“AutoSized”,但要为Right侧(以及Left侧)设置一个Margin。
所以,选择PlayerTitleAndInfo StackPanel,将HorizontalAlignment更改为Stretch,并将Right边距设置为60。
(我想要Right侧有这么大的Margin的原因是,我想在标题区域的TopRight角放一个图标)。
但在那之前,我想调整Title区域的大小,与MediaElement(视频屏幕)相比。因为目前Title区域比我们需要的要大一点,这会影响MediaElement的大小和应用程序的焦点。
所以,选择LayoutRoot元素并稍微调整Row Divider,以扩大MediaElement显示区域。或者,直接更改XAML,使第一个RowDefinition大约为52像素,如下图所示。
接下来从这里下载ArrowIcon并将其添加到Project。
将ArrowIcon Path复制到MainPage的TitleGrid区域,并将其重命名为“ArrowIconRight”。
现在为它设置固定的Width和Height均为32,并将Stoke更改为PhoneForgroundBrush。
接下来,转到Fill并将Alpha更改为0%。- 不要删除Fill“No Brush”,因为这会使ArrowIconRight更难选择。虽然这可能不会对触摸界面产生任何影响,但在模拟器中会产生影响,在模拟器中,可以单击图标的圆形区域,而鼠标指针却错过了可选择区域。
接下来,将Horizontal Alignment设置为Right,将Vertical Alignment设置为Stretch。
最后,确保所有Margins均为0,除了Right边距,应设置为大约10像素。
此时Artboard应该看起来像下图。
现在转到Assets选项卡,并将GoToStateAction Behavior拖到ArrowIconRight上。
在Action的Triggers部分,保持EventName不变,并将StateName更改为“ShowPlayList”,如下图所示。
这将为用户提供另一种导航到PlayList的方法,但我们也需要在Playlist页面上有一个图标,以返回到Player页面。
所以,复制ArrowIconRight,并将其重命名为“ArrowIconLeft”。
在Transform部分,沿X轴Flip ArrowIconLeft,如下图所示。
现在选择ArrowIconLeft的“GoToStateAction”。
并在Trigger部分,将StateName更改为“ShowPlayer”。
再次选择ArrowIconLeft,并在Base State中,设置X轴的Translate为800。
现在转到PlayList State,并将X轴的Translate改回0。
然后在仍然处于PlayList State时,选择ArrowIconRight并将X轴的Translate设置为-800。
希望当您运行应用程序时,一切都能正常工作!
视频播放器控件
现在来看看我们之前样式化的控件,以便在不使用时可以隐藏它们。并且应考虑最佳的用户交互。例如,我们希望在视频播放器启动时显示控件,但又不要求用户执行任何操作来删除/隐藏它们,让他们享受视频/电影。(所以我们实际上需要在视频启动时将其放在计时器上)。我们还需要考虑用户可能不想等待控件在计时器上“淡出”视野。因此,我们还需要一个机制来允许这样做,以及一个显示控件的机制,当用户想要使用它们时。我们还需要确保在使控件显示的同时,用户不会意外选中任何控件。因为用户的耐心会很快消失。特别是如果他们只想暂停电影或调整音量,却意外地导致视频回到开头。用户交互和用户体验只能通过用户反馈来衡量和改进,但我们应该尽力为用户提供最好的产品作为起点。
我上面陈述的所有内容都基于假设和我个人的经验。这些可能是对的,也可能是错的。但除非我们有了真正的手机,并将其交到用户手中,否则我的假设就是我所拥有的。另一个限制因素是,我将不做任何代码操作,虽然Blend很棒,但可能需要一些自定义操作和代码来实现视频播放器用户交互的最佳用户体验。所以,虽然我对我要做的事情不是100%满意,但这足以评估用户反馈以改进用户体验。而用户体验才是真正重要的...
我们可以使用Visual States来操作播放器控件的显示和隐藏。但我们已经在使用Visual States来导航Player和Playlist屏幕/页面之间。而且,由于这种导航已经使用了Behaviors来“GoToNextState”和“GoToPreviousState”,它可能会干扰我们添加的任何其他States。所以,我将使用Storyboards来操作播放器控件,并且Storyboards还有一个优点是很容易在Storyboard的任何点设置持续时间/计时器。
正如您所料,我们将使用2个Storyboards。(一个用于显示播放器控件,一个用于隐藏播放器控件)。显示播放器控件的Storyboard将通过计时器来实现,并在设定的持续时间后隐藏播放器控件。所以,让我们开始吧...
但在我们开始添加Storyboards之前,我们首先需要几个元素作为Triggers来驱动Storyboards。这些将是透明的Rectangles(Fill为0% Alpha)。我们需要两个的原因是,为了创建播放器控件的显示/隐藏行为的Toggle排列。在Blend中,不可能将两个不同的Behaviors/Triggers附加到同一个元素/对象上,并期望它们以交替的方式触发。所以,我们将把一个Behaviour/Trigger附加到每个上面,当前面的Rectangle的Trigger被触发时,我们将启动我们的Storyboard,同时缩小/移动这个Rectangle以显示后面的第二个Rectangle。当用户再次单击时,第二个Rectangle上的Trigger将被触发,在我们启动第二个Storyboard的同时,我们将恢复第一个Rectangle。从而为鼠标单击或用户“Touch”手势生成一个Toggle类型的排列。简单!
所以,在“对象和时间轴”中,选择Player元素,并插入2个Rectangles,分别命名为“ShowPlayerControls”和“HidePlayerControls”。
删除两个Rectangles上的Stroke,并将两个Rectangles的Fill设置为Black,Alpha为0%。
现在将Horizontal Alignment设置为Stretch,Vertical Alignment设置为Bottom。以及Width设置为Auto,Height设置为180像素。
接下来,再次选择Player元素,并插入一个包含文本“Click in shaded area to show/hide player controls”的TextBlock,并将TextBlock重命名为“TextBlockShowAndHideControls”。
设置Horizontal Alignment为Center,Vertical Alignment为Bottom,Bottom边距约为100像素,Left和Right边距各为50像素。
(我之所以为Left和Right边距设置边距,是因为我想考虑如果视频播放器以后支持Portrait模式会发生什么。因为没有Margins,文本会溢出到屏幕边缘。我们还需要考虑如果我们支持Portrait模式,文本会如何Wrap)。
所以,转到Text属性的advanced properties,并将Text Alignment设置为“Center”,并确保TextWrapping设置为“Wrap”。
现在将Foreground颜色设置为Resource PhoneForegroundBrush,并将Font Size设置为Local Resource > PhoneFontSizeLarge。
最后,在Object and Timeline中,按照下图所示的顺序排列Player Grid中的项:MediaElement,ShowPlayerControls,TextBlockShowAndHideControls,HidePlayerControls,PlayerControls。
此时Artboard应该看起来像下图。
(上图是我选中了一个透明的Rectangle,只是为了显示其位置)。
选择TextBlockShowAndHideControls并将元素Opacity更改为0%以使其不可见。
现在转到Objects and Timeline并单击+图标,向Project添加一个新的Storyboard。
在弹出窗口中,将Storyboard命名为“ShowPlayerControlsOnTimer”,然后按OK。
这将打开Storyboard时间轴,并按F6在Blend中将视图更改为“Animation Workspace”。
在Timeline的0.1秒处,选择HidePlayerControls元素并设置一个Keyframe,如下图所示。
现在选择PlayerControls,并在0.1秒处设置一个Keyframe,将元素Opacity更改为0%。
(在Storyboard中修改元素将自动为此元素生成Keyframe)。
在Timeline的0.0秒处,选择PlayerControls元素,将Opacity更改为0%,并在Transform部分,设置Y轴的Translate为200像素。
这将使PlayerControls移出屏幕底部,因此在隐藏(不可见)时无法选择。
现在选择HidePlayerControls元素,并在Transform部分,将Y轴的Scale设置为0。
并将Center Point的Y轴设置为1。
(这将以屏幕底部的“Collapsed”的Storyboard开始,而后面的Rectangle(ShowPlayerControls)能够接受鼠标单击或“Touch”。它可能不是必需的,但动画更容易理解,因为Rectangle从“Collapsed”开始动画)。
现在将Timeline移到0.5秒,并且在选中PlayerControls的情况下,将元素Opacity改回100%。
现在将Timeline移到12.0秒,并且在选中PlayerControls的情况下,设置另一个Keyframe。
接下来,将Timeline移到12.5秒,并将PlayerControls的元素Opacity更改为0%。
选择HidePlayerControls元素,并在12.5秒处记录/设置一个Keyframe。
现在,在仍然选中HidePlayerControls的情况下,转到12.6秒,将Y轴的Scale更改为0,并将Y轴的Center Point设置为1。
接下来,再次选择PlayerControls,并在Y轴的Transform部分,设置Translate为200像素。
这就完成了在首次加载视频时显示视频播放器控件所需的基本动画。从淡入,到计时器显示,再到淡出。我们还设置了移动前面Rectangle(HidePlayerControls)移位以显示后面的Rectangle(ShowPlayerControls)以生成我们的Toggle行为所需的动作。现在我们只需要提供一个视觉提示给用户如何操作视频播放器控件...
所以,将Timeline更改为13秒,选择ShowPlayerControls元素并设置一个Keyframe。
接下来,选择TextBlockShowAndHideControls,并在13秒处设置一个Keyframe。
将Timeline移到13.5秒,并且在仍然选中TextBlockShowAndHideControls的情况下,将元素Opacity更改为100%。
选择ShowPlayerControls,并在Timeline的13.5秒处,将Fill的Alpha更改为30%。
在“ShowPlayerControlsOnTimer”Storyboard的13.5秒处,Artboard应该看起来像下图。
将Timeline移到16.0秒,并为ShowPlayerControls和TextBlockShowAndHideControls元素设置一个Keyframe。
接下来,将Timeline移到16.5秒,并将TextBlockShowAndHideControls的元素Opacity更改为0%。
最后,选择ShowPlayerControls元素,并将Fill的Alpha改回0%。
这就完成了这个Storyboard,所以像之前一样,使用X图标将其关闭,如下图所示。
下一个Storyboard更简单,这个只是隐藏/淡出视频播放器控件,并完成Rectangles的Toggle行为。
所以,像之前一样,使用+图标创建一个新的Storyboard,将这个Storyboard命名为“HidePlayerControls”,然后按OK。
在Timeline的0.0秒处,选择HidePlayerControls并设置一个Keyframe。对PlayerControls也做同样的事情。
接下来,在选中HidePlayerControls的情况下,将Timeline转到0.1秒,将Y轴的Scale更改为0,并将Y轴的Center Point更改为1。
接下来,将Timeline转到0.5秒,并将PlayerControls的元素Opacity更改为0%。
将Timeline移到0.6秒,并在Y轴上设置Translate为200像素。
此时Timeline应该看起来像下图。
像之前一样,使用X图标关闭Storyboard。
所以,现在我们有了Storyboards,我们需要附加Behaviors/Triggers来触发和控制Storyboards。我们总共需要3个:一个用于在视频加载时启动“ShowPlayerOnTimer”,一个用于隐藏PlayerControls,最后,一个用于显示PlayerControls。
所以,转到Assets选项卡,并将“ControlStoryboardAction”拖到MediaElement上,如下图所示。
在Triggers部分,将EventName更改为“MediaOpened”,并将要播放的Storyboard设置为ShowPlayerControlsOnTimer。
接下来,将另一个“ControlStoryboardAction”拖到ShowPlayerControls上。
在Triggers部分,除将Storyboard设置为ShowPlayerControlsOnTimer外,其他所有设置保持不变。
最后,将另一个“ControlStoryboardAction”拖到HidePlayerControls上。
在Triggers部分,除将Storyboard设置为HidePlayerControls外,其他所有设置保持不变。
运行您的应用程序以查看结果!
我们应该具备所有基本的交互所需,尽管可以进行一些微调。例如,当在Playlist ListBox中选择一个电影时,它会被MediaElement打开并播放。现在想象一下,如果您在一段时间内没有返回播放器屏幕,此时,视频播放器控件已经通过计时器淡出了。很难说这是好事还是坏事...用户可能会觉得在使用视频播放器时感到不对,但同时,如果控件每次媒体更改时自动显示,可能会让用户感到恼火。只有测试才能说明。但对于这个演示应用程序,我认为最好保持现状,我并不是说这是对还是错...
最终调整
这就是这个演示视频播放器应用程序的差不多内容了,请记住这是一个演示!而不是Windows 7 Phone设计的硬性规定。我也不认为微软已经最终确定了指南,这些指南将来可能会发生变化...
回顾这个演示应用程序,我发现有些地方我不太满意。最明显的是Playlist ListBox。因为页面标题看起来和列表本身几乎一样!所以我将对此进行快速编辑,以便更容易区分页面标题和列表本身。最明显的方法是更改页面标题或ListBox的背景。如果我更改标题区域背景,我还需要真正更改播放器背景以使其保持一致。正如我之前提到的电池消耗问题,这可能不是最好的主意,尽管深灰色在功耗方面可能不算太糟。最好更改ListBox的背景,因为ListBox可能不经常显示,因此功耗考虑因素实际上不是问题。因为我们目前将黑色用作ListBox的背景,所以我显然需要将ListBox背景变亮。我有两种方法可以做到这一点,一种是使用灰色(或半透明白色)。或者使用颜色或半透明颜色...我们已经讨论过有4种主题颜色(如果包括制造商预留颜色,则为5种)。所以我需要考虑我应用的任何颜色在更改Theme时将如何工作。显而易见答案是使用当前Theme颜色,或当前Theme颜色的一丝提示。
所以,转到PlayList ListBox并选择Edit Additional Templates > Edit Generated Item Container > Edit Current。
复制Highlight元素,将其重命名为“BackgroundHint”,并将其放置在Highlight元素后面。
将Visibility更改为Visible,并将元素Opacity设置为10%。
在“画板”中,PlayList ListBox应该看起来像下图。
这不是最优雅的解决方案,但这更多的是关于强化主题和电池寿命考虑因素的练习。
我可能会尝试在可下载的演示版本中让这个屏幕更优雅一些,但这足以用于本教程。
我想要调整的另一件事是可选择的区域,以及能够隐藏或显示视频播放器控件的区域。目前,只有视频播放器控件周围的区域才能单击/触摸。然而,视频播放器控件(ProgressBar和ProgressDisplay)的上半部分目前没有可用的交互。但由于它们默认开启了“IsHitTestVisible”,它们会干扰后面(在Z顺序中)的任何元素无法被单击或“触摸”。对于TextBlockShowAndHideControls也是如此,虽然大部分时间是不可见的,但仍然阻止后面的ShowPlayerControls Rectangle被单击或“触摸”。
所以,在“对象和时间轴”中,选择ProgressInfo,并在Common Properties的advanced properties中关闭(取消选中)IsHitTestVisible复选框。
我们不必担心关闭所有子元素的IsHitTestVisible,因为它们将从父ProgressInfo元素继承此属性。
现在,对TextBlockShowAndHideControls重复该步骤,关闭IsHitTestVisible。
您现在可能想知道为什么“Flick”手势仍然有效,因为它们位于所有其他元素的后面(在Z顺序中)。而诚实的答案是,我不知道确切原因,但我的猜测是,因为它们同时监视运动和单击或“Touch”交互,因此工作方式与大多数Triggers完全不同。
接下来,我想微调一下Storyboards,因为显示/隐藏没有完全正常工作。因为目前,即使当用户想要隐藏播放器控件并启动“HidePlayerControls”Storyboard时,“ShowPlayerControlsOnTimer Storyboard”仍在播放。
现在最明显的事情是,向HidePlayerControls Rectangle附加另一个“ControlStoryboardAction”。
所以,这样做,并将ControlStoryboardOption设置为“Stop”,Storyboard设置为“ShowPlayerControlsOnTimer”。
接下来,打开ShowPlayerControlsOnTimer Storyboard,并将HidePlayerControls Rectangle的结束Keyframes拖到整个Storyboard的末尾,如下图所示。
(这将阻止ShowPlayerControls被选择,直到ShowPlayerControlsOnTimer结束)。
最后,转到HidePlayerControls Storyboard,并在0.0秒处为ShowPlayerControls和TextBlockShowAndHideControls元素设置Keyframes。
这将确保这些元素在动画启动时设置为正确的状态。因为它们在ShowPlayerControlsOnTimer Storyboard中被修改,否则可能不是它们正确的状态。
就这样,这个演示和教程就完成了。
希望它有所帮助... 并且请投票!