# React Native 人脸识别 UI 方案全对比:嵌入组件 · Activity · Dialog

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 传入 cameraLenscameraRotationmirrorrgbCameraIdnirCameraIdnirCameraRotationnirMirrorsearchThresholdsearchIntervalMs 等大量参数

适合场景

  • 需要人脸预览和 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 传 rgbCameraIdnirCameraIdcameraRotationmirror 等参数,native Dialog 内部自己打开 Camera、适配旋转、坐标系映射。

4. Bridge 只在结果时通信

不像嵌入组件需要每帧回调人脸坐标/状态码,Dialog 只在 onResultonCancelled 时回调一次 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 继承链经过 AppCompatActivityFragmentActivity,理论上可以接入。但实际收益有限------Dialog 方案已经足够简洁,引入 FragmentManager 反而增加了一层抽象复杂度。

理论上可以优化嵌入组件方案:用 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

附注 :项目中同时保留了 BaiduFaceSearchActivityBaiduFaceSearchDialog 两个类,两者的核心逻辑(checkAndProcess()、相机管理、坐标映射、活体检测)几乎完全一致。真正差异只有两处:(1)结果返回方式------Activity 走 setResult + finish + onActivityResult,Dialog 走 callback + dismiss;(2)过渡动画------Activity 有系统转场,Dialog 无。当前生产环境走 Dialog,Activity 保留作为备选方案。
附注2 :自助点餐机项目(self-ordering-machine)使用的 ArcFaceSearchDialog 采用 CameraXProcessCameraProvider + PreviewView)而非 Camera1,生命周期管理更优雅。两个项目虽用不同推SDK和相机 API,但在架构层面独立收敛到了同一个 Dialog 方案。


结论

人脸识别支付场景,原生 Dialog 是最佳选择。它不是"能用"的方案,而是"用户感觉不到识别环节存在"的方案------这才是好的支付体验。

嵌入组件虽然灵活,但付出的维护成本(硬件加速黑屏、Bridge 回调性能、生命周期竞态)远超收益。Activity 在过渡动画上的断裂感在支付场景中是致命的。

Dialog 方案的核心优势只有一条:RN 页面始终在下层完整渲染,Dialog 只是临时遮罩。这个特性让整个流程的 UI 过渡完全由你掌控,而不是被 Android 系统动画牵着走。

相关推荐
沙漠1 天前
ReactNative总结系列四 --- FlatList白屏卡顿优化
react native·性能优化
wordbaby3 天前
rn-cross-calendar:一个兼容 React 18/19、RN/RNOH 的跨平台日历组件
前端·react native·harmonyos
沙漠3 天前
ReactNative总结系列三 --- 性能优化
react native·性能优化
leeyi4 天前
Graph 编排:不只是 ReAct 的通用 DAG
react native·agent·graphql
不爱吃糖的程序媛4 天前
React Native 三方库 react-native-version-number 鸿蒙适配实战:从零到版本信息展示
react native·react.js·harmonyos
Dragon Wu4 天前
React Native 配置自定义字体
react native·react.js
不爱吃糖的程序媛5 天前
小白实战手记:React Native 应用部署到鸿蒙设备全流程详解
react native·鸿蒙
不爱吃糖的程序媛5 天前
React Native 三方库 react-native-share 的 HarmonyOS 适配实战
react native·react.js·harmonyos
不爱吃糖的程序媛5 天前
React Native 应用适配鸿蒙PC 实战:从白屏到成功运行
react native·react.js·harmonyos