【iOS 开发】3D Banner的实现

1.效果

裸眼3D

2.CMMotionManager 概述

用于启动和管理运动服务的对象。

1
class CMMotionManager : NSObject

使用CMMotionManager对象启动报告设备板载传感器检测到的运动的服务。使用此对象接收四种类型的运动数据:

  • 加速度计数据,表示设备在三维空间的瞬时加速度。
  • 陀螺仪数据,表示围绕设备三个主轴的瞬时旋转。
  • 磁力计数据,指示设备相对于地球磁场的方向。
  • 设备运动数据,指示与运动相关的关键属性,例如设备的用户启动加速度、其姿态、旋转速率、相对于校准磁场的方向以及相对于重力的方向。该数据由 Core Motion 的传感器融合算法提供。

处理后的设备运动数据给出了设备的姿态、旋转速率、校准磁场、重力方向以及用户赋予设备的加速度。

只为您的应用创建一个 CMMotionManager 对象。此类的多个实例会影响从加速度计和陀螺仪接收数据的速率。

您可以按指定的更新间隔接收实时传感器数据,也可以让传感器收集数据并将其存储以供以后检索。使用这两种方法,当您不再需要数据时调用适当的停止方法 (stopAccelerometerUpdates(), stopGyroUpdates(), stopMagnetometerUpdates(),和 stopDeviceMotionUpdates()) 。

以指定的间隔处理运动更新

为了在特定的时间间隔接收运动数据,app 调用一个“start”方法,该方法采用一个操作队列(OperationQueue的实例)和一个特定类型的block handler 来处理这些更新。运动数据被传递到block handler 中。更新频率由“interval”属性的值决定。

  • 加速度计。设置accelerometerUpdateInterval属性以指定更新间隔。调用startAccelerometerUpdates(to:withHandler:) 该方法,传入一个CMAccelerometerHandler类型的block。加速度计数据作为CMAccelerometerData对象传递到block中。
  • 陀螺仪。设置gyroUpdateInterval属性以指定更新间隔。调用startGyroUpdates(to:withHandler:) 该方法,传入一个CMGyroHandler类型的块。旋转速率数据作为CMGyroData对象传递到block中。
  • 磁力计。设置magnetometerUpdateInterval属性以指定更新间隔。调用 startMagnetometerUpdates(to:withHandler:) 该方法,传递一个CMMagnetometerHandler类型的block。磁场数据作为CMMagnetometerData对象传递到block中。
  • 设备运动。设置属性deviceMotionUpdateInterval以指定更新间隔。调用startDeviceMotionUpdates(using:)startDeviceMotionUpdates(using:to:withHandler:)startDeviceMotionUpdates(to:withHandler:) 方法,传入一个CMDeviceMotionHandler类型的块。旋转速率数据作为CMDeviceMotion对象传递到block中。
1
2
3
4
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {

}];

运动数据的定期采样

为了通过周期性采样来处理运动数据,应用程序调用一个不带参数的“start”方法,并周期性地访问给定类型运动数据的属性所保存的运动数据。这种方法是游戏等应用程序的推荐方法。在一个block中处理加速度计数据会带来额外的开销,并且大多数游戏应用程序只对渲染帧时的最新运动数据样本感兴趣。

  • 加速度计。调用startAccelerometerUpdates() 以开始更新并通过读取accelerometerData 属性定期访问CMAccelerometerData 对象。
  • 陀螺仪。 调用startGyroUpdates() 以开始更新并通过读取gyroData 属性定期访问CMGyroData 对象。
  • 磁力计。调用startMagnetometerUpdates() 以开始更新并通过读取magnetometerData 属性定期访问CMMagnetometerData对象。
  • 设备运动。 调用startDeviceMotionUpdates(using:)startDeviceMotionUpdates()方法开始更新并通过读取deviceMotion属性定期访问CMDeviceMotion对象。该方法(iOS 5.0 中的新方法)允许您指定用于姿态估计的参考框架。
1
2
3
4
5
6
7
8
9
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1/15.0;
if (motionManager.deviceMotionAvailable) {
[motionManager startDeviceMotionUpdates];
double x = motionManager.deviceMotion.gravity.x;
double y = motionManager.deviceMotion.gravity.y;
double z = motionManager.deviceMotion.gravity.z;
NSLog(@"x:%f, y:%f, z:%f", x, y, z);
}

CMDeviceMotion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface CMDeviceMotion : CMLogItem
// attitude 用于标识空间位置的欧拉角(roll、yaw、pitch)和四元数(quaternion)
@property(readonly, nonatomic) CMAttitude *attitude;
// rotationRate 标识设备旋转速率
@property(readonly, nonatomic) CMRotationRate rotationRate;
// gravity 用于标识重力在设备各个方向的分量,具体值的变化遵循如下规律:重力方向始终指向地球,而在设备的三个方向上有不同分量,最大可达 1.0,最小是 0.0
@property(readonly, nonatomic) CMAcceleration gravity;
// userAcceleration 用于标识设备各个方向上的加速度,可以标识当前设备正在当前方向上减速 or 加速
@property(readonly, nonatomic) CMAcceleration userAcceleration;
// magneticField 用于标识设备周围的磁场范围和精度
@property(readonly, nonatomic) CMCalibratedMagneticField magneticField COREMOTION_EXPORT API_AVAILABLE(ios(5.0));
// heading 用于标识北极方向
@property(readonly, nonatomic) double heading COREMOTION_EXPORT API_AVAILABLE(ios(11.0));
// 传感器位置
@property(readonly, nonatomic) CMDeviceMotionSensorLocation sensorLocation COREMOTION_EXPORT API_AVAILABLE(ios(14.0));
@end

img

硬件可用性和状态

如果某个硬件功能(例如陀螺仪)在设备上不可用,则调用与该功能相关的启动方法无效。您可以通过检查相应的属性来了解硬件功能是否可用或处于活动状态;例如,对于陀螺仪数据,您可以检查isGyroAvailableisGyroActive属性的值。

3.实现方式

  • 图片分层
  • 通过传感器获取偏移角度
  • 计算偏移量,更新图片位置

4.核心代码

iOS 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#import <CoreMotion/CoreMotion.h>
@property (nonatomic, strong) CMMotionManager *motionManager;
static const CGFloat CRMotionDeviceMotionMinimumTreshold = 0.f;
// 采样频率
static const CGFloat CRMotionDeviceMotionUpdateInterval = 0.02;
// 往左偏移角度 45°
static const CGFloat CRMotionDeviceMotionXLFactor = 1.0/(M_PI_4/M_PI_2);
// 往右偏移角度 45°
static const CGFloat CRMotionDeviceMotionXRFactor = 1.0/(M_PI_4/M_PI_2);
// 往下偏移角度 60°
static const CGFloat CRMotionDeviceMotionYBFactor = 1.0/(M_PI/3/M_PI_2);
// 往上偏移角度 45°
static const CGFloat CRMotionDeviceMotionYTFactor = 1.0/(M_PI_4/M_PI_2);
// 左、右偏移位移
static const CGFloat CRMotionDeviceMotionXLimit = 20.0f;
// 上、下偏移位移
static const CGFloat CRMotionDeviceMotionYLimit = 12.0f;

- (void)setData {
if (self.imgURLs.count > 0) {
[self startMonitor];
} else {
// 停止更新
[self.motionManager stopDeviceMotionUpdates];
}
}

- (void)startMonitor {
if (!_motionManager) {
_motionManager = [[CMMotionManager alloc] init];
// 采样频率
_motionManager.deviceMotionUpdateInterval = CRMotionDeviceMotionUpdateInterval;
}

// 往左偏移角度 45°
CGFloat xl = CRMotionDeviceMotionXLFactor;
// 往右偏移角度 45°
CGFloat xr = CRMotionDeviceMotionXRFactor;
// 往上偏移角度 45°
CGFloat yt = CRMotionDeviceMotionYTFactor;
// 往下偏移角度 60°
CGFloat yb = CRMotionDeviceMotionYBFactor;
CGFloat oy = (M_PI_2/3)/M_PI_2*1.0;
CGFloat ox = 0*1.0;
// deviceMotionAvailable:检测硬件是否正常,deviceMotionActive:检测当前 CMMotionManager 是否正在提供数据更新
if (![_motionManager isDeviceMotionActive] && [_motionManager isDeviceMotionAvailable]) {
@weakify(self);
[_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion * _Nullable motion,
NSError * _Nullable error) {
@strongify(self);
double gravityX = motion.gravity.x+ox;
double gravityY = motion.gravity.y+oy;

// 上、下偏移位移
CGFloat maximumYOffset = CRMotionDeviceMotionYLimit;
CGFloat minimumYOffset = -CRMotionDeviceMotionYLimit;

// 左、右偏移位移
CGFloat maximumXOffset = CRMotionDeviceMotionXLimit;
CGFloat minimumXOffset = -CRMotionDeviceMotionXLimit;

if (fabs(gravityX) >= CRMotionDeviceMotionMinimumTreshold) {
// 计算偏移量
CGFloat fOffsetX = CRMotionDeviceMotionXLimit*gravityX*xr;
if (gravityX<=0) {
fOffsetX = CRMotionDeviceMotionXLimit*gravityX*xl;
}
CGFloat fOffsetY = -CRMotionDeviceMotionYLimit*gravityY*yt;
if (gravityY<=0) {
fOffsetY = -CRMotionDeviceMotionYLimit*gravityY*yb;
}

if (fOffsetX > maximumXOffset) {
fOffsetX = maximumXOffset;
} else if (fOffsetX < minimumXOffset) {
fOffsetX = minimumXOffset;
}
if (fOffsetY > maximumYOffset) {
fOffsetY = maximumYOffset;
} else if (fOffsetY < minimumYOffset) {
fOffsetY = minimumYOffset;
}

CGFloat bOffsetX = -fOffsetX;
CGFloat bOffsetY = -fOffsetY;

if (self.bannerView.scrollView.transform.tx != fOffsetX || self.bannerView.scrollView.transform.ty != fOffsetY) {
self.bannerView.scrollView.transform = CGAffineTransformMakeTranslation(fOffsetX, fOffsetY);
self.backImageView.transform = CGAffineTransformMakeTranslation(bOffsetX, bOffsetY);
self.frontImageView.transform = CGAffineTransformMakeTranslation(bOffsetX,bOffsetY);
}
}
}];
} else {
self.bannerView.scrollView.transform = CGAffineTransformMakeTranslation(0, 0);
self.backImageView.transform = CGAffineTransformMakeTranslation(0, 0);
self.frontImageView.transform = CGAffineTransformMakeTranslation(0,0);
}
}

Android 实现

1.注册对应的传感器

1
2
3
4
5
6
7
8
mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
// 重力传感器
mAcceleSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// 地磁场传感器
mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

mSensorManager.registerListener(this, mAcceleSensor, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(this, mMagneticSensor, SensorManager.SENSOR_DELAY_GAME);

2.通过重力传感器和地磁场传感器,获取设备的偏转角度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mAcceleValues = event.values;
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mMageneticValues = event.values;
}

float[] values = new float[3];
float[] R = new float[9];
SensorManager.getRotationMatrix(R, null, mAcceleValues, mMageneticValues);
SensorManager.getOrientation(R, values);
// x轴的偏转角度
values[1] = (float) Math.toDegrees(values[1]);
// y轴的偏转角度
values[2] = (float) Math.toDegrees(values[2]);

3.根据偏转角度执行滑动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (mDegreeY <= 0 && mDegreeY > mDegreeYMin) {
hasChangeX = true;
scrollX = (int) (mDegreeY / Math.abs(mDegreeYMin) * mXMoveDistance*mDirection);
} else if (mDegreeY > 0 && mDegreeY < mDegreeYMax) {
hasChangeX = true;
scrollX = (int) (mDegreeY / Math.abs(mDegreeYMax) * mXMoveDistance*mDirection);
}
// mDegreeX:偏转角度 mDegreeXMin和mDegreeXMax为X轴可发生偏转位移的角度最大值和最小值
if (mDegreeX <= 0 && mDegreeX > mDegreeXMin) {
hasChangeY = true;
// mYMoveDistance: Y轴上的最大偏移距离
scrollY = (int) (mDegreeX / Math.abs(mDegreeXMin) * mYMoveDistance*mDirection);
} else if (mDegreeX > 0 && mDegreeX < mDegreeXMax) {
hasChangeY = true;
scrollY = (int) (mDegreeX / Math.abs(mDegreeXMax) * mYMoveDistance*mDirection);
}
smoothScrollTo(hasChangeX ? scrollX : mScroller.getFinalX(), hasChangeY ? scrollY : mScroller.getFinalY());
最近的文章

【小程序】小程序开发避坑指南

目录 include 导入的文件能否引用其他组件,生命周期等在哪儿 如何强制命中实验,命中不了实验是什么原因 扫预览码提示网络问题,请稍后再试 展现怎么打点?日志平台实时校验无数据 获取嵌套的组件元素rect获取不到 组件里设置的文本样式被父组件统一设置的文本样式覆盖 滚动到某一位置 滑动后不准确 …

小程序 阅读全文
更早的文章

【Flutter 开发】Flutter笔记之环境配置

Flutter笔记之环境配置 Android Studio 下载地址:https://developer.android.google.cn/studioFlutter SDK 下载地址:https://flutter.dev/docs/get-started/install/macos 配置Flu …

Flutter 阅读全文