iOS 陀螺仪(仿贝壳VR图片晃动)、加速计、iOS中的传感器 等简介

2,794 阅读8分钟

Core Motion (陀螺仪、加速计)

Core Motion框架结构示意图

Core Motion 获取数据的两种方式

push

实时采集所有数据(采集频率高)

  1. 创建运动管理者对象
    CMMotionManager *mgr = [[CMMotionManager alloc] init];
    
  2. 判断加速计是否可用(最好判断)
    if (mgr.isAccelerometerAvailable) {
       // 加速计可用
    }
    
  3. 设置采样间隔
    mgr.accelerometerUpdateInterval = 1.0/30.0; // 1秒钟采样30次
    
  4. 开始采样(采样到数据就会调用handler,handler会在queue中执行)
    - (void)startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMAccelerometerHandler)handler;
    
pull

在有需要的时候,再主动去采集数据

  1. 创建运动管理者对象
    CMMotionManager *mgr = [[CMMotionManager alloc] init];
    
  2. 判断加速计是否可用(最好判断)
    if (mgr.isAccelerometerAvailable) { // 加速计可用 }
    
  3. 开始采样
    - (void)startAccelerometerUpdates;
    
  4. 在需要的时候采集加速度数据
    CMAcceleration acc = mgr.accelerometerData.acceleration;
    NSLog(@"%f, %f, %f", acc.x, acc.y, acc.z);
    

实际使用举例

仿贝壳VR图片晃动(陀螺仪)

swift 版本: 包含两个文件 GyroMannager.swift、GyroscopeImageView.swift,代码实现了在界面显示时自动可以晃动,离开界面停止晃动,下拉放大图片等功能。
Demo地址:demo->陀螺仪使用-晃动手机移动图片
GyroMannager.swift

import UIKit
import CoreMotion

public class GyroMannager {
    
    static var shared = GyroMannager()
    private let gyromanager = CMMotionManager()
    /// manager刷新时间间隔(采样频率设置为比屏幕刷新频率1/60稍小)
    public var timestamp:CGFloat = 0.016
    /// 用于记录引用 CMMotionManager 的数量
    private var useNumber:Int = 0
    
    private init() {}
    
    func monitorDeviceMotion(index: Int) {
        useNumber += index
        if useNumber <= 0 {
            stopMotionUpdates()
        } else {
            startMotionUpdates()
        }
    }
    
    /// 开始陀螺仪
    private func startMotionUpdates() {
        if self.gyromanager.isGyroAvailable {
            if self.gyromanager.isGyroActive { return }
            self.gyromanager.gyroUpdateInterval = TimeInterval(self.timestamp)
            self.gyromanager.startGyroUpdates(to: OperationQueue.main) { [weak self] (data, nil) in
                if let gyroData = data {
                    print("陀螺仪---Updates---0")
                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kCMMotionManagerUpdatesNotification"), object: self, userInfo: ["gyroData": gyroData])
                }
            }
        }
    }
    
    /// 停止陀螺仪
    private func stopMotionUpdates() {
        print("陀螺仪---stopMotionUpdates---", self.useNumber)
        if self.useNumber <= 0 {
            print("陀螺仪---stopMotionUpdates")
            self.gyromanager.stopGyroUpdates()
        }
    }
    
}

GyroscopeImageView.swift 把moveImgView放在scrollView是为了实现下拉屏幕放大图片的效果,如果只需要晃动,不需要下拉放大,可以不放在scrollView上。

//
//  GyroscopeImageView.swift
//  ORSwiftSummary
//
//  Created by orilme on 2020/10/16.
//  Copyright © 2020 orilme. All rights reserved.
//

import UIKit
import CoreMotion
import MJRefresh

class GyroscopeImageView: UIView {
    
    public lazy var moveImgView:UIImageView = {
        let moveImgView = UIImageView()
        return moveImgView
    }()
    
    public lazy var scrollView: UIScrollView = {
        let scroll = UIScrollView()
        scroll.delegate = self
        scroll.maximumZoomScale = 5.0
        scroll.minimumZoomScale = 1.0
        scroll.panGestureRecognizer.isEnabled = false
        scroll.pinchGestureRecognizer?.isEnabled = false
        scroll.panGestureRecognizer.cancelsTouchesInView = false
        
        return scroll
    }()
    
    /// 图片最大偏移量
    private var mMaxOffsetX:CGFloat = 0.00
    private var mMaxOffsetY:CGFloat = 0.00
    
    /// 陀螺仪在X、Y轴旋转的最大弧度
    /// The value must between (0, π  / 8).
    private var mMaxRotateRadian:CGFloat = CGFloat(Double.pi / 8)

    /// 相对于初始位置的旋转弧度
    private var mRotateRadianX:CGFloat = 0.00
    private var mRotateRadianY:CGFloat = 0.00
    
    /// 相对初始位置的偏移距离
    private var progressX:CGFloat = 0.00
    private var progressY:CGFloat = 0.00
    
    /// 偏移的距离
    private var currentOffsetX:CGFloat = 0.00
    private var currentOffsetY:CGFloat = 0.00
    
    /// 是否初始化过
    private var isReload:Bool = false
    /// 第一次加载的view的size
    private var startSize:CGSize = CGSize.zero
    /// 第一次加载的时候img的size
    private var startImgSize:CGSize = CGSize.zero
    
    /// 是否需要更新frame
    private var isGyroscopeUpdate: Bool = true
    /// view所在的控制器
    private weak var parentVC: UIViewController?

    override init(frame: CGRect) {
        super.init(frame: frame)
        gyroscopeImageViewLoad()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        gyroscopeImageViewLoad()
    }
    
    func gyroscopeImageViewLoad() {
        clipsToBounds = true
        scrollView.addSubview(moveImgView)
        addSubview(scrollView)
        GyroMannager.shared.monitorDeviceMotion(index: 1)
        NotificationCenter.default.addObserver(self, selector: #selector(motionManagerUpdates(_:)), name: NSNotification.Name(rawValue: "kCMMotionManagerUpdatesNotification"), object: nil)
    }
    
    @objc func motionManagerUpdates(_ notification: NSNotification) {
        if let vc = self.parentVC {
            /// 判断不在VR所在的界面时不执行后面的代码,不移动VR
            isGyroscopeUpdate = vc.isViewLoaded && (vc.view.window != nil)
            //print("陀螺仪---Updates---2", isGyroscopeUpdate, (vc.view.window != nil))
        }
        /// 隐藏不刷新,当前控制器不在最前面不刷新
        guard !isHidden,
            isGyroscopeUpdate,
            let gyroData = notification.userInfo?["gyroData"] as? CMGyroData else {
            return
        }
        //print("陀螺仪---Updates---1")
        let x = CGFloat(gyroData.rotationRate.x)
        let y = CGFloat(gyroData.rotationRate.y)
        self.imagePanTransform(x: x, y: y)
    }
    
    /* 设置移动的imageView 的 frame
     * 注意: 调用此方法时必须等GyroscopeImageView加载完成调用,或者传入width,height 即GyroscopeImageView的宽高,否则可能会引起显示偏移
     */
    func setMoveImgViewFrame(width: CGFloat?, height: CGFloat?) {
        if isReload == true {
            updateMoveImgViewFrame(height: self.frame.size.height)
            return
        }
        isReload = true
        var containerWidth = self.frame.size.width
        var containerHeight = self.frame.size.height
        if let cwidth = width {
            containerWidth = cwidth
        }
        if let cheight = height {
            containerHeight = cheight
        }
        //moveImgView.sizeToFit()
        let moveImgViewSize = moveImgView.sizeThatFits(CGSize.zero)
        let imgWidth = moveImgViewSize.width
        let imgHeight = moveImgViewSize.height
        
        var newHeight = imgHeight
        var newWidth = imgWidth
        if imgWidth/imgHeight > containerWidth/containerHeight {
            newHeight = containerHeight * 1.25
            newWidth = newHeight * (imgWidth / imgHeight)
        } else {
            newWidth = containerWidth * 1.25
            newHeight = newWidth * (imgHeight / imgWidth)
        }
        mMaxOffsetX = (newWidth - containerWidth) * 0.5
        mMaxOffsetY = (newHeight - containerHeight) * 0.5
        startSize = CGSize(width: containerWidth, height: containerHeight)
        startImgSize = CGSize(width: newWidth, height: newHeight)
        scrollView.frame = CGRect(x: -self.mMaxOffsetX, y: -self.mMaxOffsetY, width: newWidth, height: newHeight)
        moveImgView.frame = CGRect(x: 0, y: 0, width: newWidth, height: newHeight)
        self.parentVC = self.firstViewController()
    }
    
    /// 下拉重新加载过程跟新view frame
    func updateMoveImgViewFrame(height: CGFloat) {
        let ratio = height/startSize.height
        scrollView.setZoomScale(ratio, animated: false)
        scrollView.mj_size = CGSize(width: startImgSize.width, height: startImgSize.height * ratio)
    }

    /// 更新 imagView 的 frame
    func imagePanTransform(x: CGFloat, y: CGFloat) {
        #if DEBUG
        //print("监听到陀螺仪,正在刷新frame---", x, y)
        #endif
        if abs(x) < 0.01, abs(y) < 0.01 {
            /// 晃动幅度过小不更新frame,要不然太频繁
            //print("晃动幅度过小不更新frame,要不然太频繁")
            return
        }
        let timestamp = GyroMannager.shared.timestamp
        mRotateRadianY += y * timestamp
        mRotateRadianX += x * timestamp

        if (mRotateRadianY > mMaxRotateRadian) {
            mRotateRadianY = mMaxRotateRadian
        }
        else if (mRotateRadianY < -mMaxRotateRadian) {
            mRotateRadianY = -mMaxRotateRadian
        }
        if (mRotateRadianX > mMaxRotateRadian) {
            mRotateRadianX = mMaxRotateRadian
        }
        else if (mRotateRadianX < -mMaxRotateRadian) {
            mRotateRadianX = -mMaxRotateRadian
        }
        
        progressX = mRotateRadianY/mMaxRotateRadian
        progressY = mRotateRadianX/mMaxRotateRadian
        
        currentOffsetX = mMaxOffsetX * progressX
        currentOffsetY = mMaxOffsetY * progressY
        
        var frame = moveImgView.frame
        frame.origin.x = currentOffsetX
        frame.origin.y = currentOffsetY
        self.moveImgView.frame = frame
        self.layoutIfNeeded()
    }
    
    deinit {
        print("陀螺仪---view释放")
        GyroMannager.shared.monitorDeviceMotion(index: -1)
        NotificationCenter.default.removeObserver(self)
    }
    
}

extension GyroscopeImageView: UIScrollViewDelegate {
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        scrollView.panGestureRecognizer.isEnabled  = false
        scrollView.pinchGestureRecognizer?.isEnabled = false
    }
    
    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
        scrollView.panGestureRecognizer.isEnabled  = false
        scrollView.pinchGestureRecognizer?.isEnabled = false
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return moveImgView
    }
    
}

extension UIView {
    func firstViewController() -> UIViewController? {
        for view in sequence(first: self.superview, next: { $0?.superview }) {
            if let responder = view?.next {
                if responder.isKind(of: UIViewController.self){
                    return responder as? UIViewController
                }
            }
        }
        return nil
    }
}

待优化的点:CMMontionManager只负责数据的采集。利用CADisplayLink 刷新更新 view 的 frame。

摇一摇
  • 监控摇一摇的方法
    方法1:通过分析加速计数据来判断是否进行了摇一摇操作(比较复杂)。 方法2:iOS自带的Shake监控API(非常简单)。

  • 判断摇一摇的步骤:实现3个摇一摇监听方法

    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event /** 检测到摇动 */
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event /** 摇动取消(被中断) */
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event /** 摇动结束 */
    

iOS中的一些传感器

iPhone中内置的一些传感器

  • 运动/加速度传感器(Motion/Accelerometer Sensor)
    功能:通过测量三个轴的加速度大小来判断人体运动。
    局限:受重力干扰大,瞬时误差大。
    应用:活动测量,感应设备的运动(摇一摇、计步器)

  • 陀螺仪(Gyroscope)
    功能:通过测量三个轴的旋转速率来判断朝向
    局限:误差会积累,长时间读数准确性差。
    应用:感应设备的持握方式(赛车类游戏)

  • 环境光传感器(Ambient Light Sensor)
    感应周边环境光线的强弱(自动调节屏幕亮度)

  • 距离传感器(Proximity Sensor)
    功能:无需物理接触就判断附近物体的存在。
    局限:不通用,大多数只针对几种材质。
    应用:智能省电、感应是否有其他物体靠近设备屏幕(打电话自动锁屏)

  • 磁力计传感器(Magnetometer Sensor)
    功能:通过测量设备周围的磁场的强度和方向来判断朝向。
    应用:导航、感应周边的磁场(合盖锁屏)

  • 内部温度传感器(Internal Temperature Sensor)
    感应设备内部的温度(提醒用户降温,防止损伤设备)

  • 湿度传感器(Moisture Sensor)
    感应设备是否进水(方便维修人员)

运动/加速度传感器(Motion/Accelerometer Sensor)

最早出现在iOS设备上的传感器之一。加速计用于检测设备在X、Y、Z轴上的加速度 (哪个方向有力的作用)

  • 加速计的作用
    用于检测设备的运动(比如摇晃)

  • 加速计的经典应用场景
    摇一摇
    计步器

  • 加速计的原理
    检测设备在X、Y、Z轴上的加速度 (哪个方向有力的作用,哪个方向运动了)。 根据加速度数值,就可以判断出在各个方向上的作用力度。

陀螺仪(Gyroscope)

陀螺仪是随着iPhone4的上市首次出现在iOS设备上的传感器。 陀螺仪可以用于检测设备的持握方式。 陀螺仪的原理是检测设备在X、Y、Z轴上所旋转的角速度。

相对于运动/加速度传感器,从它本身的特点来说,激发它需要一个可被察觉的加速度,从而使得它更有利于侦测设备相对于外界参照物的移动,但是 对于检测设备本身的动作或姿态并不精准。这就是陀螺仪进驻 iOS 设备的原因,它 能够让设备对于自身当前的姿态有更准确的了解。iOS 设备中的陀螺仪都是三轴陀 螺仪,三轴的方向与定义与运动/加速度传感器中的三轴定义相同(图片同上)。不同的地方是陀 螺仪并不用来侦测设备在三个轴向上的线性加速度,而是用于侦测设备沿三个轴为 中线所旋转时的角速度。这里有了三个名词,分别为 pitch (纵倾), roll (横倾) 和 yaw (横摆)。让我们还是以前面介绍运动/加速度传感器时的描述开始。
想象你的设备正面向上平放在一个 水平平面上,则现在你抬起设备左边缘或右边缘,使得设备沿着 y 轴为中心线做旋 转,这就叫做 roll (横倾)。如果你抬起设备的上边缘或下边缘,让设备沿着 x 轴 为中心线做旋转,则叫做 pitch (纵倾)。如果你将设备水平的在平面上沿着垂直于水 平面的 z 轴旋转,则叫做 yaw (横摆)。从这里你可以看到,陀螺仪通过对这三个轴 角速度的侦测,可以计算得出设备当前的姿态。这对于很多赛车类游戏的操控方式 有着重要的意义,可以通过旋转设备检测到的角速度,来模拟汽车驾驶时方向盘旋 转的动作,使得这类游戏的操控体验更为直观,精确。

  • 加速计的经典应用场景
    陀螺仪在赛车类游戏中有重大作用:模拟汽车驾驶时方向盘旋转的动作,使得这类游戏的操控体验更为真实。

环境光传感器(Ambient Light Sensor)

是iOS、Mac设备中最为古老的传感器成员。
它能够让你在使用 Mac、iPhone、iPad时,眼睛更为舒适。
从一个明亮的室外走入相对黑暗的室内后,iOS设备会自动调低亮度,让屏幕显得不再那么光亮刺眼。
当你使用iPhone拍照时,闪光灯会在一定条件下自动开启。
几乎所有的Mac 都带有背光键盘,当周围光线弱到一定条件时,会自动开启键盘背光。

距离传感器(Proximity Sensor)

用于检测是否有其他物体靠近设备屏幕。
当你打电话或接电话时将电话屏幕贴近耳边,iPhone会自动关闭屏幕 ,好处是节省电量,防止耳朵或面部不小心触摸屏幕而引发一些不想要的意外操作 。

// 开启距离感应功能
[UIDevice currentDevice].proximityMonitoringEnabled = YES;
// 监听距离感应的通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(proximityChange:)
name:UIDeviceProximityStateDidChangeNotification
object:nil];

- (void)proximityChange:(NSNotificationCenter *)notification {
    if ([UIDevice currentDevice].proximityState == YES) {
	NSLog(@"某个物体靠近了设备屏幕"); // 屏幕会自动锁住
    } else {
	NSLog(@"某个物体远离了设备屏幕"); // 屏幕会自动解锁
    }
}

磁力计传感器(Magnetometer Sensor)

可以感应地球磁场,获得方向信息,使位置服务数据更精准。
可以用于电子罗盘和导航应用。
iPad的Smart Cover盒盖睡眠操作就是基于磁力计传感器。

内部温度传感器(Internal Temperature Sensor)

从 iPad一代开始,iOS设备都加入了一个内部温度传感器,用于检测内部组件温度,当温度超过系统设定的阈值时,会出现以下提示。
内部温度传感器,对于提升iOS设备自身安全性与稳定性有很大的帮助。

湿度传感器(Moisture Sensor)

湿度传感器跟其他基于微电子的传感器不同,是一个简单的物理传感器。简单来说,湿度传感器就是一张遇水变红的试纸。
Apple的维修人员就是通过检测试纸是否变红,来判断设备是否进水(设备进水不在保修范围之内)。