iOS 设备运动与加速度传感器的应用详解
1. 航向与路线的区别
在设备的运动监测中,航向(Heading)和路线(Course)是两个不同的概念。例如,一艘船可能正朝向北方(航向),但实际上却在向东北方向移动(路线)。在某些情况下,我们更关心的是路线。比如在行驶的汽车中,用户握持设备的方式通常并不重要,我们询问航向时,实际上想知道的是汽车的行驶方向。如果系统根据设备的运动情况判断,当我们请求航向时可能想要的是路线,那么它会将路线作为航向提供。
若这并非我们所需,那么可以不使用 Core Location 来确定航向,而是使用 Core Motion 框架,通过获取 CMDeviceMotion 对象的 heading 属性来得到设备在空间中的方向。
2. 加速度、姿态与活动监测
加速度是由于对设备施加力而产生的,通过设备的加速度计来检测,如果设备有陀螺仪,也会辅助检测。重力也是一种力,所以即使用户没有有意识地对设备施加力,加速度计也总有数据可测,设备可以利用加速度检测来报告其相对于垂直方向的姿态。
加速度信息的获取方式有两种:
-
预封装的 UIEvent
:可以接收一个 UIEvent,它会通知我们设备因加速而执行的预定义手势。目前,这种手势只有用户摇晃设备这一种。
-
使用 Core Motion 框架
:实例化 CMMotionManager,然后获取所需类型的信息。可以请求加速度计信息、陀螺仪信息或设备运动信息(还可以使用 Core Motion 来获取磁力计和航向信息),设备运动信息结合了陀螺仪数据和其他传感器的数据,能最好地描述设备在空间中的姿态。
3. 摇晃事件的处理
摇晃事件是一种 UIEvent。要接收摇晃事件,应用程序中必须有一个 UIResponder,它需要满足以下条件:
- 从
canBecomeFirstResponder
返回
true
。
- 实际上是第一响应者。
这个响应者或响应者链中更上层的 UIResponder 应该实现以下部分或全部方法:
-
motionBegan(_:with:)
:表示可能开始了一个摇晃动作,但不一定最终会成为摇晃。
-
motionEnded(_:with:)
:表示
motionBegan
中报告的动作结束,且最终确定为摇晃。
-
motionCancelled(_:with:)
:表示
motionBegan
中报告的动作最终不是摇晃。
通常,实现
motionEnded(_:with:)
就足够了,因为只有当用户执行摇晃手势时才会触发该方法。其第一个参数是事件子类型,但可以保证是
.motionShake
,所以对其进行测试没有意义。
以下是一个简单的实现示例:
override var canBecomeFirstResponder : Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.becomeFirstResponder()
}
override func motionEnded(_ motion: UIEvent.EventSubtype, with e: UIEvent?) {
print("hey, you shook me!")
}
为了避免抢夺视图中其他响应者的撤销功能,可以在
motionEnded
方法中进行判断:
override func motionEnded(_ motion: UIEvent.EventSubtype, with e: UIEvent?) {
if self.isFirstResponder {
print("hey, you shook me!")
} else {
super.motionEnded(motion, with: e)
}
}
4. 使用 Core Motion 框架
使用 Core Motion 框架(
import CoreMotion
)读取加速度计、磁力计、陀螺仪和组合设备运动信息的标准模式在某些方面与使用 Core Location 类似,具体步骤如下:
1. 实例化
CMMotionManager
,通常将其作为实例属性保留。可以直接初始化该属性:
let motman = CMMotionManager()
-
通过检查相应的实例属性(如
isAccelerometerAvailable)来确认所需硬件是否可用。 -
通过设置相应的属性(如
accelerometerUpdateInterval)来设置运动管理器更新新传感器读数的间隔。 -
调用相应的启动方法(如
startAccelerometerUpdates)。 -
运动管理器没有委托,有两种获取数据的方式:
- 拉取(Pull) :在需要数据时轮询运动管理器,请求相应的数据属性。轮询间隔不必与运动管理器的更新间隔相同,轮询时将获取运动管理器的当前数据,即其最近一次更新生成的数据。
-
推送(Push)
:如果目的是收集所有数据,可以调用一个相关方法,该方法接受一个回调函数,最好在由
OperationQueue管理的后台线程上调用。方法名的形式为start...Updates(to:withHandler:)。例如,对于加速度计更新,应调用startAccelerometerUpdates(to:withHandler:)。
-
当不再需要数据时,别忘了调用相应的停止方法(如
stopAccelerometerUpdates)。
以下是使用 Core Motion 框架的流程图:
graph TD;
A[实例化 CMMotionManager] --> B[检查硬件可用性];
B --> C[设置更新间隔];
C --> D[调用启动方法];
D --> E{选择获取数据方式};
E -->|拉取| F[轮询运动管理器];
E -->|推送| G[设置回调函数];
F --> H[获取当前数据];
G --> I[接收回调数据];
H --> J{是否需要更多数据};
I --> J;
J -->|是| F;
J -->|否| K[调用停止方法];
5. 原始加速度分析
如果设备只有加速度计而没有陀螺仪,虽然可以了解施加在设备上的力,但需要做出一些妥协。主要问题是,即使设备完全静止,其加速度值也会构成一个指向地球中心的归一化向量,即重力。因此,加速度计不断报告的是重力和用户引起的加速度的组合。
这种情况有好处也有坏处:好处是在一定限制下,可以使用加速度计检测设备在空间中的姿态;坏处是重力值和用户引起的加速度值混合在一起。不过,可以通过数学方法分离这些值:
-
低通滤波器
:低通滤波器会抑制用户加速度,只报告重力。
-
高通滤波器
:高通滤波器会抑制重力的影响,只检测用户加速度,将静止的设备报告为加速度为零。
在某些情况下,可能需要同时应用低通滤波器和高通滤波器,以了解重力值和用户加速度值。常见的额外技术是将高通滤波器的输出再通过低通滤波器,以减少噪声和小抖动。
以下是一个简单的示例,用于判断设备是否平放在背面:
guard self.motman.isAccelerometerAvailable else { return }
self.motman.accelerometerUpdateInterval = 1.0 / 30.0
self.motman.startAccelerometerUpdates()
self.timer = Timer.scheduledTimer(
timeInterval:self.motman.accelerometerUpdateInterval,
target: self, selector: #selector(pollAccel),
userInfo: nil, repeats: true)
@objc func pollAccel() {
guard let data = self.motman.accelerometerData else {return}
let acc = data.acceleration
let x = acc.x
let y = acc.y
let z = acc.z
let accu = 0.08
if abs(x) < accu && abs(y) < accu && z < -0.5 {
if self.state == .unknown || self.state == .notLyingDown {
self.state = .lyingDown
self.label.text = "I'm lying on my back... ahhh..."
}
} else {
if self.state == .unknown || self.state == .lyingDown {
self.state = .notLyingDown
self.label.text = "Hey, put me back down on the table!"
}
}
}
为了减少对设备在桌子上微小移动的敏感性,可以使用低通滤波器:
func add(acceleration accel:CMAcceleration) {
let alpha = 0.1
self.oldX = accel.x * alpha + self.oldX * (1.0 - alpha)
self.oldY = accel.y * alpha + self.oldY * (1.0 - alpha)
self.oldZ = accel.z * alpha + self.oldZ * (1.0 - alpha)
}
@objc func pollAccel() {
guard let data = self.motman.accelerometerData else {return}
self.add(acceleration: data.acceleration)
let x = self.oldX
let y = self.oldY
let z = self.oldZ
// ... 后续代码与之前相同 ...
}
也可以使用推送方式获取加速度数据:
self.motman.startAccelerometerUpdates(to: .main) { data, err in
guard let data = data else {
print(err)
self.stopAccelerometer()
return
}
self.receive(acceleration:data)
}
func receive(acceleration data:CMAccelerometerData) {
self.add(acceleration: data.acceleration)
let x = self.oldX
let y = self.oldY
let z = self.oldZ
// ... 后续代码与之前相同 ...
}
还有一个检测设备侧面拍打手势的示例,使用高通滤波器消除重力影响:
func add(acceleration accel:CMAcceleration) {
let alpha = 0.1
self.oldX = accel.x - ((accel.x * alpha) + (self.oldX * (1.0 - alpha)))
self.oldY = accel.y - ((accel.y * alpha) + (self.oldY * (1.0 - alpha)))
self.oldZ = accel.z - ((accel.z * alpha) + (self.oldZ * (1.0 - alpha)))
}
@objc func pollAccel(_: Any) {
guard let data = self.motman.accelerometerData else {return}
self.add(acceleration: data.acceleration)
let x = self.oldX
let thresh = 1.0
if x < -thresh {
if data.timestamp - self.oldTime > 0.5 || self.lastSlap == .right {
self.oldTime = data.timestamp
self.lastSlap = .left
self.canceltimer?.invalidate()
self.canceltimer = .scheduledTimer(
withTimeInterval:0.5, repeats: false) { _ in
print("left")
}
}
} else if x > thresh {
if data.timestamp - self.oldTime > 0.5 || self.lastSlap == .left {
self.oldTime = data.timestamp
self.lastSlap = .right
self.canceltimer?.invalidate()
self.canceltimer = .scheduledTimer(
withTimeInterval:0.5, repeats: false) { _ in
print("right")
}
}
}
}
这个手势检测有点棘手,用户必须将设备拍进张开的手中并握住,否则可能会导致错误报告,而且手势检测的延迟较高。可以通过调整代码中的一些参数来提高准确性和性能,更复杂的分析可能需要将最近的
CMAccelerometerData
对象存储在循环缓冲区中,并研究缓冲区内容以确定总体趋势。
iOS 设备运动与加速度传感器的应用详解
6. 陀螺仪的作用
一些设备配备了电子陀螺仪,这对重力和姿态报告的准确性和速度有很大影响。陀螺仪的姿态在空间中保持恒定,因此可以检测到设备姿态的任何变化,这对加速度计测量有两个重要影响:
- 加速度计可以由陀螺仪辅助,从而快速检测出重力和用户引起的加速度之间的差异。
- 陀螺仪可以检测纯旋转,在这种情况下几乎没有加速度,加速度计可能无法提供有用信息。极端情况是绕重力轴的恒定姿态旋转,仅靠加速度计完全无法检测到(因为没有用户引起的力,且重力保持不变)。
可以跟踪原始陀螺仪数据,确保设备有陀螺仪(
isGyroAvailable
),然后调用
startGyroUpdates
。从运动管理器获取的是
CMGyroData
对象,它结合了时间戳和
CMRotationRate
,报告绕每个轴的旋转速率,单位是弧度每秒,正数值表示从正轴方向看为逆时针旋转。但陀螺仪值存在缩放和偏差问题,即值基于任意比例,且随时间以大致恒定的速率逐渐增加(或减少),因此处理原始陀螺仪数据意义不大。
通常,我们更感兴趣的是至少结合陀螺仪和加速度计的数据。Core Motion 会将这些传感器的数据计算组合成一个设备运动实例(
CMDeviceMotion
),并消除传感器的内部偏差和缩放影响。
7. CMDeviceMotion 详解
CMDeviceMotion
包含以下属性,这些属性对应设备的自然 3D 框架(x 轴向右增加,y 轴向上增加,z 轴从屏幕前方增加):
| 属性 | 描述 |
| ---- | ---- |
|
gravity
| 一个
CMAcceleration
,表示一个指向地球中心、值为 1 的向量,单位是 G。 |
|
userAcceleration
| 一个
CMAcceleration
,描述用户引起的加速度,不包含重力分量,单位是 G。 |
|
rotationRate
| 一个
CMRotationRate
,描述设备绕自身中心的旋转速率,本质上是
CMGyroData
的
rotationRate
并考虑了缩放和偏差。 |
|
magneticField
| 一个
CMCalibratedMagneticField
,描述作用在设备上的磁力,单位是微特斯拉。传感器的内部偏差已被消除,精度有以下几种(
CMMagneticFieldCalibrationAccuracy
):
-
.uncalibrated
-
.low
-
.medium
-
.high
|
|
attitude
| 一个
CMAttitude
,描述设备在空间中的瞬时姿态。姿态是相对于一个参考框架(
CMAttitudeReferenceFrame
)测量的,在请求运动管理器开始生成更新时指定,需要先调用类方法
availableAttitudeReferenceFrames
确认所需参考框架在该设备上可用。不同参考框架的区别在于 x 轴的指向(y 轴与其他两轴正交):
-
.xArbitraryZVertical
:x 轴可以指向任意方向。
-
.xArbitraryCorrectedZVertical
:与上一个选项相同,但使用磁力计保持准确性(防止参考框架随时间漂移)。
-
.xMagneticNorthZVertical
:x 轴指向磁北。
-
.xTrueNorthZVertical
:x 轴指向真北。除非同时使用 Core Location 获取设备位置,否则该值可能不准确。 |
|
heading
| 一个
Double
,表示设备的方向,是从北开始顺时针的度数(不是弧度),参考框架必须是
.xMagneticNorthZVertical
或
.xTrueNorthZVertical
(否则值为 -1)。与 Core Location 的
CLHeading
不同,它是纯方向读数,不包含路线信息。不仅使用了磁力计,还使用了加速度计和陀螺仪,有助于消除局部磁异常引起的误差。 |
CMAttitude
的值可以通过以下几种属性访问,每种属性对应不同的系统,适用于不同的目的:
-
pitch, roll, yaw
:设备相对于参考框架绕自然 x 轴、y 轴和 z 轴的偏移角度,单位是弧度(也称为欧拉角)。
-
rotationMatrix
:一个
CMRotationMatrix
结构体,包含一个 3×3 矩阵,表示在参考框架中的旋转。
-
quaternion
:一个
CMQuaternion
,描述姿态。四元数常用于 3D 场景,如 SceneKit 和 Metal。
8. 利用 CMDeviceMotion 实现简单指南针/测斜仪
将设备变成一个简单的指南针/测斜仪,只需请求相对于磁北的姿态,并获取其 pitch、roll 和 yaw。具体步骤如下:
1. 确认设备支持设备运动监测:
guard self.motman.isDeviceMotionAvailable else { return }
- 选择参考框架并确认其可用性:
let r = CMAttitudeReferenceFrame.xMagneticNorthZVertical
guard CMMotionManager.availableAttitudeReferenceFrames().contains(r) else {
return
}
- 设置运动管理器的属性:
self.motman.showsDeviceMovementDisplay = true
self.motman.deviceMotionUpdateInterval = 1.0 / 30.0
- 启动设备运动更新:
self.motman.startDeviceMotionUpdates(using: r)
- 设置定时器进行轮询:
let t = self.motman.deviceMotionUpdateInterval * 10
self.timer = Timer.scheduledTimer(timeInterval:t,
target:self, selector:#selector(pollAttitude),
userInfo:nil, repeats:true)
-
在
pollAttitude方法中获取姿态信息:
@objc func pollAttitude() {
guard let mot = self.motman.deviceMotion else {return}
let acc = mot.magneticField.accuracy.rawValue
if acc <= CMMagneticFieldCalibrationAccuracy.low.rawValue {
return // 磁力计未准备好
}
let att = mot.attitude
let to_deg = 180.0 / .pi
print("\(att.pitch * to_deg), \(att.roll * to_deg), \(att.yaw * to_deg)")
let g = mot.gravity
let whichway = g.z > 0 ? "forward" : "back"
print("pitch is tilted \(whichway)")
}
当设备水平(平放在背面)且 x 轴(右边缘)指向磁北时,这些值都接近零,设备相对于正轴逆时针旋转时,每个值都会增加。例如,设备直立(顶部指向天空)时,pitch 接近 90;设备右侧边缘着地时,roll 接近 90;设备平放在背面且顶部指向北时,yaw 接近 -90。
需要注意的是,欧拉角在数学上存在一些问题:
- roll 和 yaw 从 0 到 π(180 度)逆时针旋转增加,然后跳到 -π(-180 度),继续增加到 0;而 pitch 增加到 π/2(90 度),然后减小到 0,再减小到 -π/2(-90 度),最后增加到 0。这意味着仅通过 pitch、roll 和 yaw 探索姿态时,姿态信息可能不足以描述设备的姿态,需要结合重力的 z 分量来区分不同情况。
- 在某些方向上,值会变得不准确。特别是当 pitch 接近 ±90 度(设备直立或倒置)时,roll 和 yaw 会变得不稳定,这种现象被称为“奇点”或“万向节锁”。可以使用不同的姿态表示方式,如
rotationMatrix
来解决这个问题。
9. 利用 CMAttitude 的 rotationMatrix 实现图层旋转
以下示例展示了如何使用
CMAttitude
的
rotationMatrix
属性使
CALayer
响应设备的当前姿态进行旋转:
1. 进行常规准备,选择参考框架为
.xArbitraryZVertical
:
guard self.motman.isDeviceMotionAvailable else { return }
let r = CMAttitudeReferenceFrame.xArbitraryZVertical
guard CMMotionManager.availableAttitudeReferenceFrames().contains(r) else {
return
}
self.motman.deviceMotionUpdateInterval = 1.0 / 30.0
self.motman.startDeviceMotionUpdates(using: r)
let t = self.motman.deviceMotionUpdateInterval * 10
self.timer = Timer.scheduledTimer(timeInterval:t,
target:self, selector:#selector(pollAttitude),
userInfo:nil, repeats:true)
-
在
pollAttitude方法中存储初始姿态并进行姿态转换:
@objc func pollAttitude() {
guard let mot = self.motman.deviceMotion else {return}
let att = mot.attitude
if self.ref == nil {
self.ref = att
return
}
att.multiply(byInverseOf: self.ref)
let r = att.rotationMatrix
var t = CATransform3DIdentity
t.m11 = CGFloat(r.m11)
t.m12 = CGFloat(r.m12)
t.m13 = CGFloat(r.m13)
t.m21 = CGFloat(r.m21)
t.m22 = CGFloat(r.m22)
t.m23 = CGFloat(r.m23)
t.m31 = CGFloat(r.m31)
t.m32 = CGFloat(r.m32)
t.m33 = CGFloat(r.m33)
let lay = // 获取相应的 CALayer
CATransaction.setAnimationDuration(1.0/10.0)
lay.transform = t
}
这个示例中,图层会随着设备旋转而尝试保持静止。但需要注意的是,随着时间推移,变换可能会出现漂移,即使设备静止,图层也会逐渐旋转。
.xArbitraryCorrectedZVertical
参考框架就是为解决这个问题而设计的。
综上所述,通过合理利用 iOS 设备的加速度计、陀螺仪和磁力计等传感器,结合 Core Motion 框架,我们可以实现多种有趣且实用的功能,如姿态检测、手势识别、指南针和测斜仪等。在实际应用中,需要根据具体需求选择合适的传感器和数据处理方法,并注意处理传感器数据中的各种问题,以确保功能的准确性和稳定性。
超级会员免费看
29

被折叠的 条评论
为什么被折叠?



