仅做翻译。
原文链接:https://nshipster.com/cmdevicemotion/
侵删。
在每台 iPhone 光滑的玻璃下面,一组传感器依偎在逻辑板上,向运动协处理器发送稳定的数据流。
核心运动框架使这些传感器的使用变得异常简单,为用户的交互打开了大门,超越了我们每天的轻敲和滑动。
Core Motion 让您可以观察 iOS 或 WatchOS 设备的位置和方向的变化并作出响应。得益于其专用的运动协处理器,iPhone、iPad 和 Apple Watch 可以连续读取和处理来自内置传感器的输入,而不必消耗 CPU 或耗尽电池电量。
加速度计和陀螺仪数据被投影到一个三维坐标空间,设备的中心在原点。
对于纵向放置的iPhone:
- X轴从左(负值)到右(正值)移动设备的宽度,
- Y轴从底部(-)到顶部(+)运行设备的高度,
- Z轴从后(-)到前(+)垂直穿过屏幕。
CMMotionManager
CMMotionManage 类负责提供有关当前设备运动的数据。为了保持最高水平的性能,在整个应用程序中创建和使用一个共享的 CMMotionManager 实例。
TODO: OC 与 Swift 的实现不一致。
CMMotionManager 为传感器信息提供了四个不同的接口,每个接口都有相应的属性和方法来检查硬件可用性和访问度量。
- accelerometer (加速计)测量加速度,或速度随时间的变化。
- gyroscope(陀螺仪) 测量姿态或设备的方位。
- magnetometer(磁强计)本质上是一个罗盘,测量地球相对于该装置的磁场。
除了这些单独的读数之外,CMMotionManager还提供了一个统一的“device motion”接口,该接口使用传感器融合算法将来自每个传感器的读数组合成空间中设备的统一视图。
Checking for Availability (可用性检查)
虽然现在大多数 Apple 设备都配备了一套标准的传感器,但在尝试读取运动数据之前,最好先检查一下当前设备的功能。
以下示例涉及加速度计,但您可以将“加速度计”替换为您感兴趣的运动数据类型(例如“陀螺仪”、“磁强计”或“设备运动”):
let manager = CMMotionManager()
guard manager.isAccelerometerAvailable else {
return
}
Push vs. Pull
Core Motion提供对运动数据的“pull”和“push”访问。
要“pull”运动数据,可以使用CMMotionManager的只读属性之一访问当前读数。
要接收“push”数据,您可以使用一个在指定时间间隔接收更新的闭包来开始收集所需的数据。
Starting Updates to “pull” Data
manager.startAccelerometerUpdates()
这样调用后,加速度计数据管理器可以随时访问设备的当前加速计数据。
manager.accelerometerData
也可以通过读取相应的“is active”属性来检查运动数据是否可用。
manager.isAccelerometerActive
Starting Updates to “push” Data
manager.startAccelerometerUpdates(to: .main) { (data, error) in
guard let data = data, error == nil else {
return
}
…
}
以更新间隔提供的频率调用传递的闭包。(实际上,核心运动强制执行最小和最大频率,因此指定超出该范围的值会导致该值标准化;您可以通过检查随时间推移的运动事件的时间戳来确定当前设备的有效间隔率。)
Stopping Updates
manager.stopAccelerometerUpdates()
Accelerometer in Action (加速计工作中)
假设我们想给我们的应用程序的启动页面一个有趣的效果,这样无论手机如何倾斜,背景图像都保持水平。
考虑以下代码:
if manager.isAccelerometerAvailable {
manager.accelerometerUpdateInterval = 0.01
manager.startAccelerometerUpdates(to: .main) {
[weak self] (data, error) in
guard let data = data, error == nil else {
return
}
let rotation = atan2(data.acceleration.x,
data.acceleration.y) - .pi
self?.imageView.transform =
CGAffineTransform(rotationAngle: CGFloat(rotation))
}
}
首先,我们检查以确保我们的设备能够提供加速计数据。接下来我们指定一个高更新频率。最后,我们开始更新将旋转UIImageView属性的闭包:
每个CMAccelerMeterData对象都包含一个x、y和z值—每个值都显示该轴的加速度(其中1G=地球上的重力)。如果你的设备是静止的,在纵向方向上笔直地站着,它会有加速度(0,-1,0);平躺在桌子上,它会是(0,0,-1);向右倾斜45度,会是(0.707,-0.707,0)(dat√2 tho)。
我们利用加速度计数据中的x和y分量,用双参数反正切函数(atan2)计算旋转。然后我们使用计算旋转的方法初始化cgafinetransform。不管手机怎么转,我们的图像都应该是正面朝上的——这里,它是在国家航空航天博物馆(我小时候最喜欢的博物馆)的一个假想应用程序中:
结果并不十分令人满意——图像的移动是不稳定的,在太空中移动设备对加速度计的影响和旋转一样大甚至更大。这些问题可以通过对多个读数进行采样并取平均值来缓解,但是让我们看看当我们涉及到陀螺仪时会发生什么。
Adding the Gyroscope(加上陀螺仪)
不是使用原始的陀螺仪数据,而是通过调用startGyroUpdates方法,让我们通过请求统一的“设备运动”数据来获得组合的陀螺仪和加速度计数据。使用陀螺仪,核心运动将用户的运动与重力加速度分开,并将每一项作为CMDeviceMotion对象的自己的属性。代码与我们的第一个示例非常相似:
if manager.isDeviceMotionAvailable {
manager.deviceMotionUpdateInterval = 0.01
manager.startDeviceMotionUpdates(to: .main) {
[weak self] (data, error) in
guard let data = data, error == nil else {
return
}
let rotation = atan2(data.gravity.x,
data.gravity.y) - .pi
self?.imageView.transform =
CGAffineTransform(rotationAngle: CGFloat(rotation))
}
}
好多了!
UIClunkController
我们也可以使用这个合成的陀螺/加速度数据中的另一个非重力部分来添加新的交互方法。在本例中,让我们使用CMDeviceMotion的userAcceleration属性在用户用手轻触设备左侧时向后导航。
请记住,X轴横向穿过我们手中的设备,负值在左侧。如果我们感觉到用户左侧的加速度超过2.5gs,这就是我们从堆栈中弹出视图控制器的提示。该实现与我们前面的示例仅几行不同:
if manager.isDeviceMotionAvailable {
manager.deviceMotionUpdateInterval = 0.01
manager.startDeviceMotionUpdates(to: .main) {
[weak self] (data, error) in
guard let data = data, error == nil else {
return
}
if data.userAcceleration.x < -2.5 {
self?.navigationController?.popViewControllerAnimated(true)
}
}
}
在详细视图中点击设备,我们将立即返回展品列表:
Getting an Attitude
更好的加速度数据并不是我们通过包括陀螺仪数据获得的唯一好处:我们现在也知道了该设备在太空中的真实方位。此数据通过CMDeviceMotion对象的姿态属性访问,并封装在CMAttitude对象中。CMAttitude包含设备方向的三种不同表示:
- Euler angles(欧拉角),
- A quaternion(四元数),
- A rotation matrix(旋转矩阵)。
每一个都与给定的参考系有关。
Finding a Frame of Reference(寻找参照系)
你可以把参考坐标系看作是设备的静止方向,从中可以计算出姿态。所有四种可能的参考框架都描述了这种设备平放在桌子上,并且对其指向的方向越来越具体。
- CMAttitudeReferenceFrameXArbitraryZVertical描述了一种设备,它用一个“任意”X轴平放(垂直Z轴)。实际上,第一次启动设备运动更新时,X轴固定在设备的方向上。
- CMAttitudeReferenceFrameXArbitraryCorrectedZVertical本质上是相同的,但是使用磁强计来校正陀螺仪测量值随时间的变化。
- CMAttitudeReferenceFrameXMagneticNorthZVertical描述的是一种设备,它的X轴(即设备面向您时处于纵向模式的右侧)指向磁北。此设置可能需要用户使用他们的设备执行8字形运动来校准磁强计。
- CMAttitudeReferenceFrameXTrueNorthZVertical与上一个相同,但它会根据磁/真北偏差进行调整,因此除了磁强计外,还需要位置数据。
就我们的目的而言,默认的“任意”参考系是可以的(稍后您将看到原因)。
Euler Angles
在三种姿态表示法中,欧拉角是最容易理解的,因为它们简单地描述了围绕我们已经处理过的每个轴的旋转。
- pitch(俯仰)是围绕X轴旋转,随着设备向您倾斜而增加,随着设备倾斜而减小
- roll(滚动)是围绕Y轴旋转,随着设备向左旋转而减小,向右旋转时增加
- yaw(偏航)是绕(垂直)Z轴旋转,顺时针减小,逆时针增加。
这些值中的每一个都遵循所谓的“右手法则”:拇指朝上做一只杯状的手,拇指指向三个轴中的任何一个。转向你的指尖是积极的,转向是消极的。
Keep It To Yourself(你自己留着吧)
最后,让我们试着用这个设备的态度来实现一个由两个学习伙伴使用的闪存卡应用程序的新交互。我们不会在提示和答案之间手动切换,而是在设备转动时自动翻转视图,这样测试者看到答案,而被测试者只看到提示。
从参考坐标系中找出这种转换是很困难的。为了知道要监视哪个角度,我们需要考虑设备的起始方向,然后确定设备指向哪个方向。相反,我们可以保存一个CMAttitude实例,并将其用作调整后的Euler角集的“零点”,调用multiply(byInverseOf:)方法来转换所有未来的姿态更新。
当测试者点击按钮开始测试时,我们首先配置交互(注意initialAttitude的设备移动的“拉动”):
// get magnitude of vector via Pythagorean theorem
func magnitude(from 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调用中,我们计算由三个Euler角描述的向量的大小,并将其作为触发器来显示或隐藏提示视图:
if manager.isDeviceMotionAvailable {
manager.startDeviceMotionUpdates(to: .main) {
// translate the attitude
data.attitude.multiply(byInverseOf: initialAttitude)
// calculate magnitude of the change from our initial attitude
let magnitude = magnitude(from: data.attitude) ?? 0
// show the prompt
if !showingPrompt && magnitude > showPromptTrigger {
if let promptViewController =
self?.storyboard?.instantiateViewController(
withIdentifier: "PromptViewController"
) as? PromptViewController
{
showingPrompt = true
promptViewController.modalTransitionStyle = .crossDissolve
self?.present(promptViewController,
animated: true, completion: nil)
}
}
// hide the prompt
if showingPrompt && magnitude < showAnswerTrigger {
showingPrompt = false
self?.dismiss(animated: true, completion: nil)
}
}
}
在实现了所有这些之后,让我们来看看交互。随着设备旋转,显示屏会自动切换视图,quizee永远看不到答案:
Further Reading(进一步阅读)
我在前面浏览了CMAttitude的四元数和旋转矩阵组件,但它们激发起我的兴趣。尤其是四元数,有着有趣的历史,如果你想得够久,它会让你陷入特别艰难的思考。
Queueing Up(排队)
为了保持代码示例的可读性,我们将所有的运动更新发送到主队列。一个更好的方法是将这些更新安排在它们自己的队列上,然后调度回main来更新UI。
let queue = OperationQueue()
manager.startDeviceMotionUpdates(to: queue) {
[weak self] (data, error) in
// motion processing here
DispatchQueue.main.async {
// update UI here
}
}
记住,并不是所有由核心运动产生的相互作用都是好的。通过运动导航可能很有趣,但也可能很难发现,很容易意外触发,而且可能并非所有用户都可以访问。与无目的的动画类似,过度使用花哨的手势会使你更难集中精力完成手头的任务。
谨慎的开发者会跳过那些让人分心的噱头,找到使用设备动作来丰富应用程序并取悦用户的方法。