给运动手表做绑定功能,最自然的交互就是扫机身上那个二维码。听起来是个一天能搞完的活,实际做下来在原生配置上卡了大半天,Android 编译报 missingDimensionStrategy 相关的错,权限给了相机还是黑屏,扫到码之后弹框弹了三四次。这些坑一个都不在业务代码里。
这篇把当时用 react-native-camera 做扫码页的完整过程拆开讲,从原生工程配置、权限声明,一路讲到遮罩布局、扫描线动画和重复扫码的去抖处理。更多详情可以看官方文档 https://github.com/react-native-community/react-native-camera 。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 一个扫码页到底要解决哪几件事,为什么它比看上去麻烦
react-native-camera的原生配置,missingDimensionStrategy那一行是干什么的AndroidManifest.xml里哪些权限是扫码必需的,哪些是顺带声明的- 用四块半透明遮罩围出中间的扫码区,这种布局怎么搭
- 用
Animated做上下往复的扫描线,以及动画循环的写法 - 扫到码之后为什么会连续触发多次,怎么做去抖
- 取景框
ViewFinder组件的封装,四个角是怎么画出来的 - 这个库现在已经归档了,社区转向了什么
先看做出来的效果。

# 一、扫码页要解决的四件事
动手之前先把要做的事列清楚,代码结构就不会乱。
第一件是把相机预览铺满屏幕。react-native-camera 提供的 RNCamera 组件本身就是一个 View,给它设宽高就能显示预览画面。
第二件是识别二维码。这个库内置了条码识别能力,只要指定要识别的码制类型,识别到就通过回调把结果给你。这一步几乎不用自己写逻辑。
第三件是画界面。相机预览是铺满的,但用户需要一个明确的「把码放这里」的视觉引导。常见做法是在预览上盖一层半透明黑色遮罩,中间挖一个方形的洞,洞里画四个角的取景框,再加一条上下移动的扫描线。这部分是工作量最大的。
第四件是处理扫码结果。识别是持续进行的,同一个码在一秒内会被识别很多次,直接弹框会弹一堆。要做去重和状态控制。
四件事里,第三第四件才是真正花时间的地方。
# 二、装包和原生配置
装完包之后要把原生模块链接进工程。
react-native link react-native-camera
这条命令会自动修改 Android 和 iOS 的原生工程文件,把库的代码加进构建流程。需要说明的是,react-native link 这套手动链接机制在后来的版本里已经被自动链接(autolinking)取代了,装完包直接重新编译就行,不需要再执行 link。老项目升级上来的时候如果两套机制并存,反而会因为重复注册报错。
接着改 Android 的构建配置。
// 配置andriod/app/src/build.gradle
defaultConfig {
applicationId "com.jtyapps"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
// 添加这里
missingDimensionStrategy 'react-native-camera', 'general'
}
// 配置andriod/gradle/wrapper/gradle-wrapper.properties
// 本教程使用的是这个版本
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
missingDimensionStrategy 这一行值得解释一下,因为不加它编译一定报错,而报错信息又看不出所以然。
react-native-camera 这个库在内部定义了一个叫 react-native-camera 的构建维度(flavor dimension),下面有两个取值。一个是 general,用的是 Android 自带的条码识别;另一个是 mlkit,接的是 Google 的机器学习套件,识别更强但要引入 Google Play 服务。Gradle 在合并依赖时发现你的主工程没有声明这个维度,就不知道该选哪个变体,于是直接报错。missingDimensionStrategy 的作用就是告诉它「我这边没这个维度,你就按 general 来」。
国内发行的应用一般选 general,因为 mlkit 依赖 Google Play 服务,国行手机上大多没有。
abiFilters 那两行是限制打包进 APK 的 CPU 架构。只留 armeabi-v7a 和 x86 能显著减小包体积,但要注意 arm64-v8a 被排除掉了,在 64 位设备上会以兼容模式运行 32 位库。这在当年问题不大,现在部分应用市场对 64 位支持是有硬性要求的,实际项目里得按上架要求来配。
gradle-4.10.1 这个版本是当时的选择,现在肯定要跟着 Android Gradle Plugin 的要求走,以官方文档为准。
# 三、权限清单
权限声明在 andriod/app/src/main/AndroidManifest.xml 里。
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.front" android:required="false"/>
这一长串里,扫码真正需要的只有三条。CAMERA 是相机权限,缺了它 RNCamera 会一直黑屏。VIBRATE 用来在扫码成功时震一下,给用户一个明确的反馈。两条 uses-feature 声明相机是可选特性,required="false" 很重要,写成 true 的话没有摄像头的设备在应用市场里会直接看不到你的应用。
剩下那些是这个项目里其他功能带的。蓝牙和定位是心率带那部分需要的(这块的完整实现在React Native 蓝牙连接心率带设备里),网络状态是断网检测用的,存储权限是保存图片用的。
顺带修一处原文里的拼写错误。原来写的是 READ_EXTERNAL_STORAGEE,末尾多了一个 E,正确的常量名是 READ_EXTERNAL_STORAGE。上面代码块里我已经改过来了。这个错误的表现是权限静默失效,Android 不认识这个字符串就当没声明,运行时申请这个权限会一直失败,很容易查半天以为是机型问题。
另外要提醒的是,Android 6.0 之后 CAMERA 属于危险权限,光在清单里声明不够,运行时还得动态申请。原代码里用的是 RNCamera 组件自带的 permissionDialogTitle 和 permissionDialogMessage 两个属性,由组件内部去申请。这两个属性在后续版本里被 androidCameraPermissionOptions 这类新写法替代了,具体以你用的版本文档为准。
iOS 那边不用配这么多,只要在 Info.plist 里加上 NSCameraUsageDescription 说明用途,文案会显示在系统的权限弹框里,审核也会看这句话写得是否合理。
# 四、扫码页的整体结构
先看渲染部分的骨架。
<View style={styles.allContainer}>
<RNCamera
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
onBarCodeRead={this.barcodeReceived.bind(this)}
onCameraReady={() => { console.log('ready') }}
permissionDialogTitle={'Permission to use camera'}
permissionDialogMessage={'We need your permission to use your camera phone'}
style={styles.cameraStyle}
>
{/* 顶部导航条(半透明黑) */}
{/* 中间留白(半透明黑) */}
{/* 扫码行:左遮罩 + 取景框 + 右遮罩 */}
{/* 底部提示(半透明黑) */}
</RNCamera>
</View>
RNCamera 铺满全屏,所有 UI 都作为它的子元素叠在预览画面之上。这里的关键点是 barCodeTypes,只声明 qr 一种码制。识别的码制越少性能越好,如果还要扫条形码就把对应的类型加进数组。
onBarCodeRead 是识别回调,识别到就调用,参数里的 e.data 是解析出来的字符串内容。