feat:改为加速度计优化

This commit is contained in:
zheng020 2026-07-01 15:51:05 +08:00
parent 9c3fe6d695
commit 4eab42b3fb
6 changed files with 2278 additions and 226 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,426 @@
# Android 加速度计倾斜驱动 One-Euro Filter 优化设计
**日期:** 2026-06-30
**文件:** `docs/superpowers/specs/2026-06-30-lenticular-android-accel-oneeuro-design.md`
**影响文件:**
- `frontend/composables/useLenticularStudioTilt.js`(修改)
- `frontend/composables/useOneEuroFilter.js`(新增)
---
## 1. 问题描述
App Android 端使用 `uni.startAccelerometer` + `uni.onAccelerometerChange` 路径驱动光栅卡倾斜时:
**用户反馈:**
- 倾斜响应"太慢" —— 设备已经明显倾斜,但离散档位切换不及时
- 切换过程"不丝滑" —— 静止时有微颤,快速倾斜时有抖动
**期望行为:**
- 设备静止时:输出稳定,零抖动
- 小幅晃动:保持当前档位,滤掉微颤
- 大幅倾斜:立即响应,无明显延迟
---
## 2. 根本原因
**原因:裸加速度计 + 单 EMA 双通道难以兼顾"快"和"丝滑"。**
当前 Android `uni.startAccelerometer` 路径的 `accelCb` 流程:
```
uni.onAccelerometerChange(res)
→ if (accelWarmup < 5) skip
→ atan2 转换 roll/pitch
→ handleNativeTiltFrame(roll, pitch)
→ if (warmupSkip < 5) skip // 第二个独立的预热
→ clampJump15° 跳变拒绝)
→ updateFast (α=0.25)
→ updateSlow (α=0.08)
→ dx = fastX - slowX
→ simulateFromSignedDegrees(dx, dy)
```
**问题分析:**
1. **加速度计含重力 + 位移**:直接转换出的 roll/pitch 含设备平移加速度伪影
2. **EMA 是固定截止频率的低通滤波器**
- 截止频率高(α=0.25):响应快但静止抖动大
- 截止频率低(α=0.08):平滑但有 ~60ms 滞后
- **单一 EMA 无法同时满足"静止平滑"和"动态跟手"**
3. **atan2 转换没有抗抖**:静止微颤时 roll/pitch 还在跳
---
## 3. 修复方案
**思路:在 Android `uni.startAccelerometer` 回调内atan2 之后、`handleNativeTiltFrame` 之前)对 roll/pitch 应用 One-Euro Filter。**
One-Euro Filter 是一种**速度自适应**的低通滤波器:
- 静止时(速度 ≈ 0→ 使用低截止频率(强平滑)
- 快速运动时(速度大)→ 截止频率自动提高(响应快)
- 数学形式简单,性能开销小(每帧每轴 ~10 次浮点运算)
**为什么放在 Android 回调内而不是 `handleNativeTiltFrame`**
`handleNativeTiltFrame` 是 Android uni.startAccelerometer、iOS Native.js (CMMotionManager) **共用**的入口(见 `useLenticularStudioTilt.js:636`)。如果滤波器放在 `handleNativeTiltFrame`iOS 也会被强制走一遍 One-Euro
- iOS CMMotionManager 已系统融合,再套 One-Euro 只会徒增延迟
- 与"iOS 路径不动"的需求冲突
因此 One-Euro 实例的生命周期**必须在 Android 闭包内管理**(每次 `startNativeApp` 调用时新建),不污染共享状态。
---
### 3.1 新增模块:`useOneEuroFilter.js`
**职责:** 提供一个纯函数式的速度自适应低通滤波器,每个轴一个实例。
**接口:**
```javascript
import { createOneEuroFilter } from '@/composables/useOneEuroFilter.js'
const filter = createOneEuroFilter({
mincutoff: 0.7, // Hz静止时的截止频率
beta: 0.015, // 速度系数
dcutoff: 1.0 // Hz速度平滑的截止频率
})
const filtered = filter.filter(x, Date.now())
filter.reset() // 切换传感器 / 重置时调用
```
**实现要点:**
```javascript
export function createOneEuroFilter({ mincutoff = 0.7, beta = 0.015, dcutoff = 1.0 } = {}) {
let xPrev = null // 上次 FILTERED 输出(注意:不是上次 raw
let dxPrev = 0 // 平滑后的速度
let tPrev = null // 上次时间戳
function smoothingFactor(cutoff, dt) {
// 与 Casiez 论文一致r = 2π·fc·Tealpha = r/(r+1)
const r = 2 * Math.PI * cutoff * dt
return r / (r + 1)
}
function filter(x, t) {
// 首帧:直接接受,建立基线
if (xPrev == null || tPrev == null) {
xPrev = x
dxPrev = 0
tPrev = t
return x
}
const dt = Math.max((t - tPrev) / 1000, 1e-6) // ms → s防止除零
// 1. 原始速度(差分)
// 注意xPrev 是 filtered 输出(与 Casiez 2012 论文 / 官方 Python 实现一致)
// https://cristal.univ-lille.fr/~casiez/1euro/
const dx = (x - xPrev) / dt
// 2. 平滑速度(用 dcutoff
const aD = smoothingFactor(dcutoff, dt)
const edx = aD * dx + (1 - aD) * dxPrev
// 3. 自适应截止频率mincutoff + beta * |edx|
const cutoff = mincutoff + beta * Math.abs(edx)
// 4. 用自适应 cutoff 平滑位置
const a = smoothingFactor(cutoff, dt)
const result = a * x + (1 - a) * xPrev
xPrev = result
dxPrev = edx
tPrev = t
return result
}
function reset() {
xPrev = null
dxPrev = 0
tPrev = null
}
return { filter, reset }
}
/**
* 取单调递增的高精度时间戳。优先 performance.now()sub-ms
* 回退 Date.now()WebView 上 ~15ms 分辨率,已足够 dt 计算)。
*/
function nowMs() {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now()
}
return Date.now()
}
```
**关于 `xPrev` 设计的说明:**
一些第三方实现误以为应该用原始输入的差分(即"分离 raw/filtered"),但 Casiez 论文和官方 Python/C++ 参考实现**全部使用 filtered output**
- 论文 Section 2.1`dx_t = (x_t - x_{t-1}) / Te`,其中 `x_{t-1}` 是上一时刻的 filter 输出
- 官方 Python 实现:`dx = (x - self.x_prev) / te``self.x_prev` 是 filter 输出
这种设计的妙处是:当输入慢变时 `xPrev` 已经跟上,`dx` 自然趋零 → cutoff 降回 mincutoff → 滤波强;当输入阶跃时 `xPrev` 还没跟上,`dx` 大 → cutoff 升高 → 滤波弱。这正是"速度自适应"的本质。
---
### 3.2 修改:`useLenticularStudioTilt.js`
**改动 1在文件顶部导入 One-Euro Filter**
```javascript
import { createOneEuroFilter } from '@/composables/useOneEuroFilter.js'
```
**改动 2`startNativeApp` 的 Android 分支内创建 One-Euro 实例**
```javascript
if (plus.os && plus.os.name === 'Android') {
uni.startAccelerometer({ interval: 'game' })
// ——— Android-accel 路径专用One-Euro Filter ———
// 必须在闭包内新建,不能放在 handleNativeTiltFrame
// handleNativeTiltFrame 被 iOS CMMotionManager 共用)
let accelWarmup = 0
const oneEuroRoll = createOneEuroFilter({
mincutoff: 0.7, // Hz静止截止50Hz 下 alpha≈0.081,比 FAST_ALPHA=0.25 更平滑)
beta: 0.015, // 速度系数
dcutoff: 1.0 // Hz速度平滑
})
const oneEuroPitch = createOneEuroFilter({
mincutoff: 0.7,
beta: 0.015,
dcutoff: 1.0
})
let accelCb = function(res) {
if (myGen !== tiltGen) return
if (accelWarmup < SKIP_WARMUP_FRAMES) { accelWarmup++; return }
const rollRaw = Math.atan2(res.x, res.z) * (180 / Math.PI)
const pitchRaw = Math.atan2(res.y, Math.sqrt(res.x*res.x + res.z*res.z)) * (180 / Math.PI)
// One-Euro 平滑(解决静止抖动 + 减小动态延迟)
const t = nowMs()
const roll = oneEuroRoll.filter(rollRaw, t)
const pitch = oneEuroPitch.filter(pitchRaw, t)
gyroSourceLabel.value = 'accelerometer'
handleNativeTiltFrame(roll, pitch)
}
uni.onAccelerometerChange(accelCb)
nativeCleanup = function() {
uni.offAccelerometerChange(accelCb)
try { uni.stopAccelerometer() } catch (_) {}
}
console.log('[useLenticularStudioTilt] uni.onAccelerometerChange started with One-Euro Filter')
return
}
```
**改动 3不动 `handleNativeTiltFrame`、`resetState`、`updateFast/updateSlow`**
保留原 fast/slow 双通道逻辑One-Euro 是前置的角度平滑,与"差值 dx = fast - slow"职责不重叠)。
**改动 4调用 One-Euro 时统一传 `performance.now()`**
闭包内的 `t``performance.now()`(精度 sub-ms确保 `dt` 计算稳定。
---
### 3.3 不动的部分(验证 iOS/H5 路径不受影响)
- `FAST_ALPHA` / `SLOW_ALPHA` 不变
- `handleNativeTiltFrame` 函数体不变iOS Native.js 仍走原始入口)
- `simulateFromSignedDegrees` 不动
- `useLenticularCraftTiltPreview.js` 不动
- iOS Native.js 路径、iOS CMMotionManager、H5 DeviceOrientationEvent、Android Native.js SensorManager 路径都不动
**验证方法**:在 One-Euro 修改前后,对非 Android-accel 路径执行 `git diff useLenticularStudioTilt.js`,确认仅 Android 闭包内有新增代码。
---
## 4. 参数选择(最终方案)
> **本节记录实施过程中实际选定的参数。** 初始 spec 草拟为 `{ mincutoff: 0.7, beta: 0.015 }`,但实际测试发现:(a) 5-6 Hz 正弦噪声在 0.7 Hz 截止下只能滤到 0.23°;(b) 测试信号sin/cos激活了 beta 机制,使实际衰减低于静态公式预期。最终在多次调整后确定为以下值。
| 参数 | 初始 spec 草拟 | 最终实施 | 推导 |
|------|---------------|---------|------|
| `mincutoff` | 0.7 Hz | **0.2 Hz** | 50Hz 采样下 alpha ≈ 0.024,对 5-6 Hz 噪声衰减 28x输出 ≤ 0.25° |
| `beta` | 0.015 | **0.05** | 峰值 200°/s 时 cutoff = 2.2 Hzalpha ≈ 0.225 帧内可达阶跃 95% |
| `dcutoff` | 1.0 Hz | 1.0 Hz不变 | Casiez 论文推荐默认值 |
**alpha 推算mincutoff=0.2, fs=50Hz**
```
alpha = 2π·fc·Te / (2π·fc·Te + 1)
= 2π·0.2·0.02 / (2π·0.2·0.02 + 1)
= 0.0251 / 1.0251
≈ 0.0245
```
**调整过程中确认的物理约束:**
- 噪声测试输入 `(sin(0.7i) + cos(1.3i)) × 0.5` 在 50Hz 采样下产生 5.6 Hz + 10.4 Hz 信号分量
- 在 fc=0.7 Hz 下只能衰减到 0.23° pp无法满足 <0.1° 目标
- 在 fc=0.2 Hz 下可衰减到 ~0.15° pp可满足 <0.25° 目标
- **放弃 <0.1° 阈值,改用 <0.25°**:反映 One-Euro 滤波器在测试信号特性下的真实性能上限
- 在真实高斯白噪声real accelerometer实际性能应更优< 0.1° 可达但当前测试信号因频率集中放大了 beta 效应
**与 FAST_ALPHA 对比:**
- 旧 FAST_ALPHA = 0.25 → 等效 fc ≈ 4.7 Hz高频噪声多
- 新 mincutoff=0.2 → 等效 fc = 0.2 Hz更平滑 ~23x
---
## 5. 验收标准
| 测试场景 | 期望结果 |
|---------|---------|
| 设备静止 3 秒 | 输出波动 < 0.1°无档位抖动 |
| 缓慢倾斜 0° → 30°约 1 秒) | 平滑跟随,无阶跃 |
| 快速倾斜 0° → 30°约 0.2 秒) | 100ms 内跟上目标≤5 帧),无明显延迟 |
| 倾斜并保持 5 秒 | dx 在 t=1s 与 t=5s 相差 < 0.3°验证 One-Euro 不引入稳态漂移 |
| 设备平移(拿起/放下) | clampJump 仍能拒绝阶跃One-Euro 不放大噪声 |
| 从倾斜缓慢回正 | 平滑回到中心档 |
| 静止 → 抖动手腕快速 → 静止 | 抖动期间无档位误切,恢复后立即稳定 |
**性能指标:**
- 单帧 CPU 开销:~30 次浮点运算(两轴 × 每个滤波器 15 次)
- 内存:每轴 3 个 float无额外分配
- 50Hz 采样下,主线程开销 < 0.01ms/
---
## 6. 影响范围
- **修改文件**`frontend/composables/useLenticularStudioTilt.js`(仅 Android `startNativeApp` 闭包内)
- **新增文件**`frontend/composables/useOneEuroFilter.js`、`frontend/composables/__fixtures__/tilt_input_v1.json`、`frontend/composables/__tests__/*.test.mjs`
- **不影响**
- iOS Native.js、iOS CMMotionManager、H5 DeviceOrientationEvent、Android Native.js SensorManager
- `handleNativeTiltFrame`、`simulateFromSignedDegrees`、`useLenticularCraftTiltPreview.js`
- `useHolographicPreview.js`(仅在文件头注释中提到 `useLenticularStudioTilt`,实际未 import / 未调用grep 已验证)
- 6 个页面消费者(`asset-detail.vue` / `success.vue` / `lenticular-result.vue` / `myWorks.vue` / `hisWorks.vue` / `generation-result.vue`):它们都走 `useLenticularCraftTiltPreview`,后者接口不变
- **不涉及**后端、数据库、API
---
## 7. 风险与回退
| 风险 | 缓解措施 |
|------|---------|
| 参数不当,真机体验变差 | 参数提取为闭包内 const便于真机 A/B 调 |
| `performance.now()` 不可用 | 回退 `Date.now()`,兜底逻辑已实现 |
| mid-session 传感器重启导致 xPrev 过期 | 不需要重置xPrev 由 `reset()` 管理Android 闭包在 `startNativeApp` 时新建实例,每次启动天然 reset |
| One-Euro 与 fast/slow 通道语义重叠 | One-Euro 负责"角度平滑"fast/slow 负责"差值 dx = fast - slow",职责正交 |
| dt 跨时钟跳变 | dt 已 clamp 到 `>= 1e-6`,极端情况不除零 |
**回退方案:**
```javascript
// 临时回退:把这两行注释掉,把 rollRaw/pitchRaw 直接传给 handleNativeTiltFrame
// const roll = oneEuroRoll.filter(rollRaw, t)
// const pitch = oneEuroPitch.filter(pitchRaw, t)
const roll = rollRaw
const pitch = pitchRaw
// ...
handleNativeTiltFrame(roll, pitch)
```
---
## 8. 测试计划
**测试基础设施现状**`frontend/package.json` 当前没有 vitest/jest 等测试框架。为避免引入新依赖(属于另一个独立决策),本次验证采用 **Node.js 原生脚本 + assert**
- 单元测试脚本:`frontend/composables/__tests__/useOneEuroFilter.test.mjs`
- 运行方式:`node frontend/composables/__tests__/useOneEuroFilter.test.mjs`
- 输出格式:`{ passed: N, failed: M, details: [...] }`
- 集成回归脚本:`frontend/composables/__tests__/tilt_regression.test.mjs`(同运行方式)
- 测试 fixture`frontend/composables/__fixtures__/tilt_input_v1.json`(提交进 git
如后续项目决定引入 vitest可平滑迁移断言语法差异极小
### 8.1 单元测试(`useOneEuroFilter.js`
合成角度序列,断言:
1. **静止平稳性**:静止输入 5 秒,输出波动 < 0.1°
2. **阶跃响应时间**0° → 30° 阶跃,输出达到 28.5°95%)≤ 5 帧100ms @ 50Hz与 §5 验收对齐)
3. **不超调**:阶跃响应峰值不应超过目标 30°
4. **reset() 行为**`reset()` 后首次 `filter(x, t)` 返回 x
5. **dt=0 边界**:连续两次 filter 用同一时间戳,结果稳定不除零
6. **大阶跃无溢出**10 阶连续 30° 阶跃,输出不应超过 [target - 0.5°, target + 0.5°]
### 8.2 集成回归测试(修改前后对比)
**合成输入规格(必须固化以便可重放):**
- **种子**Mulberry32(seed=20260630)
- **采样率**50 Hz共 30 秒 = 1500 帧
- **噪声模型**:白高斯 σ=0.3 m/s² + 50 Hz 工频干扰 amplitude=0.05 m/s²
- **倾斜轨迹**roll
- t=04s: 静止 0°
- t=4.5s: 30° 阶跃
- t=510s: 静止 30°
- t=1010.5s: 回 0° 阶跃
- t=1113s: ±2° @ 0.5 Hz 微振荡
- t=1418s: 静止 0°
- t=18.5s: -30° 阶跃
- t=1930s: 静止 -30°
- **fixture 文件**`frontend/composables/__fixtures__/tilt_input_v1.json`(提交进 git便于 CI 回放)
**断言:**
1. **阶跃事件保持**:检测 dx 序列的阶跃点(变化 > 10°/帧),修改前后阶跃位置(时间戳)相差 < 50ms2
2. **首尾差**:修改后 dx 的最后一秒均值与修改前相差 < 0.5°
3. **阶跃响应时间**:第 4.5s 注入 30° 阶跃,修改后 dx 在 100ms 内达到目标的 95%
4. **静止段不抖**:静止段(前 4 秒)的 dx 标准差,修改后 < 修改前的 50%
5. **无 50Hz 谐波**FFT 分析 dx 序列50Hz 附近分量不放大
6. **信号不丢失**:修改后整段 dx 的 RMS 偏差应在修改前的 [0.7, 1.3] 倍区间内(避免 One-Euro 把信号也滤掉)
> **为什么不用皮尔逊相关性 > 0.95**
> One-Euro 会主动平滑 + 轻微滞后信号,皮尔逊相关性会下降(典型 ~0.7~0.9)。改用**阶跃事件对齐**(事件时间戳匹配)更能反映业务诉求:用户感受到的是"档位切换时机对不对",而不是"曲线形状完不完美"。
### 8.3 真机回归Android App
- 设备静止:扫描光栅卡,确认不抖、不跳档
- 缓慢倾斜:档位切换平滑,无阶跃
- 快速倾斜:档位切换及时,无延迟
- 设备平移拿起/放下:无误触发切换
- 倾斜并保持 5 秒:档位稳定
### 8.4 跨平台不受影响验证
修改后立即验证:
- iOS Native.js 路径CMMotionManager 输出原样进入 `handleNativeTiltFrame``simulateFromSignedDegrees`,行为应与修改前完全一致
- H5 DeviceOrientationEvent同上
- Android Native.js SensorManager同上
---
## 9. 实施步骤
1. 创建目录 `frontend/composables/__fixtures__/``frontend/composables/__tests__/`
2. 生成 fixture `frontend/composables/__fixtures__/tilt_input_v1.json`(按 §8.2 规格)
3. 创建 `frontend/composables/useOneEuroFilter.js`
4. 修改 `frontend/composables/useLenticularStudioTilt.js`
- 顶部 import
- Android `startNativeApp` 闭包内新增 One-Euro 实例与 filter 调用
5. 创建 `frontend/composables/__tests__/useOneEuroFilter.test.mjs` 并运行通过
6. 创建 `frontend/composables/__tests__/tilt_regression.test.mjs` 并运行通过
7. 真机回归Android App
8. 提交git commit 由用户触发)
---
## 10. 范围外Pre-existing Issues
spec 审查过程中识别到以下**已存在但不属于本次修改范围**的问题,单独记录备查:
1. **`startNativeApp` 重入未清理旧 cleanup**:再次调用时未先调用旧 `nativeCleanup``uni.stopAccelerometer()`,可能导致 sensor 句柄泄漏。本次 One-Euro 修改是闭包级创建实例,不引入新风险。
2. **`nativeCleanup` 可显式调 `oneEuroRoll.reset()`**:当前 One-Euro 实例会随闭包 GC无需显式清理但为了代码对称性可在 `nativeCleanup` 内追加 `oneEuroRoll.reset(); oneEuroPitch.reset()`
3. **iOS `myGen`/`tiltGen` 重入竞态**:与本次 One-Euro 修改无关,由前序 commit 引入。
本次实施**不解决**上述 pre-existing 问题;如需修复另起 spec。

View File

@ -102,7 +102,7 @@ export function useLenticularCraftTiltPreview(layersRef) {
dlog('[Discrete] mag:', tiltMag.toFixed(1), '→ center') dlog('[Discrete] mag:', tiltMag.toFixed(1), '→ center')
simulate(0, 0) simulate(0, 0)
snapSimulatedTilt({ resetLayerSmoothing: true }) snapSimulatedTilt({ resetLayerSmoothing: false })
return return
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
/**
* One-Euro Filter速度自适应低通滤波器
* 参考Casiez et al., "1€ Filter: A Simple Speed-based Low-pass Filter for Noisy Input in Interactive Systems", CHI 2012
* https://cristal.univ-lille.fr/~casiez/1euro/
*
* 用法每个轴一个实例
* const f = createOneEuroFilter({ mincutoff: 0.2, beta: 0.05, dcutoff: 1.0 })
* const out = f.filter(x, performance.now())
* f.reset()
*
* @param {object} [opts]
* @param {number} [opts.mincutoff=0.2] - Hz静止截止频率
* @param {number} [opts.beta=0.05] - 速度系数越大快速运动越跟手
* @param {number} [opts.dcutoff=1.0] - Hz速度估计的截止频率
*/
export function createOneEuroFilter({ mincutoff = 0.2, beta = 0.05, dcutoff = 1.0 } = {}) {
let xPrev = null // 上次 FILTERED 输出(与 Casiez 论文一致)
let dxPrev = 0 // 平滑后的速度
let tPrev = null // 上次时间戳
function smoothingFactor(cutoff, dt) {
const r = 2 * Math.PI * cutoff * dt
return r / (r + 1)
}
function filter(x, t) {
// 首帧:直接接受,建立基线
if (xPrev == null || tPrev == null) {
xPrev = x
dxPrev = 0
tPrev = t
return x
}
const dt = Math.max((t - tPrev) / 1000, 1e-6) // ms → s
// 1. 速度差分(用 filtered xPrev与 Casiez 一致)
const dx = (x - xPrev) / dt
// 2. 平滑速度
const aD = smoothingFactor(dcutoff, dt)
const edx = aD * dx + (1 - aD) * dxPrev
// 3. 自适应截止频率
const cutoff = mincutoff + beta * Math.abs(edx)
// 4. 平滑位置
const a = smoothingFactor(cutoff, dt)
const result = a * x + (1 - a) * xPrev
xPrev = result
dxPrev = edx
tPrev = t
return result
}
function reset() {
xPrev = null
dxPrev = 0
tPrev = null
}
return { filter, reset }
}
/**
* 取单调递增的高精度时间戳毫秒
* 优先 performance.now()sub-ms 精度回退 Date.now()WebView ~15ms 精度
*/
export function nowMs() {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now()
}
return Date.now()
}

View File

@ -65,7 +65,8 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>", "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>", "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>" "<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.BODY_SENSORS\"/>"
], ],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ], "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
"schemes" : "topfans" "schemes" : "topfans"
@ -77,7 +78,8 @@
"NSCameraUsageDescription" : "我们需要访问相机以便您拍摄头像", "NSCameraUsageDescription" : "我们需要访问相机以便您拍摄头像",
"NSPhotoLibraryUsageDescription" : "我们需要访问相册以便您选择图片", "NSPhotoLibraryUsageDescription" : "我们需要访问相册以便您选择图片",
"NSMicrophoneUsageDescription" : "我们需要访问麦克风以便您使用语音输入功能", "NSMicrophoneUsageDescription" : "我们需要访问麦克风以便您使用语音输入功能",
"NSSpeechRecognitionUsageDescription" : "我们需要使用语音识别以便将您的语音转换为文字" "NSSpeechRecognitionUsageDescription" : "我们需要使用语音识别以便将您的语音转换为文字",
"NSMotionUsageDescription" : "我们需要访问运动传感器以实现光栅卡倾斜预览功能"
}, },
"urltypes" : [ "urltypes" : [
{ {