CMDevice​Motion

仅做翻译。

原文链接:https://nshipster.com/cmdevicemotion/
侵删。

在每台 iPhone 光滑的玻璃下面,一组传感器依偎在逻辑板上,向运动协处理器发送稳定的数据流。

核心运动框架使这些传感器的使用变得异常简单,为用户的交互打开了大门,超越了我们每天的轻敲和滑动。

Core Motion 让您可以观察 iOS 或 WatchOS 设备的位置和方向的变化并作出响应。得益于其专用的运动协处理器,iPhone、iPad 和 Apple Watch 可以连续读取和处理来自内置传感器的输入,而不必消耗 CPU 或耗尽电池电量。

加速度计和陀螺仪数据被投影到一个三维坐标空间,设备的中心在原点。

20201214171601.png

对于纵向放置的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。不管手机怎么转,我们的图像都应该是正面朝上的——这里,它是在国家航空航天博物馆(我小时候最喜欢的博物馆)的一个假想应用程序中:

2020.12.14.18.11.01.gif

结果并不十分令人满意——图像的移动是不稳定的,在太空中移动设备对加速度计的影响和旋转一样大甚至更大。这些问题可以通过对多个读数进行采样并取平均值来缓解,但是让我们看看当我们涉及到陀螺仪时会发生什么。

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))
    }
}

好多了!

2020.12.14.18.16.01.gif

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永远看不到答案:

2020.12.14.19.53.01.gif

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
    }
}

记住,并不是所有由核心运动产生的相互作用都是好的。通过运动导航可能很有趣,但也可能很难发现,很容易意外触发,而且可能并非所有用户都可以访问。与无目的的动画类似,过度使用花哨的手势会使你更难集中精力完成手头的任务。

谨慎的开发者会跳过那些让人分心的噱头,找到使用设备动作来丰富应用程序并取悦用户的方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容