6 为手机开发

6为手机开发

         到目前为止在书中我们将注意力集中于用户体验。为你的应用程序进行适当的设计,对于你的应用程序是否成功是至关重要的,在手机上的开发过程会把它结合在一起。为手机开发你的应用程序,你需要理解Silverlight在手机上的工作方式。这就是你将在这一章学到的内容。

应用程序生命周期

         当你创建一个新的Silverlight for Windows Phone项目时,该项目包含了你开始构建一个手机应用程序需要的所有文件。这里有几个关键文件,但是让我们先从XAML文件。如你在如图6.1可以看到的,App.xaml和MainPage.xaml文件有与它们相关的代码文件(他们有一个“.cs”扩展名,因为这个项目是一个c#项目)。

                       

图6.1 在一个新项目中的重要文件

         这些代码文件分别代表了伴随主页面和应用程序类的代码。正如我们在第二章中讨论的,当你写第一个手机应用程序,App.xaml文件是我们用来存储全局(整个应用程序范围内)资源的位置。这个类也会随着App.xaml文件表示应用程序本身。事实上,当你的应用程序启动时它是这个文件第一个启动(不是MainPage.xaml的类)。这类被称为应用程序类,它继承于Application类:

         public partial class App :Application

         {

                   // ...

         }

         这个类是用于存储应用程序范围的代码/数据。例如,应用程序类为你的整个应用程序存储主框架。RootFrame属性公开了这个框架,你的所有页面将显示在这个框架里:

         public partial class App : Application

         {

                   /// <summary>

                   /// Provides easy access to the root frame

                   /// of the Phone Application.

                   /// </summary>

                   /// <returns>The root frame of the Phone Application.</returns>

                  public PhoneApplicationFrame RootFrame { get; private set; }

                   // ...

         }

         尽管这个应用程序类代表你运行的应用程序,它通过Application类的Current属性公开了一个单例。例如,在你的代码任何地方想获得当前应用程序类的,你应该调用应用类上的静态Current属性:

         Application theApplication = App.Current;

         你应该注意到Current属性返回应用程序类(基类)的一个实例。如果你想访问Application类实例本身的属性,你必须把它转换为App类,就像这样:

         App theApplication = (App)App.Current;

         var frame = theApplication.RootFrame;

         // ...or...

         frame = ((App)App.Current).RootFrame;

         在初始化应用程序类的过程中,RootFrame被创建同时在手机上显示给用户。但是如果你查看应用程序类的代码,也许看起来不明显,为何实际显示的是MainPage.xaml。诀窍就是我在图6.1中强调的第三个文件:WMAppmanifest . xml。

         Manifest文件中包含了一系列设置来帮助手机(和商城)来确定关于你的应用程序的信息。文件中的一段信息描述了如何启动应用程序。文件的Tasks章节部分包含一个DefaultTask元素,该元素定义了NavigationPage来启动你的应用程序。在这里你可以看到:        

         <?xml version="1.0" encoding="utf-8"?>

         <Deployment ... >

                   <App ... >

                            <!-- ... -->

                            <Tasks>

                                     <DefaultTask Name ="_default"

                                                                 NavigationPage="MainPage.xaml"/>

                            </Tasks>

                            <!-- ... -->

                   </App>

         </Deployment>

         一旦App类被创建和初始化,应用程序被告知导航到URI在DefaultTask的Navigation属性中。这就是为什么你的MainPage.xaml被显示。

更改XAML文件的名称

         如果你决定更改XAML文件的名称(和底层类文件),你必须确定在x:Class声明的名称也被修改。

         使用混合的应用程序类和清单文件,你的应用程序优雅地启动了同时你的第一个页面被显示了。这类似于典型的桌面应用程序如何启动,但是你的手机应用程序的生命周期实际上远远不同。

导航

         你的应用程序里没有窗体。这是一个重要的暗示,尽管它被称为Windows Phone, 名称中的“Windows”并不代表这个Windows是一个操作系统。你将不得不去习惯于一个不同的开发风格。整个应用程序模式是建立在基于页面导航的概念上,这应该让任何使用Web开发的用户感到舒适。

         当你启动一个应用程序时,它将导航到一个特定的页面(在WMAppManifest。xml文件的默认任务所决定的)。当用户导航到其他页面,页面的堆栈增长。用户可以按下在机身上的返回按钮回到应用程序内的上一页。如果用户在你的应用程序的第一个页面,按下返回按钮将退出应用程序,如图6.2所示。

         这种页面导航意味着一个忠告就是,在编写Windows Phone应用程序时候,没有明显的方式来退出应用程序。作为支持一个方法来关闭应用程序的替代方案,导航框架规定,当用户在应用程序的第一页按下返回按钮将退出程序,如图6.2所示。

对于Silverlight用户

         如果你已经使用了标准版Silverlight,你会注意到这个导航API看起来是一样的。最大的不同是不支持所有的导航模式,即使API使它们看起来很像。例如,在NavigationService中CanGoForward 和GoForward存在但不受支持。

         通过编程方式,你可以使用NagivationService类与导航设施交互。要访问应用程序的NavigationService类的实例,你有两个选择来找到导航APIs。

l  类的 NavigationService属性从PhoneApplicationPage继承(例如,添加到你的项目中 MainPage.xaml或其他“页面”项目内容添加到你的项目)。

l  PhoneApplicationFrame类,它具有NavigationService类所有的导航功能。

         尽管他们在功能上是相同的,这两种实现方式并不依赖于一个公共基类或者接口来强制执行。他们仅仅是按照惯例一致而已。

         NavigationService类有很多有用的方法,但是最常见的用法包括Navigate和GoBack。Navigate方法启动一个到一个新 “页面”的导航,就像这样:

         NavigationService.Navigate(new Uri("/Views/SecondPage.xaml",

                                                                                    UriKind.Relative));

         // or

         NavigationService.Navigate(new Uri("/Views/SecondPage.xaml?id=123",

                                                                                    UriKind.Relative));

         Navigate中的URI调用专门搜索,在.xap文件中查找一个文件,这通常是模仿你的项目结构来进行,如图6.3所示。

         HyperlinkButtons会自动连接来使用导航服务,所以这在你的XAML中工作的很好:

         <HyperlinkButton NavigateUri="/Views/SecondPage.xaml"

                                                        Content="Go to 2nd Page" />

         相反,NavigationService类的GoBack方法返回到导航页面堆栈顶部的页面 (或返回堆栈中,下一段落介绍)。这种方法模拟用户按下返回按钮:

         NavigationService.GoBack();

         当你浏览一个应用程序,NavigationService类跟踪所有的页面,所以GoBack能返回必要的页面。这个页面的堆栈称为返回堆栈。通过提供一个属性,NavigationService提供只读方式来访问返回堆栈:

         IEnumerable<JournalEntry> backStack = NavigationService.BackStack;

 

 

图6.2页面导航解释 

图6.3 URI映射到项目中的文件

         BackStack属性允许你遍历返回堆栈并询问每个页面的来源,但不改变它:

         // Iterate through the BackEntries

         foreach (var entry in NavigationService.BackStack)

         {

                   Uri page = entry.Source;

         }

         NavigationService允许的唯一变化是删除在backStack中最后一个条目。你可以通过RemoveBackEntry完成,这只会从backStack中删除当前页面:

         // Remove the last page from the navigation

         NavigationService.RemoveBackEntry();

         当导航发生时(即使在程序的启动),代表页面的类有机会知道它正在导航到什么地方。这是通过重写PhoneApplicationPage类上的方法实现的。这些可重写的第一种方法是OnNavigatedTo方法:

         public partial class MainPage : PhoneApplicationPage

         {

                   // ...

                   // I was just navigated to

                   protected override void OnNavigatedTo(NavigationEventArgs e)

                   {

                            base.OnNavigatedTo(e);

                            var uri = e.Uri;

                   }

         }

         此外,当导航离开一个页面时,也有一些可重写的方法(OnNavigatingFrom和OnNavigatedFrom):

public partial class MainPage : PhoneApplicationPage

{

         // …

         // Navigation to another page is about to happen

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)

{

         base.OnNavigatingFrom(e);

}

         // Navigation to another page just finished happening

         protected override void OnNavigatedFrom(NavigationEventArgs e)

         {

                  base.OnNavigatedFrom(e);

         }

}

         当导航发生变化时如果你想做出反应,NavigationService也提供了一些事件,包括Navigating,Navigated和NavigationFailed。在全局监测导航时,这些事件是非常有用的。

意外的循环导航

         在你的应用程序要小心、谨慎的进行导航,因为当你本想使用GoBack方法,而使用NavigationService的Navigate方法时,你可能不小心进入循环导航。

         例如,你可能想去在你的应用程序中的一个选项页面。当用户完成了改变选项时,使用Navigate方法返回上一页可能是吸引人的,但这样做会导致上一页在页面栈中出现两次。使用GoBack方法是回到以前页面的正确方式。

         正如之前所示,你导航到的URI可以包含查询字符串信息。你可以在你的OnNavigatedFrom方法得到URI并尝试手动解析查询字符串,但这是不必要的。 NavigationContext类支持简单的访问查询字符串。例如,如果你想从查询字符串中得到ID,你可以简单的使用NavigationContext代替:

         protected override void OnNavigatedTo(NavigationEventArgs e)

{

         base.OnNavigatedTo(e);

         if (NavigationContext.QueryString.ContainsKey("id"))

         {

                   var id = NavigationContext.QueryString["id"];

                   // Use the id

         }

}

         通过使用NavigationService,NavigationContext和PhoneApplication类,你可以控制在你的应用程序的各个部分进行导航,以及给用户一个更直观的体验。

墓碑机制

         当我们回顾第一章,介绍Windows Phone, Windows Phone支持一个概念叫tombstoning。tombstoning的想法基本上是给人以多任务的错觉,没有触及在同一时间运行多个应用程序的现实。实现这种幻觉,你的应用程序可以在运行,休眠和暂停状态之间转换,如图6.4所示。

         当你的应用程序在屏幕上时(用户可见的)应用程序处于运行状态。但是大量的东西可以打扰你的应用程序,包括一个电话,一个“toast”提示,甚至包括用户按下启动或搜索键。当一些事情中断了你的应用程序,你的应用程序将得到通知,它将被置为失效。在失效的过程中,对你的应用程序你可以保存某些数据(或状态)。一旦失效完成后,你的进程被移动到一个休眠状态。如果操作系统决定了,它需要你处于休眠状态的应用程序使用的内存时,操作系统会将你的应用程序从内存中移除(称为悬浮状态),但将保留你在失效过程中保存的任何数据(状态)。如果用户返回到你的应用程序(通常是通过后退按钮),你的应用程序被通知说它将被激活。激活过程中你需要确定,手机是从休眠或中止状态被激活,并且决定是仅仅重新启动应用程序,还是将保存的状态还给你应用程序以使其恢复原状。对于用户而言,应用程序应该采取相应的行动,就像它一直在后台运行。

 

图6.4 墓碑机制如何工作    

墓碑机制

         Tombstoning给人以“多任务处理的幻觉”并且没有任何开销。它允许操作系统加载和卸载应用程序就好像它是在后台运行。用户应该没有注意到你的应用程序是被停止或卸载。它应该是“一直在工作”。

         这个过程通过PhoneApplicationService类公开给你,这个类的实例默认情况下在项目的App.xaml文件中创建:

         <Application ...>

                   <!--Application Resources-->

                   <Application.Resources>

                   </Application.Resources>

                   <Application.ApplicationLifetimeObjects>

                            <shell:PhoneApplicationService

                                     Launching="Application_Launching"

                                     Closing="Application_Closing"

                                     Activated="Application_Activated"

                                     Deactivated="Application_Deactivated" />

                   </Application.ApplicationLifetimeObjects>

         </Application>

         PhoneApplicationService类被创建成与我们先前讨论过的应用程序类具有相同的生命周期。当它被创建时在,它在应用程序类自身中包装成4个事件。默认的Windows Phone应用程序模板创建了该对象,并已经为你进行了绑定。如果你查看App.xaml的cs / vb文件(在应用程序类定义),你将在App.xaml类中看到提到的相应的方法:     

         public partial class App : Application

{

// ...

private void Application_Launching(object sender,LaunchingEventArgs e)

{

         // ...

}

private void Application_Activated(object sender,ActivatedEventArgs e)

{

         // ...

}

private void Application_Deactivated(object sender,DeactivatedEventArgs e)

{

         // ...

}

private void Application_Closing(object sender,ClosingEventArgs e)

{

         // ...

}

         // ...

}

         这些样板的功能,在你的应用程序更改的状态时被调用。第一个和最后的事件(启动和关闭),每个只发生过一次:当你的应用程序首次直接由用户启动时(例如,通过点击应用程序图标或响应一个toast通知),和当用户关闭应用程序时(例如,在应用程序的第一页按下返回)。但对于墓碑机制真正神奇的是发生在其他两个处理程序:Activated和Deactivated。这些处理程序是在tombstoning期间被调用,以允许你保存和加载状态到你的应用程序中。

         为了保持你的状态,PhoneApplicationService类允许你访问属性包(property bag)以存储数据。这个类支持一种状态属性,它是一个IDictionary<string, object>集合,用于通过名称存储序列化的对象。使用这个类,只需要使用这个类的静态的Current属性来获得当前服务对象和使用该类的状态属性。下面的示例是将一个简单的对象(颜色)存储到状态属性中:

         private void Application_Deactivated(object sender,DeactivatedEventArgs e)

         {

                   PhoneApplicationService.Current.State["favoriteColor"] = Colors.Red;

         }       

 

注意

         在一般情况下,如果你刚刚开始学习Silverlight和.NET,序列化对象的概念是一个对象在内存版本被以兼容格式写入暂时或永久存储。这个存储通常是提取内存中的对象并储存他们以备反序列化回内存对象。当你把对象存储到PhoneApplicationService的状态集合时使用了序列化。但是序列化也可以用于将数据存储到手机上的内存时,或者甚至当你想跨越网络连接保存数据时。        

         应用程序类是一个常见的地方来储存这种状态,但是由于PhoneApplicationService类才是真正的奇迹发生的地方,你可以在任何地方处理这些状态。例如,在一个单页面应用程序中,你可能会随着页面导航使用PhoneApplicationService类来存储数据(这也在tombstoning期间发生):        

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)

{

base.OnNavigatingFrom(e);

PhoneApplicationService.Current

                                                        .State[MoodKey] = moodPicker.SelectedItem;

}

         因为PhoneApplicationService的Current方法总是指向应用程序的当前服务,你可以在任何地方使用这个状态类。

存储墓碑状态

         虽然墓碑机制给了你机会去保存状态,你应该只在这里保存短暂的状态。墓碑状态应该仅满足于重新启动应用程序。你可以保存寿命更长或非易失性的状态到独立的存储里面(在后面章节讨论)或者存储在云里。tombstoning状态的生命是由操作系统管理的并且可能不受你控制的消失。因此,墓碑状态应该仅仅是为了满足你的应用程序从被暂停到被“唤醒”的最小状态。墓碑的状态越大,将你从tombstoning中唤醒的时间就越长。因为用户不应该注意到墓碑机制,最小化这个状态是非常重要的。        

         当你的应用程序被激活时,你可以检查事件参数的IsApplicationInstancePreserved属性,来判断你的应用程序是休眠还是实际上的暂停。如果它是被暂停你可能需要恢复在Deactivated事件中保存的状态:

private void Application_Activated(object sender,ActivatedEventArgs e)

{

         if (e.IsApplicationInstancePreserved)

         {

                   // Nothing to do as your application was just 'Dormant'

         }

         else

         {

                   // Recover State and resurrect your

                   // application from being 'Suspended'

                   var stateBag = PhoneApplicationService.Current.State;

                   var state = stateBag["favoriteColor"];

                   var color = (Color)state;

         }

}

         在激活期间你拥有一个完整的十秒种来处理数据。十秒结束已经是很长的时间了,但它并不能作为基准。用户可能在这之前放弃。一般来说,调优这个过程使它尽可能的快,将会给你一个更好的用户体验。

         默认情况下导航框架也是应用程序墓碑机制的一部分,所以当应用程序被激活时,第一个被导航到的页面是用户最后使用的页面。此外,页面堆栈也会被保存,因此你不必手动保存此信息。应用程序会在与他们被停用时相同的地方(智能导航)被激活。到目前为止,你应该理解一个手机应用程序的生命周期是如何工作的。了解了这些,你的应用程序需要处理导航、墓碑机制和启动/关闭等行为的混合,使用这些知识武装你来构造你的手机应用程序。

手机体验

         手机是不同的。是的,我们已经在这本书谈过了,但是它值得重申。手机是不同的。开发体验也是不同的。在你开发应用程序的同时,你将不得不面对与用户在手机上的体验相关几个不同方面:手机方向,触摸输入,手机特有的控件,以及应用程序栏。在这个小节中,你将学习到如何处理这些不同领域的功能。

方向

         手机支持三种主要方向:纵向、左横向和右横向,如图6.5、6.6和6.7。现在所有的手机上都支持这些方向。你可以控制你的应用程序支持的方向以及在他们之间切换。

 

图6.5纵向

 

图6.6 左横向方向

 

图6.7右横行方向

         在PhoneApplicationPage类上,你可以给你的应用程序指定你希望支持的方向:

         <phone:PhoneApplicationPage

                                               ...

                                               SupportedOrientations="PortraitOrLandscape">

         SupportedOrientations接受SupportedPageOrientation枚举中的一个:Landscape, Portrait, 或PortraitOrLandscape。通过指定PortraitOrLandscape,你告诉手机,你希望允许随着手机的转动来改变方向。默认情况下,手机简单地尝试使用新方向以显示你的页面。因为默认的项目模板创建一个页面,在本质上只是一个网格内嵌套一个网格,通常这会正常工作。但在很多情况下,当用户改变手机方向时你想定制体验。PhoneApplicationPage类支持一个当方向发生改变时触发的OrientationChanged事件,这是一种常见的方法来改变用户界面:

         public partial class MainPage : PhoneApplicationPage

{

         // ...

         // Constructor

         public MainPage()

         {

                   InitializeComponent();

                   // Change size of app based on changed orientation

                   OrientationChanged += MainPage_OrientationChanged;

         }

}

         这会给你一个机会根据需要更改你的应用程序。例如,如果你只是想调整你的一些用户界面,你可以根据方向扩展该应用程序:

         void MainPage_OrientationChanged(object sender,

                                                                                                       OrientationChangedEventArgs e)

         {

                   if ((e.Orientation & PageOrientation.Landscape)

                            ==PageOrientation.Landscape)

                   {

                            theScaler.ScaleX = theScaler.ScaleY = .86;

                   }

                   else if ((e.Orientation & PageOrientation.Portrait)

                            ==PageOrientation.Portrait)

                   {

                            theScaler.ScaleX = theScaler.ScaleY = 1.16;

                   }

         }                

         虽然使用渲染转换将帮助你改变你的用户界面,你可能会决定为了方向的改变重新安排你的设计,甚至使用不同的视图。变化的程度完全取决于你。

在Blend中设计方向更改

         杰米·罗德里格斯有一个优秀的展示,是关于他使用Blend创建的一些行为的例子,使用这些行为可以让你改变你的应用程序布局。在http://channel9.msdn.com/blogs/jaime+rodriguez/windows-phone-design-days-blend观看他的视频。

为触摸设计

         在手机上的主要输入是触摸。这就是在为手机进行设计时不同于为其他应用进行设计的主要原因。此刻在书中,你应该有一个关于这种差异的设计比喻的好主意,但对这个比喻的编程是一个不同的故事。

         如果你是来从微软开发平台来到Windows Phone开发的(例如,Silverlight,.NET等),最简单的方法来处理触摸就是使用内置的鼠标事件。事实上,如你可能期望的那样,触摸界面在模仿鼠标事件。例如,要处理在应用程序界面的轻触事情,你可以简单地处理鼠标事件:

public partial class MainPage : PhoneApplicationPage

{

// Constructor

public MainPage()

{

InitializeComponent();

                            ContentPanel.MouseLeftButtonUp +=

new MouseButtonEventHandler(ContentPanel_MouseLeftButtonUp);

}

void ContentPanel_MouseLeftButtonUp(object sender,MouseButtonEventArgs e

{

         theText.Text = "Content Panel Tapped";

}

// ...

}

         虽然内置的鼠标事件的确工作,但是他们不经常使用,因为触摸非常不同于鼠标输入。我们往往拖动、点击和捏屏幕这与我们(或可能)用鼠标有很大的不同。手机支持四点触摸,因此鼠标事件最终沦落为允许多个手指的任何输入。

         帮助处理触觉,手机上有多个层次的API来帮助你到达触摸表面。在最低层,触摸类可以报告用户做的每一个触摸交互。当你想获得尽可能接近金属层面,Silverlight的触摸类是最有用的。触摸类有一个静态FrameReported事件,触摸交互发生时被调用。通过它你可以了解到有多少触摸点被使用(例如,有几根手指在触控屏上)以及触摸点在什么位置。事件包含一个参数,使得你可以获取触摸点的信息。下面是一个示例使用FrameReported事件展示touch是如何在手机表面上被拖拽:

         public partial class MainPage : PhoneApplicationPage

{

  // Constructor

public MainPage()

{

     InitializeComponent();

                  Touch.FrameReported += new

                           TouchFrameEventHandler(Touch_FrameReported);

}

void Touch_FrameReported(object sender, TouchFrameEventArgs e

{

     var mainTouchPoint = e.GetPrimaryTouchPoint(this);

     if (mainTouchPoint.Action == TouchAction.Move

     {

              theText.Text = string.Concat("Moving: ",mainTouchPoint.Position);

     }

}

  // ...

}

         TouchFrameEventArgs类有几块功能。两个最重要的是GetPrimaryTouchPoint和GetTouchPoints方法。GetPrimaryTouchPoint的方法用于获取接触点对象,相对于一个设计时的特定UIElement。主要的触点是第一个触摸屏幕的事物决定的。相对意味着所有的触摸点都是相当UIElement的。接触点类可以告诉你位置和正在发生的行动,如下所示的代码示例:

         void Touch_FrameReported(object sender, TouchFrameEventArgs e)

         {

                   // Get the main touch point (relative to a UIElement)

                   TouchPoint mainTouchPoint = e.GetPrimaryTouchPoint(ContentPanel);

                   // Get the position

                   Point position = mainTouchPoint.Position;

                   // Get the Action of the Touch

                   switch (mainTouchPoint.Action

                   {

case TouchAction.Move

     theText.Text = "Moving";

     break;

case TouchAction.Up

     theText.Text = "Touch Ended";

     break;

case TouchAction.Down

     theText.Text = "Touch Started";

     break;

                   }

         }

         GetTouchPoints方法获取触摸信息,这些信息是相对于一个UIElement的,但在这种情况下当前所有触摸点作为一个TouchPoint对象集合被返回:

         void Touch_FrameReported(object sender, TouchFrameEventArgs e)

         {

                   // Get all the touch points

                   TouchPointCollection points = e.GetTouchPoints(ContentPanel);

                   theText.Text = string.Concat("#/Touch Points: ", points.Count);

         }

         每一个集合中的TouchPoint对象表示手机上的一个触摸点。所有的手机支持至少4个点的触摸。你与来自集合中的单独触摸点的工作方式,与你从GetPrimaryTouchPoint方法中获取TouchPoint的方式完全相同。

         尽管Touch.FrameReported事件会给你很多的控制权,但你可能想要更高层次的,这样你就可以处理简单的运动或缩放行为。为了满足这种需求,Silverlight也支持操作。操作背后的概念是能够很容易使用普通操作与屏幕上的对象交互。这些包括对象的缩放和移动。这个UIElement类通过支持三个事件来直接支持这些动作,如表6.1中描述的。

表6.1操作事件

事件

描述

ManipulationStarted

发生在一个操作(或按压或拖拽)开始于UIElement

ManipulationCompleted

发生在一个操作(或按压或拖拽)完成于UIElement

ManipulationDelta

触发一个操作(或按压或拖拽)发生在UIElement上

         这些事件被用于支持对象在屏幕上操作,不仅仅是触摸。操作的基本思路基本想法是试图通过拖动或调整大小来改变手机上的对象时给以相关通知。例如,ManipulationDelta事件发送有关操作的信息,当操作发生时以ManipulationDeltaEventArgs对象参数的形式发送。

这个参数即包含了操作的累计计量和上一个增量与当前位置的差异。操作的数据被定义在一个叫做ManipulationDelta的类中。ManipulationDelta类包含两条信息:转换和规模。这些信息直接与Silverlight的转换是如何工作的方法关联(例如,特别是TranslateTransform和ScaleTransform)。转换的数量表示一个项被拖拽(或移动)了多远。规模表明有多少项被捏手势改变了大小。例如,使用操作来移动一个对象你可以添加一个TranslateTransform到你的设计中:

         ...

         <Ellipse Fill="Red"

                            Width="200"

                            Height="200"

                            x:Name="theCircle">

                   <Ellipse.RenderTransform>

                            <TranslateTransform x:Name="theTransform" />

                   </Ellipse.RenderTransform>

         </Ellipse>

         ...

         转换就位后,你可以处理ManipulationDelta事件并使用TranslateTransform来移动元素响应拖拽:

         public partial class MainPage : PhoneApplicationPage

         {

                   // Constructor

                   public MainPage()

                   {

                            InitializeComponent();

                            ManipulationDelta += new EventHandler<ManipulationDeltaEventArgs>(

                                     MainPage_ManipulationDelta);

                   }

                   void MainPage_ManipulationDelta(object sender,

                                                                                             ManipulationDeltaEventArgs e)

                   {

                            // Move (e.g. Translate) the ellipse based on delta

                            ManipulationDelta m = e.CumulativeManipulation;

                            theTransform.X = m.Translation.X;

                            theTransform.Y= m.Translation.Y;

                   }

                   ...

         }

         在这个特殊的示例中,CumulativeManipulation用来获得整个触摸操作。 操作的转换属性包含了转换的数量,但我们不能确定这种操作是关于一个在页面上的特定元素(因为我们注册整个页面的ManipulationDelta事件)。我们可以仅仅为我们的椭圆注册操作,但是作为替换我们还可以测试看看什么容器被操作,通过测试ManipulationContainer就像这样:

         void MainPage_ManipulationDelta(object sender,

                                                                                             ManipulationDeltaEventArgs e)

         {

                   if (e.ManipulationContainer == theCircle

                   {

                            // Move (e.g. Translate) the ellipse based on delta

                            ManipulationDelta m = e.CumulativeManipulation;

                            theTransform.X = m.Translation.X;

                            theTransform.Y = m.Translation.Y;

                   }

         }

         使用捏和缩放触摸手势也采用同样的工作方式,你可以使用一个ScaleTransform来改变大小替代TranslateTransform:

         <Ellipse Fill="Red"

                            Width="200"

                            Height="200"

                            x:Name="theCircle">

                   <Ellipse.RenderTransform>

                            <ScaleTransform x:Name="theTransform"

                                                                 CenterX="100"

                                                                 CenterY="100"/>

                   </Ellipse.RenderTransform>

         </Ellipse>

         然后,在操作事件里,你可以简单地使用ManipulationDelta的scale属性替换Translate,就像这样:

         void MainPage_ManipulationDelta(object sender,

         ManipulationDeltaEventArgs e)

         {

                   if (e.ManipulationContainer == theCircle)

                   {

                            // Size (e.g. Scale) the ellipse based on delta

                            ManipulationDelta m = e.CumulativeManipulation;

                            theTransform.ScaleX = m.Scale.X;

                            theTransform.ScaleY = m.Scale.Y;

                   }

         }

         操作还包括触摸手势的惯性信息。惯性背后的含义是能够说明如果用户操作结束后还可能继续移动。与用户进行更有机互动时惯性信息变得更为重要。如果你看过,当轻拂过一个手机上的列表框,即使用户不再触摸手机时列表还继续向上滑动,这就是通过使用惯性完成的。

         例如,在ManipulationCompleted事件里你可以测试IsInertial来看看操作是否包含惯性的速度信息:

         void MainPage_ManipulationCompleted(object sender,

                                                                                             ManipulationCompletedEventArgs e)

         {

                   if (e.IsInertial

                   {

                            var m = e.TotalManipulation;

                            var velocity = e.FinalVelocities;

                            theTransform.X =

                                     m.Translation.X + (velocity.LinearVelocity.X / 100);

                            theTransform.Y =

                                     m.Translation.Y + (velocity.LinearVelocity.Y / 100);

                   }

         }

         一旦代码确定它是惯性,代码可以使用FinalVelocities来改变转换的结果(在这个例子中)。你还可以看到LinearVelocity是否大于某一阈值来确定它是否是一个“flick”:

         void MainPage_ManipulationCompleted(object sender,

                                                                                             ManipulationCompletedEventArgs e)

         {

                   if (e.IsInertial)

                   {

                            var velocity = e.FinalVelocities;

                            // Is it a Right Flick?

                            if (velocity.LinearVelocity.X > 100

                            {

                                     // ...

                            }

                   }

         }

         操作事件是专门处理拖拽和缩放的,但正如我们之前讨论过的,触摸手势有许多类型。虽然访问操作和访问较低级别的触摸类很有帮助,但对于大多数的触摸界面,你可能想更直接的访问这些手势的事件。

         UIElement类提供了访问最常见类型的触摸手势的方法。你可以看到UIElement类公开的触摸事件,在表6.2。

表6.2UIElement的触摸事件

事件

描述

Tap

当用户触摸UIElement然后相当迅速的移开她的手指时发生

Double-tap

当一个用户连续两次轻触一个UIElement时发生

Hold

当一个用户用她的手指触摸并继续按住一个UIElement时发生

         连接到这些事件非常简单,如连接事件:

         public partial class MainPage : PhoneApplicationPage

         {

                   // Constructor

                   public MainPage()

                   {

                            InitializeComponent();

                            var listener = theCircle.Hold +=

                                     new EventHandler<GestureEventArgs>(theCircle_Hold);

                   }

                   void theCircle_Hold(object sender, GestureEventArgs e)

                   {

                            // Do a hold

                   }

         }

         UIElement类代表任何页面上的可视元素,所以你可以连接这些事件于任何对象之上(例如,按钮,列表框,网格,椭圆,等等)。随着你处理触摸,你将在你的应用程序中使用到这些各种各样的基于触摸的APIs。当处理常见的手势时,元素的方式是最简单的,但是当你想要更多的控制触屏屏幕的原始情况时,你将不得不深入到APIs的堆栈中。

应用程序客户区域

         作为整个应用程序的基础,外壳(Shell)负责托管你的页面。这也意味着一定的职责,外壳提供了存在于Microsoft.Phone.Shell命名空间中的一组类,在那里你可以做出一些决定如何显示你的应用程序。正如在第一章中解释的,应用区域分解成一个系统托盘,逻辑的客户区,以及应用程序栏(如图6.8)。

 

图6.8应用程序客户区域

         你可以通过SystemTray类决定隐藏系统托盘(进入一种“全屏模式”),如下所示:

         void MainPage_Loaded(object sender, RoutedEventArgs e)

         {

                   SystemTray.IsVisible = false;

         }

         使用SystemTray类允许你改变这种情况以满足你的需要。但是你不一定需要在代码中进行设定,还可以通过在XAML中支持的一个附加属性:

         <phone:PhoneApplicationPage ...

                   xmlns:shell=

                            "clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"

                   shell:SystemTray.IsVisible="True">

         实际上并没有真正的“全屏模式”如Windows Phone文档建议;相反,这只是表明是否显示或不显示系统托盘。显示系统托盘缩小你的应用程序的尺寸;它不是简单的显示在你的应用程序“上面”。

         在手机上的一些操作也将显示在你的应用程序部分内容之上(但不会导航离开你的应用程序)。接到一个手机,警告,锁屏都是这些操作常见的例子。当手机在你的应用之上显示这些内容时掩盖了你的应用程序。通过处理PhoneApplicationFrame的Obscured和Unobscured事件,你可以对这些活动作出反应:

         var rootFrame = ((App)App.Current).RootFrame;

         rootFrame.Obscured += new

                   EventHandler<ObscuredEventArgs>(RootFrame_Obscured);

         rootFrame.Unobscured +=

                   new EventHandler(RootFrame_Unobscured);

         这些事件允许你,在你的应用程序被遮挡时作出响应。当你正在编写一个Silverlight手机游戏的时候,一个常见的用法是进入一个暂停界面。

应用程序栏

         正常的屏幕的另一部分是应用程序栏。这个栏是工具栏和菜单的一个组合。虽然你可以使用Blend创建一个应用程序栏(也就是XAML),对应用程序栏进行编程呈现一些小的不便之处。正如我之前解释的,应用程序栏支持ApplicationBarIconButton和ApplicationBarMenuItem对象,以允许用户与应用程序交互:

         <phone:PhoneApplicationPage.ApplicationBar>

                   <shell:ApplicationBar IsVisible="True"

                                                                 IsMenuEnabled="True">

                   <shell:ApplicationBarIconButton

                            IconUri="/icons/appbar.add.rest.png"

                            Text="add" />

                   <shell:ApplicationBarIconButton

                            IconUri="/icons/appbar.back.rest.png"

                            Text="revert" />

                   <shell:ApplicationBar.MenuItems>

                            <shell:ApplicationBarMenuItem Text="options" />

                            <shell:ApplicationBarMenuItem Text="about" />

                   </shell:ApplicationBar.MenuItems>

                   </shell:ApplicationBar>

         </phone:PhoneApplicationPage.ApplicationBar>

         在XAML视图中注册单击事件是有可行的,并且如你预期的方式工作:

         <shell:ApplicationBarIconButton

                   IconUri="/icons/appbar.add.rest.png"

                   x:Name="addIconButton"

                   Click="addIconButton_Click"

                   Text="add" />

         但应用程序栏不同于典型的按钮或类似的Silverlight控件。菜单、图标按钮对象不支持命令(像其他ButtonBase-derived控件),所以不能使用Silverlight支持的与命令进行数据绑定。即使你在ApplicationBar中名字元素(如上),一系列后台代码的结果也是空,所以事件连接会失败:

         public partial class MainPage : PhoneApplicationPage

         {

                   // Constructor

                   public MainPage()

                   {

                            InitializeComponent();

                            // Doesn't work because addIconButton is null!

                            addIconButton.Click += new EventHandler(addIconButton_Click);

                   }

         ...

         }

         这无法工作是因为一个技术原因(注1),但是比理解原因更重要的是,当你需要在代码中引用图标按钮或菜单项时,你将需要从PhoneApplicationPhone的ApplicationBar属性上按顺序获取它们。虽然这是脆弱的和痛苦的,现在只有这种方法才能工作。下面展示了如何做到这一点:

         public partial class MainPage : PhoneApplicationPage

         {

                   // Constructor

                   public MainPage()

                   {

                            InitializeComponent();

                            // Wire-up the button by ordinal

                            addIconButton =

                                     (ApplicationBarIconButton)ApplicationBar.Buttons[0];

                            // Wire-up the menu by ordinal       

                            optionMenuItem =

                                     (ApplicationBarMenuItem)ApplicationBar.MenuItems[0];

                            // Works now that the manual wire-up was added

                            addIconButton.Click += new EventHandler(addIconButton_Click);

                   }

         ...

         }

         注1:ApplicationBar是通过一个附加属性应用到PhoneApplicationPage类中。这意味着命名的元素不是namescope的组成部分,所以调用FrameworkElement类的FindName方法(生成的片段类使用的)无法找到图标按钮和菜单项。

理解闲置检测

         当用户已经停止使用手机并锁定手机时,Windows Phone操作系统会自动检测。手机将会进入锁定屏幕。这个锁定屏幕可以显示一个密码来打开锁定屏幕,或者只是指示用户滑动壁纸屏幕回到手机上。

         有时候,当你构建某些类型的应用程序,你想让你的应用程序继续运行,而不管该手机  是否有输入。有两种类型的闲置检测模式:应用程序和用户。

         这两种模式中简单的是用户闲置检测模式。在PhoneApplicationService类中有一个属性叫做UserIdleDetectionMode并且默认启用了。这意味着如果没有触摸事件被检测到,它会使手机进入锁定屏幕。你可以改变这种行为(允许你的应用程序继续运行,作为主要运行的应用程序,即使用户不与应用程序交互),通过禁用此检测模式就像这样:

         // Allow application to stay in the foreground

         // even if the user isn't interacting with the application

         PhoneApplicationService.Current.UserIdleDetectionMode =

                   IdleDetectionMode.Disabled;

         禁用用户闲置检测模式是很有用,当你正在做的事情吸引用户,而不需要输入(例如,显示一个视频,显示一个时钟,等等)。

         然而,应用程序空闲检测模式不是关于阻止锁定屏幕,而是确定启用锁定屏幕时应用程序应该做什么。默认情况下,应用程序空闲检测模式是启用的,这意味着当锁定屏幕出现时应用程序被暂停(因为它将要进入墓碑状态)。通过禁用应用程序闲置检测模式,你可以让你的应用程序在锁定屏幕下继续运行。要禁用应用程序空闲检测模式,你还是使用PhoneApplicationService类:

         // Allow your application to continue to run

         // when the lock screen is shown

         PhoneApplicationService.Current.ApplicationIdleDetectionMode =

                   IdleDetectionMode.Disabled;

倾斜效应

         在手机上很多位置上,在一个列表或其他控件上进行条目选择的时候,一个微妙而又有效的反馈机制被使用了。这被称为倾斜的效果。在图6.9中,你可以看到, 列表第二项没有与触觉相互作用;然而,在图6.10中你可以看到第二项是巧妙的倾斜给用户以反馈,她正在触摸该项目。事实上,静态图像里这很难看到,但实际上倾斜与用户接触的条目进行了交互。

 

图6.9未倾斜的

 

图6.10 倾斜的

         不幸的是这种效果是没有内建在框架内,但是它存在于Silverlight for Windows Phone工具包中。当你在你的XAML文件中声明了引入工具包的XML命名空间,你可以在任何级别上指定它来启用或禁用此行为。与引用Silverlight for Windows Phone工具包控件(见第五章,为手机而设计)一致,要求同样的命名空间。通常情况下,你将它包括在页面级别来在所有控件上支持倾斜效果,就像这样:

         <phone:PhoneApplicationPage ...

                   xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;

                                               assembly=Microsoft.Phone.Controls.Toolkit"

                   toolkit:TiltEffect.IsTiltEnabled="True">

         TiltEffect.IsTiltEnabled属性可应用于任何单独的控件,如果你想把这些效果应用到特定的控件而不是在页面或容器级别也一样。另一个附加属性是TiltEffect.SuppressTilt。这个附加属性允许你,在一个更高的级别TiltEffect.IsTiltEnabled附加属性已经启用的情况下,关闭特定控件上倾斜效果。例如:

         <Grid x:Name="ContentPanel"

                            Grid.Row="1"

                            Margin="12,0,12,0"

                            toolkit:TiltEffect.IsTiltEnabled="False">

                   <ListBox FontSize="28"

                                     Name="listBox1"

                                     toolkit:TiltEffect.SuppressTilt="True"

                                     ItemsSource="{Binding}" />

         </Grid>

         因为这些属性是附加属性,你可以在代码中对其设置:

         using Microsoft.Phone.Controls;

         public partial class MainPage : PhoneApplicationPage

         {

                   // Constructor

                   public MainPage()

                   {

                            InitializeComponent();

                            listBox1.SetValue(TiltEffect.SuppressTiltProperty,false);

                  }

         }

我们在那里?

         此时你在书中才刚刚开始了解如何为手机编写代码的基础内容,但是你应该熟悉手机应用程序的生命周期,以及简单的如何通过触摸与用户进行交互的任务和事件。这一章专注于为你展示为手机开发Silverlight程序的基本原理,包括运行在手机上的Silverlight应用程序的不同之处(相对于使用Silverlight编写的桌面或Web应用程序)。通过应用基础的墓碑机制、导航和触控交互,此刻你应该能够创建引人注目的用户界面了。

posted @ 2012-08-28 17:29  newetmscontact  阅读(256)  评论(0编辑  收藏  举报