React Native 人脸识别 UI 方案全对比:嵌入组件 · Activity · Dialog
基于双屏柜台点餐机 和 自助点餐机两个真实项目的实践经验总结。
背景
在 Android 上做人脸识别支付,核心流程是:
刷卡页 → 人脸识别 → 查询用户信息 → 订单确认 → 支付
其中"人脸识别"这一步需要在 RN 页面上叠加摄像头预览 + 人脸检测框 + 识别状态提示。有三种主流实现方式:
| 方案 | 简述 |
|---|---|
| 嵌入组件 | 原生 CameraView 封装成 RN 组件,直接嵌入 RN 布局 |
| Activity | 新开一个全屏原生 Activity,通过 onActivityResult 返回结果 |
| Dialog | 在当前 Activity 上弹出全屏透明 Dialog,dismiss 后 RN 页面仍在下方 |
一、嵌入组件(FaceAICameraView)
原理
jsx
// RN 侧
<PaymentModal>
<View style={styles.faceCircleContainer}>
<FaceAICameraView
cameraLens={cameraLens}
cameraRotation={cameraRotation}
mirror={cameraMirror}
rgbCameraId={rgbCameraId}
nirCameraId={nirCameraId}
searchThreshold={0.9}
isDetecting={isCameraReady}
onVerifyMatched={handleFaceVerifyMatched}
/>
</View>
<Text>{faceTip}</Text>
</PaymentModal>
原生 CameraX/TextureView 通过 ViewManager 注册为 React 组件,在 JSX 布局中占据一块区域。
优点
- 与 RN 布局完全融合,可以做圆角裁剪、动画叠加、手势交互
- 无需额外页面跳转,状态管理都在同一个组件树里
- 用户体验有潜力做到"无感知切换"
缺点
- Android 硬件加速冲突 :TextureView 在
borderRadius或硬件图层下会黑屏/白屏,需要手动管理图层策略 - 生命周期复杂:RN 组件 mount/unmount 对应相机 open/close,状态竞争容易出现"相机未就绪就先开始检测"
- 旋转/镜像/坐标系:摄像头方向、预览旋转、人脸坐标映射全部要从 JS 层传参协调,bug 频发
- 性能开销:JS ↔ Native bridge 每帧传人脸坐标/状态码,高频回调容易掉帧
- props 膨胀 :需要从 JS 传入
cameraLens、cameraRotation、mirror、rgbCameraId、nirCameraId、nirCameraRotation、nirMirror、searchThreshold、searchIntervalMs等大量参数
适合场景
- 需要人脸预览和 RN UI 深度融合(如 AR 特效、动画蒙层)
- 人脸识别是页面的一部分而非独立流程
- 有充足的开发和调试时间处理兼容问题
二、原生 Activity
原理
kotlin
// Native Module
@ReactMethod
fun startFaceSearch(cameraLens: Int, promise: Promise) {
val intent = Intent(currentActivity, FaceSearchActivity::class.java)
intent.putExtra("cameraLens", cameraLens)
currentActivity.startActivityForResult(intent, REQUEST_CODE)
}
js
// RN 侧
const result = await BaiduFace.startFaceSearch(0);
// Activity 关闭后拿到结果
handleResult(result);
启动一个新的全屏 Activity,识别完成后 finish() 并通过 onActivityResult 回调返回。
优点
- 生命周期完全独立,不受 RN 组件树影响
- 相机管理简单,Activity 级
onResume/onPause直接对应相机开/关 - Bridge 只在结果返回时通信一次(与 Dialog 方案一致)
缺点
- 过渡动画断层:Activity 的进入/退出动画是系统级的,无法和 RN Modal 的淡入淡出完美衔接
- 背景不可见:Activity 盖住整个 App,用户看不到之前的 UI 上下文(如刷卡页)
- 结果回传链路长 :
onActivityResult→ Promise resolve → JS 回调,出错时难以定位 - 返回键处理不一致:物理返回键关闭 Activity vs 关闭 RN Modal,用户困惑
- 不支持透明背景:Activity 类天然无法做成半透明叠加效果
适合场景
- 人脸识别是完全独立的流程(如门禁、考勤)
- 不需要保留背景 UI 上下文
- 对过渡动画要求不高
三、原生 Dialog(推荐)
原理
kotlin
@ReactMethod
fun startFaceSearch(cameraLens: Int, checkLiveness: Boolean, promise: Promise) {
currentActivity.runOnUiThread {
val dialog = BaiduFaceSearchDialog(
currentActivity,
cameraLens = cameraLens,
checkLiveness = checkLiveness
)
dialog.onResult = { faceId, similarity, image, liveness ->
promise.resolve(resultMap)
}
dialog.onCancelled = {
promise.reject("CANCELLED", "User cancelled")
}
dialog.show()
}
}
kotlin
// Dialog 初始化
window?.apply {
setLayout(MATCH_PARENT, MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) // 透明背景
setDimAmount(0.0f) // 不透出 RN Modal 的 dim,复用底层遮罩
setCanceledOnTouchOutside(false)
}
在当前 Activity 上弹出一个全屏透明 Dialog,dismiss 时 RN 页面原封不动还在下方。
优势一览
scss
┌─────────────────────────────────────────────┐
│ RN 页面(始终渲染,不被销毁) │
│ ┌─────────────────────────────────────┐ │
│ │ PaymentModal (半透明遮罩) │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ 原生 Dialog(透明背景) │ │ │
│ │ │ ┌─────┐ │ │ │
│ │ │ │ 🎥 │ 人脸预览 + 框 + 提示│ │ │
│ │ │ └─────┘ │ │ │
│ │ └───────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
优点
1. 无过渡动画,丝滑
Dialog 是 show() / dismiss(),没有 Activity 的系统转场动画。用户从刷卡页切人脸时,底层 RN Modal 遮罩始终存在,Dialog 打开后直接显示在遮罩上方,视觉完全连续。
scss
嵌入模式:点击人脸 → RN setState → 等待相机就绪 → 开始识别(有白屏/黑屏风险)
Activity: 点击人脸 → 系统转场动画(约0.3s)→ Activity 出现 → 等待相机启动 → 识别 → finish → 又一个转场动画
Dialog: 点击人脸 → show()(无转场动画)→ 等待相机启动 → 识别 → dismiss()(无转场动画)
注意:Dialog 省掉的是系统 Activity 转场动画的开销,但相机 open + surface ready + 首帧到达这个链路本身仍需数百毫秒(实测首次 1.2s 左右,后续 600-900ms)。三者在"相机启动延迟"上没有本质差别。
2. 过渡 UI 天然支持
Dialog 打开前 RN 可以做过渡 UI:
scss
SWIPE_CARD → FACE_PAY(过渡头像 + 呼吸圆环 + "正在启动人脸识别...")
→ Dialog.show() → 识别成功
→ Dialog.dismiss() → FACE_LOADING(头像 + 动画 + "正在查询用户...")
→ 订单确认
整个过程高度可控,用户看到的是连续的动画而非页面跳转。
3. 相机管理完全在 native 层
不需要从 JS 传 rgbCameraId、nirCameraId、cameraRotation、mirror 等参数,native Dialog 内部自己打开 Camera、适配旋转、坐标系映射。
4. Bridge 只在结果时通信
不像嵌入组件需要每帧回调人脸坐标/状态码,Dialog 只在 onResult 或 onCancelled 时回调一次 Promise。与 Activity 方案的 onActivityResult 本质相同,都是一次性回调------真正的优势在于省掉了嵌入组件的高频帧事件,而不是比 Activity 更优。
5. 关闭/取消逻辑自然
用户点关闭/切换刷卡 → dialog.cancel() → Promise reject CANCELLED → JS 收到 → setPaymentIdentityStep('SWIPE_CARD')。不涉及 Activity 的返回栈管理。
6. 生命周期简单
Dialog 的 onStart/onStop 对应生命周期,不依赖 RN 组件树 mount/unmount。
缺点
- 无法与 RN 组件做复杂交互动画(但人脸识别场景不需要)
- Dialog 尺寸/位置固定为全屏,不能像嵌入组件那样任意裁切
- 需要 Android 原生的 XML 布局能力
适合场景
- 需要流畅过渡的人脸识别支付流程
- 人脸识别是流程中的一环,需要保留背景 UI
- 追求最佳的用户感知体验
方案对比总结
| 维度 | 嵌入组件 | Activity | Dialog |
|---|---|---|---|
| 过渡流畅度 | ★★☆ | ★☆☆ | ★★★ |
| 实现复杂度 | ★☆☆ 高 | ★★☆ | ★★☆ |
| Bridge 开销 | 高(每帧事件) | 低(一次回调) | 低(一次回调) |
| 相机管理 | JS 侧传参 | Native 自管 | Native 自管 |
| UI 融合度 | ★★★ | ★☆☆ | ★★☆ |
| 生命周期风险 | 高 | 中 | 低 |
| 返回栈干扰 | 无 | 有 | 无 |
| 调试难度 | 高 | ★★☆ | ★★☆ |
有没有比 Dialog 更好的方案?(结论:没有)
1. Fragment 方案(Android 原生)
用 DialogFragment 替代 Dialog,生命周期管理更标准化(由 FragmentManager 管理),支持 setMaxLifecycle 精确控制。新版 React Native 的 ReactActivity 继承链经过 AppCompatActivity → FragmentActivity,理论上可以接入。但实际收益有限------Dialog 方案已经足够简洁,引入 FragmentManager 反而增加了一层抽象复杂度。
2. FullScreen Modal with Native View(嵌入组件优化)
理论上可以优化嵌入组件方案:用 overflow: visible + 绝对定位避免 hardware layer 冲突,但这只是补丁。核心问题(Bridge 开销、坐标系映射、生命周期竞态)依然存在。
3. 当前最优解:Dialog + 模拟模式
推荐方案就是现在用的:
markdown
生产环境:原生 Dialog
└─ BaiduFaceSearchDialog → 双目摄像头 → 活体检测 → 1:N 搜索
开发/测试:模拟模式
└─ __DEV__ && MOCK_MODE → 跳过 Dialog → 直接返回 MOCK_PER_001
这个组合兼顾了:
- 生产性能:原生 Dialog,零 Bridge 开销,丝滑过渡
- 开发效率:模拟模式在不插摄像头的设备上也能跑通完整支付流程
- 可维护性 :JS 侧只调
searchFace()一个函数,内部根据环境决定走 Dialog 还是 mock
附注 :项目中同时保留了
BaiduFaceSearchActivity和BaiduFaceSearchDialog两个类,两者的核心逻辑(checkAndProcess()、相机管理、坐标映射、活体检测)几乎完全一致。真正差异只有两处:(1)结果返回方式------Activity 走setResult + finish + onActivityResult,Dialog 走callback + dismiss;(2)过渡动画------Activity 有系统转场,Dialog 无。当前生产环境走 Dialog,Activity 保留作为备选方案。
附注2 :自助点餐机项目(self-ordering-machine)使用的ArcFaceSearchDialog采用CameraX(ProcessCameraProvider+PreviewView)而非Camera1,生命周期管理更优雅。两个项目虽用不同推SDK和相机 API,但在架构层面独立收敛到了同一个 Dialog 方案。
结论
人脸识别支付场景,原生 Dialog 是最佳选择。它不是"能用"的方案,而是"用户感觉不到识别环节存在"的方案------这才是好的支付体验。
嵌入组件虽然灵活,但付出的维护成本(硬件加速黑屏、Bridge 回调性能、生命周期竞态)远超收益。Activity 在过渡动画上的断裂感在支付场景中是致命的。
Dialog 方案的核心优势只有一条:RN 页面始终在下层完整渲染,Dialog 只是临时遮罩。这个特性让整个流程的 UI 过渡完全由你掌控,而不是被 Android 系统动画牵着走。