4.2 iOS触摸屏 iOS的触摸屏用户体验是非常好的,iOS也是最早支持多点触摸的操作系统。多点触摸、手势等在iOS开发中得到了广泛应用。这一节着重介绍iOS触摸屏的相关操作。 4.2.1 iOS输入处理 触摸这种操作方式,带给我们更加人性化的体验。例如,可避免因长时间按压物理键盘而导致的手指酸痛,而且采用电容屏,可以屏蔽一些非人为的输入操作。触摸,给屏幕带来更加灵活的可操作性,摆脱了物理键盘的功能限制,用户可以使用屏幕上的每一像素,只要手指足够小并可以单击到。 4.2.2 iOS多点触摸与手势 iOS赋予用户至少3.5英寸的宽广视野,在当时可谓令人眼前一亮。在这不大不小的舞台上,手指可以灵活地跳动,而在此之前,传统的触屏手机都只能用单指,而且很多都是电阻屏。但是自从iOS 横空出世之后,一切都改变了。 1)多点触摸 iOS彻底打破了传统手机的操作模式,多点触摸使之更为人性化。多点触摸的实现代码如下: -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSUInteger numTouches = [touches count]; } 上述方法传递一个NSSet实例与一个UIEvent实例,可以通过获取touches参数中的对象来确定当前有多少根手指触摸,touches中的每个对象都是一个UITouch事件,表示一个手指正在触摸屏幕。倘若该触摸是一系列轻击的一部分,则还可以通过询问任何UITouch对象来查询相关的属性。 同鼠标操作一样,iOS也可以有单击、双击甚至更多类似的操作,有了这些,在这个有限大小的屏幕上,可以完成更多的功能。正如上文所述,通过访问它的touches属性来查询: -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSUInteger numTaps = [[touches anyObject] tapCount]; } 2)手势 手势是指从一个或多个手指接触屏幕开始,到手指离开屏幕为止所发生的所有事件。无论这个过程耗时多长,只要还有手指停留在屏幕上,就处于某个手势之中,除非发生意外情况。 有了手势之后,屏幕才可以感应到我们的手在做什么动作。很多场合,一些控件已经能够支持双指拉开放大、捏合缩小的动作,图片的多指旋转功能灵活、方便,符合我们的生活习惯,诸如此类的功能都是多指技术应用于现实的最好证明。可以通过以下方式检测手势: -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ If ([touches count]==2) { // 检测触摸点个数 NSArray *twoTouches = [touches allObjects]; // 获取触摸点数组 UITouch *first = [twoTouches objectAtIndex:0]; // 第一个触摸点 UITouch *second = [twoTouched objectAtIndex;1]; // 第二个触摸点 CGFloat initialDistance = distanceBetweenPoints( [first locationInView:self.view],[second locationInView:self.view]); // 计算两个触摸点之间的距离 } } iPhone iOS 4.x可以通过设置开启屏幕的缩放功能:三指连按两次便可切换放大,在放大的情况下还可以通过三指来移动屏幕;再次三指连按两次便可恢复正常状态。 iPad iOS 4.3已经可以支持四指操作,通过各种手势,可以实现诸多以前无法实现的效果:四指向上滑动可以显示后台运行的程序;四指向左滑动可以向左切换已打开的程序,四指向右滑动则可以向右切换已打开的程序;在程序打开的情况下,四指捏合可以关闭当前程序。 4.3 Windows Phone触摸屏 Windows Phone与Windows Mobile相比,一个明显的区别是Windows Phone支持电容屏,不需要手写笔即可操作,支持多点触摸;当然,在这之前Android与iOS早已经支持了这些功能,不过Windows Phone的多点触摸支持做得很出色,以笔者的经验来看,采用Windows Phone的HTC MOZART手机要比iPhone 3GS多点触摸更容易操作。Windows Phone的多点触摸屏可以支持4个指头同时触摸,甚至更多。当然这对程序员来说不一定是好事,因为这意味着需要更多的程序来实现。要测试多点触摸程序,Windows Phone的手机必不可少,但是如果PC显示器支持多点触摸,那么使用Windows Phone模拟器也可以。 本节主要介绍两方面的内容:一是在Silverlight中如何处理低级别触摸屏事件(XNA中低级别触屏处理的方式与Silverlight类似);二是介绍Manipulation Event及Routed Event。 4.3.1 Windows Phone输入处理 本部分先介绍在Silverlight中是如何处理低级别触摸屏事件的,并且以开发中一个常见的问题作为例程。在正式介绍之前,先来了解一下什么是element。 1)element的含义 Silverlight编程中,element有两个含义:一是XML中的标签;二是界面中显示的对象,即UIElement。UIElement是非常重要的一个基类,所有的控件,如Grid、TextBlock、Button、Image等都集成于UIElement之中。 2)Silverlight触屏 Silverlight触屏的接口分为低级别和高级别两类,高级别的通过UIElement类的三个事件处理来实现——ManipulationStarted、ManipulationDelta和ManipulationCompleted。后面我们会介绍高级别事件处理接口;低级别事件处理接口则通过Touch.FrameReported实现。 Silverlight触屏接口最重要的一个类是TouchPoint,一个TouchPoint代表一个指头触摸屏幕。TouchPoint有4个只读属性(见表4-2)。 表4-2 TouchPoint的只读属性 属性 类型 说 明 Action TouchAction 可以是Down、Move或者Up Position Point 表示触摸坐标,它是一个相对坐标,相对于UIElement左上角的位置 Size Size Windows Phone中目前这个还没有实际作用 TouchDevice TouchDevice TouchDevice对象有两个只读属性: Id、int类型用来区分是哪个指头 DirectlyOver、UIElement类型表示最上层的UI Element是哪一个 要想使用低级别触屏接口,需要在程序中注册如下事件处理函数: Touch.FrameReported += OnTouchFrameReported; 一般可以把这行代码放到页面的构造函数中。 OnTouchFrameReported函数原型如下: void OnTouchFrameReported(object sender, TouchFrameEventArgs args) { } 在这个处理函数中,可以通过TouchFrameEventArgs的以下三个方法获得所需的信息: (1)GetTouchPoints,获得所有的触摸坐标,返回的是TouchPointCollection,多点触摸可以通过它来判断。 (2)GetPrimaryTouchPoint,只返回一个TouchPoint。 (3)SuspendMousePromotionUntilToucnUp,关于这个成员函数的具体含义可查阅MSDN。 3)Siliverlight应用程序 现在来创建一个Siliverlight应用程序DevDivTouchMultiPage,步骤如下。 1 打开MainPage.xaml.cs文件,在MainPage的构造函数中添加如下代码: Touch.FrameReported += OnTouchFrameReported; 2 打开MainPage.xaml文件,添加一个TextBlock控件。 3 找到如下代码: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"></Grid> 将其修改为: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock Height="76" HorizontalAlignment="Left" Margin="62,147,0,0" Name="textBlock1" Text="TextBlock" VerticalAlignment="Top" Width="242" /> </Grid> 4 为MainPage类添加一个成员函数: void OnTouchFrameReported(object sender, TouchFrameEventArgs args) { TouchPoint primaryTouchPoint = args.GetPrimaryTouchPoint(null); if (primaryTouchPoint != null && primaryTouchPoint.Action == TouchAction.Down) { if (primaryTouchPoint.TouchDevice.DirectlyOver == textBlock1) { Debug.WriteLine("button1 is pressed!"); } } } 5 在Visual Studio中按F5键调试,运行效果如图4-3所示。 图4-3 6 选中TextBlock文字,再查看Visual Studio的Output窗口,如图4-4所示。 图4-4 可以看到Output窗口显示“button1 is pressed!”。 4)使用低级别触屏接口容易遇到的问题 (1)多页面相关支持。前面的章节中已经简单介绍过,一个Silverlight程序显示界面的根节点是一个Frame,叫做RootFrame,在其上面可以显示页面。一个应用程序可以包含多个页面,默认加载的是“MainPage”页面,页面之间可以跳转。 为刚创建的“DevDivTouchMultiPage”添加一个“Windows Phone Portrait Page”,方法是右击工程,选择Add→New Item→Windows Phone Portait Page,这样,工程中就增加了Page1.xaml和Page1.xaml.cs。 修改MainPage.xaml,增加一个Button,并且为这个Button增加一个Click处理函数,当单击这个Button的时候,跳转到Page1页面中去。 将MainPage.xaml中的如下代码: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock Height="76" HorizontalAlignment="Left" Margin="62,147,0,0" Name="textBlock1" Text="TextBlock" VerticalAlignment="Top" Width="242" /> </Grid> 修改为: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock Height="76" HorizontalAlignment="Left" Margin="62,147,0,0" Name="textBlock1" Text="TextBlock" VerticalAlignment="Top" Width="242" /> <Button Content="Button" Height="72" HorizontalAlignment="Left" Margin="20,323,0,0" Name="button1" VerticalAlignment="Top" Width="430" Click="button1_Click" /> </Grid> 修改MainPage.xaml.cs,为MainPage类增加一个成员函数: private void button1_Click(object sender, RoutedEventArgs e) { this.NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative)); } 代码很简单,通过NavigationService进行页面跳转。 执行程序,单击【Button】按钮之前和之后的界面对比如图4-5所示。 图4-5 可以看到单击【Button】按钮以后,页面跳转成功了。 (2)触屏处理事件的响应。不要退出程序,进入Visual Studio,打开“MainPage.xaml.cs”,在“OnTouchFrameReported”中设置一个断点,然后点手机或者模拟器Page1页面上的任意位置,这时候发生了意想不到的事情:我们明明点的是Page1的屏幕,执行的却是MainPage的“OnTouchFrameReported”,如图4-6所示。 图4-6 原来,我们调用的“Touch.FrameReported”注册触屏处理事件是针对应用程序的,而不是针对特定页面的,所以点Page1时会在MainPage中接收到触屏事件。关于这个问题的解决办法,后文会详细介绍。 【案例】 下面来做另一个有趣的小实验,实验之前先补充两点知识:首先,页面创建以后,程序退出之前是不销毁的,即从MainPage切换到Page1,MainPage不销毁;其次,可以在页面中加载事件,添加事件处理函数。步骤如下。 1 为MainPage添加页面处理函数,修改MainPage.xaml文件,将下列代码: <phone:PhoneApplicationPage x:Class="DevDivTouchMultiPage.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" shell:SystemTray.IsVisible="True"> 修改为: <phone:PhoneApplicationPage x:Class="DevDivTouchMultiPage.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" shell:SystemTray.IsVisible="True" Loaded="PhoneApplicationPage_Loaded"> 2 修改MainPage.xaml.cs文件,为MainPage类增加成员函数: private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e) { Touch.FrameReported += OnTouchFrameReported; } 同时把MainPage构造函数中的“Touch.FrameReported += OnTouchFrameReported;”去掉。 3 调试执行程序,在Visual Studio中按F5键开始执行,然后单击MainPage中的【Button】按钮切换到Page1,再按返回键(左箭头键),返回MainPage,在“OnTouchFrameReproted”上添加断点,如图4-7所示。 图4-7 4 用手触摸“MainPage”上任意一点,奇怪的事情发生了:每按一下屏幕,断点会执行两次,好像是按了两次屏幕一样。 每当页面加载的时候,都会调用“Loaded”事件处理函数,也就是说,刚启动应用程序主页面加载时,调用一次OnTouchFrameReported,当从Page1切换回MainPage时,主页面又加载一次,则又调用一次OnTouchFrameReported,所以触摸屏幕的时候就会收到多个消息;如果在MainPage和Page1之间不断切换,就会发现,每切换一次都会多收到一次触屏事件。但是,如果把“Touch.FrameReported += OnTouchFrameReported;”加到MainPage的构造函数中就不会发生这样的问题了,因为主页面只构造一次,所以构造函数只会执行一次,FrameReported事件处理函数也只注册了一次。 (3)问题解决。回过头来我们去解决一开始遇到的问题,即在Page1中触摸屏幕却在MainPage中收到消息。我们知道,“Touch.FrameReported += OnTouchFrameReported”是注册事件处理函数,如果在显示Page1的时候不想处理触屏事件,自然想取消注册,这其实也很简单,只需要调用: Touch.FrameReported -= OnTouchFrameReported; 于是我们修改buton1_Click,修改后代码为: private void button1_Click(object sender, RoutedEventArgs e) { Touch.FrameReported -= OnTouchFrameReported; this.NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative)); } 问题就解决了。 5)Silverlight里面高级别触屏处理涉及的3个事件 Silverlight里面高级别触屏处理涉及的三个事件分别为ManipulationStarted、ManipulationDelta和ManipulationCompleted。 Touch.FrameReported处理的是整个应用程序的触屏事件,而Manipulation事件是针对特定的UIElement的。其应用示例步骤如下。 1 创建一个Silverlight工程DevDivHelloManipulation。 2 打开MainPage.xaml文件,在其中加一个Button,然后为这个Button加上Manipulation处理函数,代码如下: <Button Content="Button" Height="72" HorizontalAlignment="Left" Margin="111,154,0,0" Name="button1" VerticalAlignment="Top" Width="160" ManipulationStarted="button1_ManipulationStarted"/> 3 打开MainPage.xaml.cs文件,为MainPage类加上成员函数: private void button1_ManipulationStarted(object sender, ManipulationStartedEventArgs e) { Debug.WriteLine("button1_ManipulationStarted called!"); } 4 调试执行程序。单击Button发现有Log输出,如果点屏幕其他地方则不会输出Log。 Manipulation Event是针对特定UIElement的,而不是针对整个Application的。 6)Silverlight中routed event事件处理 一般来说,Manipulation事件由最上层的控件来处理,但是当它不能处理这个事件或者不愿意处理的时候,可以交给它的父控件来处理,就这样一直向上递交,直到Visual tree的顶点。 界面上的所有UIElement都会因父子关系而形成一个树状结构,一个页面的根节点就是PhoneApplicationPage,而每一个Sliverlight应用程序都有一个Root,它是PhoneApplicationFrame,我们称之为Root Frame,所以页面中如果所有的控件都不处理Manipulation事件,则可以交给PhoneApplicationPage来处理,只需要PhoneApplicationPage的子类重载OnManipulationStarted即可。 ManipulationStarted事件的“event”参数是ManipulationStartedEventArgs,它继承于RoutedEventArgs。而RoutedEventArgs定义了OriginalSource属性,通过它可以知道事件是从哪个Element来的。 4.3.2 Windows Phone多点触摸与手势 Windows Phone中无论是低级别的处理函数,还是ManipulationEvent,都是支持多点触摸的。多点触摸的检测和上一节的例子差别不大,只是处理参数稍有不同。手势就是把触摸路径组合起来,在此不再举例说明。 源代码下载地址: http://www.devdiv.com/thread-85477-1-1.html