原文链接:http://nshipster.com/cmdevicemotion/
前言
陀螺仪和加速器一般都难入我们法眼。
Core Motion framework
使我们可以很容易驾驭这些传感器,让用户动动手指点划滑就可以完成相关的交互。
含M7或M8处理器中的动态处理器的设备还特别提供了获取已保存动态活动的功能,就比如走了多少步,爬楼梯,还有另外一些运动状态(走圈圈等)。
CM让开发者可以通过传感器(加速器、陀螺仪还有磁力仪)发送原始或经处理过的混合数据来观察并响应iOS设备朝向。
加速器和陀螺仪数据以iOS设备上3维坐标来展示。当手机呈水平放置的时候(如下图),X轴从左(负值)到右(正值),Y周从下(负值)到上(正值),还有就是Z轴垂直方向上从背屏(负值)到屏幕(正值)。
CoreMotionManager
CoreMotionManager
类提供iOS设备动态数据的存取方法。有意思的是,CM提供了 "拉"和"推"两种方式的获取方式。 你可以通过获取当前任何传感器的状态或是CoreMotionManager
的只读属性组合数据来“拉”动态数据。你可以通过block闭包在特定的时间间隔来获取你想要的更新数据集合来捕获“推”数据。
为了保证在高层表现上的性能,苹果推荐你在APP中使用单例CoreMotionManager
。
CoreMotionManager
为4类数据(传感器,加速器,陀螺仪,磁力仪)提供了一致的接口。举个栗子,与陀螺仪交互 - 获取你想要的数据。
可用校验
let manager = CoreMotionManager()
if manager.gyroAvailable {
// ...
}
栗子中manager是视图控制器的一个属性
设置更新时间
manager.gyroUpdateInterval = 0.1
时间单位用的是NSTimeInterval
,反应灵敏降低了,但是CPU使用率减少了。
开始“拉”数据
manager.startGyroUpdates()
通过调用上述方法,manager.gyroData
就可以获取到设备当前的陀螺仪数据。
开始“推”数据
let queue = NSOperationQueue.mainQueue
manager.startGyroUpdatesToQueue(queue) {
(data, error) in
// ...
}
句柄闭包在给定的时间间隔会被不断的调用。
停止更新
manager.stopGyroUpdates()
使用加速器
举个栗子给APP增加一个有趣的功能,不论设备怎么倾斜,背景图始终指向重力方向。
思考如下代码:
首先我们核实确认我们设备可以获取加速器的数据,紧接着我们设定一个高频更新率,然后我们开始在回调闭包中处理图片得旋转角度:
if manager.accelerometerAvailable {
manager.accelerometerUpdateInterval = 0.01
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()) {
[weak self] (data: CMAccelerometerData!, error: NSError!) in
let rotation = atan2(data.acceleration.x, data.acceleration.y) - M_PI
self?.imageView.transform = CGAffineTransformMakeRotation(CGFloat(rotation))
}
}
每个CMAccelerometerData
包中都包含了x,y,z值 - 每个值都展示重力在该坐标轴加速量。也就是说,如果你的设备竖直方向精致放置,那么值将是 (0, -1, 0);在桌子上方面了值会是(0, 0, -1);右倾斜个35度将会是(0.707, -0.707, 0)。
我们计算旋转角度是通过将加速器数据中的x,y分量进行arctan2的计算,然后在CGAffineTransform
中使用该角度。我们图像将会一直保持在右边,不论我们怎么转动手机。
戳这里动图地址。。
结果不尽人意,图片动画有点钝,空间移动设备的影响与转动影响比起来同样甚至更为厉害。我们可以通过多次读取并取它们的均值来抵消此负面影响,但是我们还是开看下当我们引入陀螺仪的时候会发生什么。
添加陀螺仪
我们不从使用startGyroUpdates
获取陀螺仪的原始数据开始。。。我们直接从deviceMotion
数据类型中获取加速器和陀螺仪的组合数据。CM将用户的运动与重力加速度两块分割开来并且提供了CMDeviceMotionData
实例变量做句柄。上述栗子的简单代码如下:
if manager.deviceMotionAvailable {
manager.deviceMotionUpdateInterval = 0.01
manager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) {
[weak self] (data: CMDeviceMotionData!, error: NSError!) in
let rotation = atan2(data.gravity.x, data.gravity.y) - M_PI
self?.imageView.transform = CGAffineTransformMakeRotation(CGFloat(rotation))
}
}
这样看上去就好多了。。。原图戳这里。
UIClunkController (有意思)
我们同样可以使用gyro/acceleration
中其他非重力部分数据来添加交互方法。在这种情形下,我们可以使用CMDeviceMotionData
的userAcceleration
属性来让用户通过触碰设备左侧的方式来回退导航层级。
记得X轴左边对应负值。如果我们可以感应到用户向做加速并大于2.5Gs的时候,这时候将促使我们的试图控制器出栈。实现代码仅需数行:
if manager.deviceMotionAvailable {
manager.deviceMotionUpdateInterval = 0.02
manager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) {
[weak self] (data: CMDeviceMotion!, error: NSError!) in
if data.userAcceleration.x < -2.5 {
self?.navigationController?.popViewControllerAnimated(true)
}
}
}
效果是不是很棒,动图展示戳这里。
获取朝向
我们从陀螺仪数据中能获取的加速数据并不是唯一的好东东,同时我们从中还能知道设备在空间中的朝向。我们可以从CMDeviceMotionData
中attitude
属性获取CMAttitude
实例。CMAttitude
中含有三个能代表设备朝向的值:欧拉角度,四元组,还有一个旋转矩阵。
每个CMAttitude
都与相对应的帧相关。
找到对应的帧
// 略一段。。。
- CMAttitudeReferenceFrameXArbitraryZVertical 描述一个设备平铺状态,Z轴垂直X轴任性。在实际中,当你第一次启动设备运动状态更新的时候X轴将会被修正为设备的朝向。
- CMAttitudeReferenceFrameXArbitraryCorrectedZVertical 本质上与上面相同但是在陀螺仪测试过程中使用磁力仪进行校正。使用磁力仪将增加CPU的开销。
- CMAttitudeReferenceFrameXMagneticNorthZVertical 描述了设备平躺,并且X轴执行北极。这种设定可能需要你的用户进行八个方向的校准操作。
- CMAttitudeReferenceFrameXTrueNorthZVertical 与第三个相同,但是这种北极的校正差异故在除了磁力仪以后还需要地理位置数据。
我们给出的意见是尽管“任性”。
欧拉角度
三个维度的表达中,欧拉角度是最容易懂得,因为他们简单描述了我们对每个坐标轴转动。pitch
是X周方向的转动,增加的时候表示设备正朝你倾斜,减少的时候表示疏远;roll
是Y轴的转向,值减少的时候表示正往左边转,增加的时候往右;yaw
是Z轴转向,减少是时候是顺时针,增加的时候是逆时针。
上面的特性否满足右手定则。(是不是突然想起了高中物理,咦不对我啥时候读过高中,高中是什么。。),即往手指指向方向走势正值,反之负值。
Keep It To Yourself(翻译成“只有你能占有它”,只有你才能看到答案)
解谜游戏APP,朝向你的时候是题目+答案,背向你的时候是题目。
当出谜题的人点击按钮开始出题玩游戏,我们首相确认交互 - 注意用initialAttitude
方法拉动下deviceMotion
:
// get magnitude of vector via Pythagorean theorem
func magnitudeFromAttitude(attitude: CMAttitude) -> Double {
return sqrt(pow(attitude.roll, 2) + pow(attitude.yaw, 2) + pow(attitude.pitch, 2))
}
// initial configuration
var initialAttitude = manager.deviceMotion.attitude
var showingPrompt = false
// trigger values - a gap so there isn't a flicker zone
let showPromptTrigger = 1.0
let showAnswerTrigger = 0.8
然后就是我们所熟悉的方法调用startDeviceMotionUpdates
,我们计算出三个欧拉角度的矢量大小并且使用它们作为展示谜题答案的触发器:
if manager.deviceMotionAvailable {
manager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) {
[weak self] (data: CMDeviceMotion!, error: NSError!) in
// translate the attitude
data.attitude.multiplyByInverseOfAttitude(initialAttitude)
// calculate magnitude of the change from our initial attitude
let magnitude = magnitudeFromAttitude(data.attitude) ?? 0
// show the prompt
if !showingPrompt && magnitude > showPromptTrigger {
if let promptViewController = self?.storyboard?.instantiateViewControllerWithIdentifier("PromptViewController") as? PromptViewController {
showingPrompt = true
promptViewController.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
self!.presentViewController(promptViewController, animated: true, completion: nil)
}
}
// hide the prompt
if showingPrompt && magnitude < showAnswerTrigger {
showingPrompt = false
self?.dismissViewControllerAnimated(true, completion: nil)
}
}
}
效果戳这里。
衍生阅读
略
后续补充
为了不影响用户交互性,我们一般把CoreMotionManager
的更新放在专属的线程队列而不是主线程队列。NSOperationQueue
提供了addOperationWithBlock
便于我们实现:
et queue = NSOperationQueue()
manager.startDeviceMotionUpdatesToQueue(queue) {
[weak self] (data: CMDeviceMotion!, error: NSError!) in
// motion processing here
NSOperationQueue.mainQueue().addOperationWithBlock {
// update UI here
}
}
最后作者交代了下误操作带来一些不要的用户体验,在使用CM的时候要经过深思熟虑。好的电子可以让APP变得生动并且取悦用户。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。