内置加速计是iPhone和iPod Touch中最酷的特性之一,iPhone可以通过这个小设备知道用户握持手机的方式,以及用户是否移动了手机。iPhoneOS使用加速计处理自动旋转,并且许多游戏都使用他做为控制机制。它还可以用于检测摇动和其他突发的运动。
通过感知特定方向的惯性力总量,加速计可以测量出加速度和重力。iPhone内的加速计是一个三轴加速计,这意味着他能够检测到三维空间中的运动或重力引力。因此,加速计不但可以指示握持电话的方式(如自动旋转功能),而且如果电话放在桌子上的话,还可以指示电话的正面朝下还是朝上。
加速计可以测量g引力,因此加速计返回值为1.0时,表示在特定方向上感知到1g。如果是静止握持iPhone而没有任何运动,那么地球引力对其施加的力大约为1g。如果是纵向竖直地握持iPhone,那么iPhone会检测并报告其y轴上施加的力大约为1g。如果是以一定角度握持iPhone,那么1g的力会分布到不同的轴上,这取决于握持iPhone的方式。在以45度角握持时,1g的力会均匀地分解到两个轴上。
如果检测到的加速计值远大于1g,那么即可以判断这是突然运动。正常使用时,加速计在任一轴上都不会检测到远大于1g的值。如果摇动、坠落或投掷iPhone,那么加速计便会在一个或多个轴上检测到很大的力。
iPhone加速计使用的三轴结构是:iPhone长边的左右是X轴(右为正),短边的上下是Y轴(上为正),垂直于iPhone的是Z轴(正面为正)。需要注意的是,加速计对y坐标轴使用了更标准的惯例,即y轴伸长表示向上的力,这与Quartz 2D的坐标系相反。如果加速计使用Quartz 2D做为控制机制,那么必须要转换y坐标轴。使用OpenGL ES时则不需要转换。
UIAccelerometer类是单独存在的。要获取对此类的引用,请调用sharedAccelerometer方法:
UIAccelerometer *accelerometer=[UIAccelerometer sharedAccelerometer];
从加速计获取信息与从Core Location获取信息相似。创建一个符合UIAccelerometerDelegate协议的类,执行可以获取加速计信息的方法。
在分配委托时,需要以秒指定更新间隔。iPhone的加速计支持最高以每秒100次的频率进行轮询,但无法保证真正达到这么多次更新,或者可以精确均匀分隔这些更新。要分配委托或指定轮询间隔为每秒60次,可以如下所示:
accelerometer.delegate=self;
accelerometer.updateInterval=1.0f/60.0f;
完成之后,剩余的事情是实现加速计用于更新委托的方法,accelerometer:didAccelerate:。第二个变量包含了来自加速计的真实数据,嵌入在类UIAcceleration的一个对象中。
如前所述,iPhone加速计可以检测3个轴上的加速度,并且对使用UIAcceleration类实例的委托提供此信息。每个UIAcceleration实例都有x、y和z的属性,分别有一个带符号的浮点值。值0表示加速计在此轴上没有检测到任何运动。正值或负值表示一个方向上的力。例如,y的负值表示感受到了向下的力,这可能表示电话是纵向竖直握持的。y的正值表示在向上的方向施加了某些力,这可能意味着电话是倒置的或者电话正在向下运动。(这里有点疑问?说反了吧?)
请在头脑中牢记图15-1的示意图,并查看加速计结果。注意,在现实生活中,几乎不可能获得如此精确的值,因为加速计非常敏感,可以感知非常微笑的运动,而我们通常只能感知立体空间3个轴上的某一个微小受力。这是现实世界中的物理,而不是高中物理实验室。
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
NSString *newText=[[NSString alloc] initWithFormat:@"Max: x: %g\ty:%g\tz:
%g",acceleration.x,acceleration.y,acceleration.z];
label.text=newText;
[newText release];
}
此方法可以在每次调用时更改界面上的标签,调用此方法的频率取决于以前指定的updateInterval的值。
1,检测摇动
加速计通常用于检测摇动。与手势相似,摇动可以作为应用程序输入的一种形式。例如,对于绘图程序GLPaint,iPhone的示例代码之一,用户可以通过摇动iPhone擦除绘图,就像Etch-a-Sketch一样。摇动检测功能相对来说是微不足道的,主需要检查某个轴上的绝对值是否大于阈值(yu)。在正常使用期间,3个轴之一所注册的值通常在1.3g左右,若要远高于此值,则需要刻意施加力量。加速计好像不能注册高于2.3g左右的值。因此,请勿将阈值设置得高于此值。
要检测摇动,请检查绝对值是否大于1.5(表示轻微摇动)和2.0(表示剧烈摇动),代码如下:
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
if (fbasf(acceleration.x)>2.0 || fbasf(acceleration.y)>2.0 || fbasf(acceleration.z)>2.0 ) {
//do something here...
}
}
上述方法可以检测到任一轴上任何超过2g力的运动。通过要求用户前后摇动一定次数才能注册为一次摇动,我们可以执行更复杂的摇动检测,代码如下:
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
static NSInteger shakeCount=0;
static NSDate *shakeStart;
NSDate *now=[[NSDate alloc] init];
NSDate *checkDate=[[NSDate alloc] initWithTimeInterval:1.5f sinceDate:shakeStart];
if ([now compare:checkDate]==NSOrderedDescending || shakeStart ==nil) {
shakeCount=0;
[shakeStart release];
shakeStart=[[NSDate alloc] init];
}
[now release];
[checkDate release];
if (fbasf(acceleration.x)>2.0 || fbasf(acceleration.y)>2.0 || fbasf(acceleration.z)>2.0 ) {
shakeCount++;
if (shakeCount>4) {
//do something
shakeCount=0;
[shakeStart release];
shakeStart=[[NSDate alloc] init];
}
}
}
此方法可以查明加速计报告的值大于2的次数,如果在1.5s的时间内发送了4次,则注册为一次摇动。
2.加速计用作方向控制器
或许加速计在第三方应用程序中最常见的用法是作为游戏控制器。在游戏中不是使用按钮控制字符或对象的移动,而是使用加速计。例如,在赛车游戏中,像转动方向盘那样转到iPhone也许就可以驾驶汽车,而向前倾斜表示加速,向后倾斜表示刹车。
具体如何将加速计用作控制器,这很大程度上取决于游戏的特定机制。在很简单的情况下,可能只需要获取一个轴的值,乘以某个数,然后添加到所控制对象的坐标系中。
使用加速计作为控制器的一个棘手问题是,委托方法并不能保证以指定的间隔回调。如果告诉加速计每秒钟更新60次委托类,所能确定的仅仅是他每秒钟更新的次数绝对不会多于60次。因此不能保证每秒钟得到60次均匀分隔的更新,所以如果所做的动画是基于来自加速计的输入,那么必须要弄清楚委托方法调用之间的时间。
说明 本章中的应用程序在仿真器上不起作用,因为仿真器中没有加速计。
摇动与击碎
我们要编写一个应用程序,他在检测到摇动之后会使电话看起来和听起来好像他因为摇动而破碎一样。启动此应用程序后,程序会显示一张图片,他看起来像是iPhone的首页。
摇动电话时,发出破碎的声音,并更换图片。
然后只需触摸屏幕,就可以重置其初始状态。
创建一个基于视图的项目 ShakeAndBreak。在 15 ShakeAndBreak文件夹中找到 两张图像和一个声音文件,拖到Resources文件夹中。还有一个icon.png,也添加到Resources文件夹。
展开Resources文件夹中的info.plist,在属性列表中添加一个条目,告诉应用程序不要使用状态栏。单击Information Property List行,单击出现在本行末端的按钮,添加一个新的子值,将新行的Key改为UIStatusBarHidden,然后在右键单击刚才添加的行的空Value列。此时应该出现一个上下文菜单,选择ValueType为Boolean。此行应该变为一个复选框,单击选中此复选框。最后,修改Icon File为icon.png。
然后,展开Classes文件,单击ShakeAndBreakViewControl
#define kAccelerationThreshold 2.2
#define kUpdateInterval (1.0f/10.0f)
#import <AudioToolbox/AudioToolbox.h>
@interface ShakeAndBreakViewControl
IBOutlet UIImageView *imageView;
BOOL brokenScreenShowing;
SystemSoundId soundID;
UIImage *fixed;
UIIMage *broken;
}
@property ..
@end
我们定义更新频率为1s 10次,这已经足够检测到一次摇动。通常来说,轮询时会使用能满足要求的最低频率。在将加速计用作控制器时,需要以相当快的速率轮询,通常达到每秒30次到60次更新。
打开ShakeAndBreakViewControl
单击ShakeAndBreakViewControl
- (void)viewDidLoad {
UIAccelerometer *accel=[UIAccelerometer sharedAccelerometer];
accel.delegate=self;
accel.updateInterval=kUpdateInterval;
NSString *path=[[NSBundle mainBundle] pathForResource:@"glass" ofType:@"wav"];
AudioServicesCreateSyste
self.fixed=[UIImage imageNamed:@"home.png"];
self.broken=[UIImage imageNamed:@"homebroken.png"];
imageView.image=fixed;
brokenScreenShowing=NO;
}
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
if (! brokenScreenShowing) {
if (acceleration.x >kAccelerationThreshold || acceleration.y >kAccelerationThreshold || acceleration.z
>kAccelerationThreshold) {
imageView.image=broken;
AudioServicePlaySystemSo
brokenScreenShowing=YES;
}
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
imageView.image=fixed;
brokenScreenShowing=NO;
}
@end
滚弹珠程序
我们的下一个小游戏,是通过倾斜电话在iPhone的屏幕上移动弹珠。此处,我们使用Quartz 2D来处理动画。在处理游戏或其他需要平滑动画的程序时,通常的规则是使用OpenGL。这里使用Quartz 2D是因为他比较简单。
使用基于视图的模版创建一个新项目 Ball。在15 Ball文件夹中,找到ball.png,将其添加到Resources文件夹中。
然后单击Classes文件夹,Cmd+N新建UIView subClass,命名为BallView.m。
双击BallViewController.xib,单击View图标,将视图的类改为BallView,将视图的背景颜色改为黑色,然后重新设置File's Owner的view输出口为BallView。
单击BallViewController.h:
#define kUpdateInterval (1.0f/60.0f)
@interface BallViewController:UIViewController <UIAccelerometerDelegate> {
}
@end
转到BallViewController.m:
- (void)viewDidLoad {
UIAccelerometer *accel=[UIAccelerometer sharedAccelerometer];
accel.delegate=self;
accel.updateInterval=kUpdateInterval;
[super viewDidLoad];
}
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
[(BallView *)self.view setAcceleration:acceleration];
[(BallView *)self.view draw];
}
单击BallView.h:
#define kVelocityMultiplier 500
@interface BallView:UIView {
UIImage *image;
CGPoint currentPoint;
CGPoint previousPoint;
UIAcceleration *acceleration;
CGFloat ballXVelocity;
CGFloat ballYVelocity;
}
- (void)draw;
@end
切换到BallView.m:
- (id)initWithCoder:(NSCoder *)coder {
if (self=[super initWithCoder:coder]) {
self.image=[UIIMage imageNamed:@"ball.png"];
self.currentPoint=CGPointMake((self.bounds.size.width/2.0f)+(image.size.width / 2.0f),(self.bounds.size.height/2.0f)+(image.size.height / 2.0f));
ballXVelocity=0.0f;
ballYVelocity=0.0f;
}
return self;
}
- (void)drawRect:(CGRect) rect {
[image drawAtPoint:currentPoint];
}
- (CGPoint)currentPoint {
return currentPoint;
}
- (void)setCurrentPoint:(CGPoint)newPoint {
previousPoint=currentPoint;
currentPoint=newPoint;
if (currentPoint.x<0) {
currentPoint.x=0;
ballXVelocity=0;
}
if (currentPoint.y<0) {
currentPoint.y=0;
ballYVelocity=0;
}
if (currentPoint.x > self.bounds.size.width -image.size.width ) {
currentPoint.x=self.bounds.size.width -image.size.width;
ballXVelocity=0;
}
if (currentPoint.y > self.bounds.size.height -image.size.height ) {
currentPoint.y=self.bounds.size.height -image.size.height;
ballYVelocity=0;
}
CGRect currentImageRect=CGRectMake(currentPoint.x,currentPoint.y,currentPoint.x+image.size.width,currentPoint.y+image.size.height);
CGRect previousImageRect=CGRectMake(previousPoint.x,previousPoint.y,previousPoint.x+image.size.width,previousPoint.y+image.size.height);
[self setNeedsDisplayInRect:CGRectUnion(currentImageRect,previousImageRect)];
}
- (void)draw {
static NSDate *lastDrawTime;
if (lastDrawTime !=nil) {
NSTimeInterval secondsSinceLastDraw=-([lastDrawTime timeIntervalSinceNow]);
ballYVelocity=ballYVelocity+-(acceleration.y *secondsSinceLastDraw);
ballXVelocity=ballXVelocity+(acceleration.x *secondsSinceLastDraw);
CGFloat xAcceleration=secondsSinceLastDraw *ballXVelocity *500;
CGFloat yAcceleration=secondsSinceLastDraw *ballYVelocity *500;
self.currentPoint = CGPointMake(self.currentPoint.x+xAcceleration,self.currentPoint.y+yAcceleration);
}
//Update last time with current time
[lastDrawTime release];
lastDrawTime=[[NSDate alloc] init];
}
@end