iOS-Events[4]-Motion Events

本文对Motion Events的机制进行讲解。

iOS系统的Events类型分为以下几种:Multitouch events, Accelerometer events, Remote control events。

Events

Motion Events

Motion Events包括设备的定位(Location)、朝向(Orientation)和移动(Movement)。通过处理这些Events,可以对App进行更精妙的处理。加速器(Accelerometer)和陀螺仪(gyroscope)数据可以用于检测设备的倾斜(tilting)、旋转(rotation)和摇晃(shaking)。

加速器(Accelerometer)实际上是由三个不同维度的加速度构成的,分别对应x, y, z轴,每个加速器测量该维度随着时间沿着线性的速度变化。把这三个组合在一起,就可以取到设备的朝向以及任意方向上的移动。陀螺仪(gyroscope)是测量这三个方向上的旋转速度。

Getting the Current Device Orientation with UIDevice

如果只需要获取设备的朝向,而并不需要获取到精准的向量信息,使用UIDevice类即可。使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void) viewDidLoad {
// Request to turn on accelerometer and begin receiving accelerometer events
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
}

- (void)orientationChanged:(NSNotification *)notification {
NSLog(@"%@", @([UIDevice currentDevice].orientation));
}

-(void) viewDidDisappear {
// Request to stop receiving accelerometer events and turn off accelerometer
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
}

这里,为了防止硬件处于省电关闭状态,需要先调用一下beginGeneratingDeviceOrientationNotifications方法,在不需要时,调用endGeneratingDeviceOrientationNotifications方法。

Detecting Shake-Motion Events with UIEvent

当用户摇晃设备时,iOS系统计算加速器的加速度,一旦达到阈值,则生成一个UIEvent,并将其传递给App处理。Motion Events只会在开始和结束时候,传递给App,不像Touch Events是连续的。Motion Events只包含了一种事件类型UIEventTypeMotion,和子事件类型UIEventSubtypeMotionShake,以及时间戳Timestamp

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if(motion == UIEventSubtypeMotionShake)
NSLog(@"A shake began..");
}


- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if(motion == UIEventSubtypeMotionShake)
NSLog(@"A shake ended..");
}

- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if(motion == UIEventSubtypeMotionShake)
NSLog(@"A shake cancelled..");
}

Setting and Checking Required Hardware Capabilities for Motion Events

如果App是严重依赖于设备的加速器或者陀螺仪的数据,而对于那些不支持的设备,App可能并不能起到作用,则App应该在info.plist中加入以下字段:

1
2
3
4
5
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>accelerometer</string>
<string>gyroscope</string>
</array>

iOS系统会根据该字段判断是否在必要时运行App,且App Store也会通过该字段确保用户的设备是有上述硬件的。

如果App并不是严重依赖该硬件,可以在运行时判断硬件是否支持,下面章节会介绍。

Capturing Device Movement with Core Motion

如果需要精确的加速器和陀螺仪数据,可以使用CoreMotion.frameworkCoreMotion与UIKit不同,它与UIEvent无关,也不会使用响应链,而是简单的分发Core Motion Events.

Core Motion Events包含三种数据,分别是:

  • CMAccelerometerData:包含每个轴方向的移动加速度;
  • CMGyroData:包含三个轴方向的旋转加速度;
  • CMDeviceMotion:包含一些不同的测量数据,例如海拔以及其他一些由旋转加速度和移动加速度构成的数据。

以上的数据类,都继承自CMLogItem,该类包含了时间戳,可以用于比较Event的前后顺序。

这里用到的类是CMMotionManager,每个App都必须只能创造一个该类的实例,否则会导致数据的接收收到影响。例如,检查硬件是否支持:

1
2
3
4
5
_motionManager = [CMMotionManager new];
NSLog(@"%d", _motionManager.gyroAvailable);
NSLog(@"%d", _motionManager.accelerometerAvailable);
NSLog(@"%d", _motionManager.magnetometerAvailable);
NSLog(@"%d", _motionManager.deviceMotionAvailable);

CMMotionManager有两种方式可以获取到数据,分别是:

  • Pull:调用CMMotionManager的startUpdate方法,并且通过计时器,去获取CMMotionManager的属性值,来主动获取数据;
  • Push:设置CMMotionManager的updateInterval属性,来设置间隔时间,并调用CMMotionManager的startUpdate的block方法,来被动获取数据。

显然,Push方法更好一些,下面是例子:

1
2
3
4
5
6
7
8
_motionManager = [CMMotionManager new];
if(_motionManager.gyroAvailable){
_motionManager.gyroUpdateInterval = 3.0;
[_motionManager startGyroUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMGyroData * _Nullable gyroData, NSError * _Nullable error) {
if(!error)
NSLog(@"x:%lf y:%lf z:%lf", gyroData.rotationRate.x, gyroData.rotationRate.y, gyroData.rotationRate.z);
}];
}

注意,在不需要数据时,要调用stopUpdate,这样可以关闭传感器,以节省设备电量。

(1) 间隔时间设置

下面是获取数据的推荐频率:

  • 10-20(Hz):用于判断设备朝向;
  • 30-60(Hz):游戏或者其他App用于获取实时的用户输入;
  • 70-100(Hz):需要高频Motion的App,例如检测用户点击屏幕或者快速摇晃。

(2) 三维轴朝向

加速器的三维轴朝向:

Acceleration_Axes

陀螺仪的三维轴朝向:

Gyroscope_Axes

(3) Motion Data

CMDeviceMotion同时包含了加速器和陀螺仪的数据,另外,对重力加速度(Gravity)与用户的加速度做了区分。

CMDeviceMotionattitude属性,包含了以下属性:

  • 等式(Quaternion)
  • 旋转矩阵(Rotation Matrix)
  • 欧拉角(Euler Angles):x轴转过角度(Pitch)、y轴转过角度(Roll)、z轴转过角度(Yaw)

如果没有设置,则CMDeviceMotion的参考轴面是重力轴面,即:

Gravity

CMDeviceMotion通过这个参考轴面来与当前轴面进行比较,从而得出设备的旋转信息,如果需要,可以修改参考轴面,例如,下面是以Pitch开始的轴面为参考轴面,而不是以重力轴面:

1
2
3
4
5
6
7
8
9
10
11
12
-(void) startPitch {
// referenceAttitude is a property
self.referenceAttitude = self.motionManager.deviceMotion.attitude;
}

- (void)drawView {
CMAttitude *currentAttitude = self.motionManager.deviceMotion.attitude;
[currentAttitude multiplyByInverseOfAttitude: self.referenceAttitude];
// Render bat using currentAttitude
[self updateModelsWithAttitude:currentAttitude];
[renderer render];
}

Applications

利用Motion Events可以做很多有意思的事情,例如:

保持图片永远水平:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"me.jpg"]];
iv.frame = self.view.bounds;
[self.view addSubview:iv];

_motionManager = [CMMotionManager new];
if(_motionManager.accelerometerAvailable){
_motionManager.accelerometerUpdateInterval = 0.1f;
__weak UIImageView *weakIV = iv;
[_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
double angle = atan2(accelerometerData.acceleration.x, accelerometerData.acceleration.y) - M_PI;
if(fabs(angle - lastAngle) > 0.05f)
weakIV.transform = CGAffineTransformMakeRotation(angle);
lastAngle = angle;
}];
}

倾斜可以自动滑动列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)viewDidLoad {
[super viewDidLoad];
_tv = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
[_tv registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cellID"];
_tv.dataSource = self;
[self.view addSubview:_tv];

_row = 10;
_motionManager = [CMMotionManager new];
if(_motionManager.accelerometerAvailable){
_motionManager.accelerometerUpdateInterval = 0.1f;
[_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
if(accelerometerData.acceleration.z < -0.5f)
_row = MIN(_row + 1, 99);
else if(accelerometerData.acceleration.z > 0.5f)
_row = MAX(0, _row - 1);
[_tv scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:_row inSection:0] atScrollPosition:UITableViewScrollPositionNone animated:YES];
}];
}
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 100;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellID" forIndexPath:indexPath];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"123"];
}
cell.textLabel.text = [NSString stringWithFormat:@"%d", (int)indexPath.row + 1];
return cell;
}