实现原理
Android二维码扫描限制扫描区域的实现方式是通过修改相机预览图片截取灰度图的范围来实现限制扫描区域。相机预览尺寸因手机而不同,相机会选择一个相等的或相近的尺寸用来做预览尺寸,根据这个尺寸提取出预览图片的YUV数据。YUV数据区别于RGB数据,YUV数据将像素的灰度值(明亮度)与色值(色彩与饱和度)区分开,分别来存。我们的插件中使用的数据格式是YUV420格式。它的存储方式是先将每个像素的Y值(灰度值)存储起来,这样就是一个width * height大小的数组。另外再存储UV值,每四个像素共用一个U值和V值,所以U = V = Y / 4。最终获得的数组大小是width * height * 3 / 2。这样存储的好处是,即使只拿到了Y值也是可以渲染图片的,只不过渲染的是黑白图片。因为二维码是二维平面图片,只提取黑白颜色是能够识别二维码信息的。所以利用PlanarYUVLuminanceSource类将截取区域的X,Y,width,height值设置进去,在getMatrix(获取灰度图矩阵)时就会只获取这一区域的YUV数据。我们把X,Y,width,height值设置为二维码扫描框的大小和位置,这样在识别二维码时就只能识别扫描框中的二维码了。
iOS二维码扫描限制扫描区域是通过rectOfInterest方法来实现的,它的实现方式要比Android的方法更简单,因为iOS的原生代码支持这样的设置,我们只需要将参数传进去就可以了。但是一个需要特别注意的地方是坐标系发生了变换。常规的坐标系是左上角为原点,向右为X轴,向下为Y轴。而在iOS的rectOfInterest方法传入的参数是以右上角为原点,向左为X轴,向下为Y轴。这样X,Y,width,height四个值就要发生变换才可以。
X -> Y / SCREEN_HEIGHT
Y -> (SCREEN_WIDTH - X - 扫描框宽度)/ SCREEN_WIDTH
width -> 扫描框高度 / SCREEN_HEIGHT
height -> 扫描框宽度 / SCREEN_WIDTH
示例:self.output.rectOfInterest = CGRectMake(100 / (SCREEN_HEIGHT), (SCREEN_WIDTH - 100 - 200) / SCREEN_WIDTH, 100 / SCREEN_HEIGHT, 200 / SCREEN_WIDTH);//200 * 100的原点在(100,100)位置的扫描框
需要注意的是,设置扫描框区域范围时会受到屏幕宽高比设置的影响。一般我们的手机屏幕宽高比都是16:9的,插件中默认设置的是4:3,如果我们不相应的设置屏幕宽高比,则设置完扫描区域后会发现在Y轴方向上能够实现对扫描区域的控制,但是在X轴上则会比设置的值要宽一些,这应该是屏幕的预览效果在4:3的宽高比设置下被拉伸所引起的误差。所以需要设置合适的屏幕宽高比。
Android的设置方式是在js端设置属性ratio:'16:9'。全面屏的手机的比值一般为'2:1'。
iOS的设置方式是在js端设置属性defaultVideoQuality:RNCamera.Constants.VideoQuality["720p"]。
更多的设置方式参考react-native-camera官方文档
使用示例
<RNCamera
style={styles.preview}
ratio={'16:9'}
defaultVideoQuality={RNCamera.Constants.VideoQuality["720p"]}
scanAreaLimit={true}
scanAreaX={115}
scanAreaY={328}
scanAreaWidth={522}
scanAreaHeight={521}
flashMode={this.state.torchState == 'off' ? RNCamera.Constants.FlashMode.off : RNCamera.Constants.FlashMode.torch}
onBarCodeRead={this._onBarCodeRead.bind(this)}
permissionDialogTitle={'请求相机权限'}
permissionDialogMessage={'应用没有获取到相机权限,请先到设置中为应用开启相机权限'}
</RNCamera>
可以通过在View中用绝对布局画一个框来看一下这个扫描区域的范围。
<View style={{width: 522 * winWidth / 750, height: 521 * winHeight / 1334, position:'absolute', left: 115 * winWidth / 750, top: 328 * winHeight / 1334, borderWidth: 1, borderColor: 'red', borderStyle: 'solid', backgroundColor: 'transparent'}}>
</View>
插件代码修改
js端
RNCamera.js增加五个属性
属性名 | 类型 | 默认值 | 描述 |
---|---|---|---|
scanAreaLimit | boolean | false | 用来控制是否开启扫描区域限制 |
scanAreaX | int | 0 | 扫描区域原点X值 |
scanAreaY | int | 0 | 扫描区域原点Y值 |
scanAreaWidth | int | 0 | 扫描区域宽度 |
scanAreaHeight | int | 0 | 扫描区域高度 |
Android端
官方#1667号修改被还原
CameraViewManager.java
@ReactProp(name = "scanAreaLimit")
public void setScanAreaLimit(RNCameraView view, boolean scanAreaLimit) {
view.setScanAreaLimit(scanAreaLimit);
}
@ReactProp(name = "scanAreaX")
public void setScanAreaX(RNCameraView view, int scanAreaX) {
view.setScanAreaX(scanAreaX);
}
@ReactProp(name = "scanAreaY")
public void setScanAreaY(RNCameraView view, int scanAreaY) {
view.setScanAreaY(scanAreaY);
}
@ReactProp(name = "scanAreaWidth")
public void setScanAreaWidth(RNCameraView view, int scanAreaWidth) {
view.setScanAreaWidth(scanAreaWidth);
}
@ReactProp(name = "scanAreaHeight")
public void setScanAreaHeight(RNCameraView view, int scanAreaHeight) {
view.setScanAreaHeight(scanAreaHeight);
}
RNCameraView.java
//ScanAreaLimitParams
private boolean mScanAreaLimit = false;
private int mScanAreaX = 0;
private int mScanAreaY = 0;
private int mScanAreaWidth = 0;
private int mScanAreaHeight = 0;
public void setScanAreaLimit(boolean scanAreaLimit) {
this.mScanAreaLimit = scanAreaLimit;
}
public void setScanAreaX(int scanAreaX) {
this.mScanAreaX = scanAreaX;
}
public void setScanAreaY(int scanAreaY) {
this.mScanAreaY = scanAreaY;
}
public void setScanAreaWidth(int scanAreaWidth) {
this.mScanAreaWidth = scanAreaWidth;
}
public void setScanAreaHeight(int mScanAreaHeight) {
this.mScanAreaHeight = mScanAreaHeight;
}
/*...*/
new BarCodeScannerAsyncTask(delegate, mMultiFormatReader, correctData, correctWidth, correctHeight, mScanAreaLimit, mScanAreaX, mScanAreaY, mScanAreaWidth, mScanAreaHeight).execute();
/*...*/
BarCodeScannerAsyncTask.java(核心修改)
/*...*/
private final int COMMON_WIDTH = 750;
private final int COMMON_HEIGHT = 1334;
/*...*/
/*...*/
private BinaryBitmap generateBitmapFromImageData(byte[] imageData, int width, int height, boolean scanAreaLimit, int scanAreaX, int scanAreaY, int scanAreaWidth, int scaAreaHeight) {
if(scanAreaLimit == false) {
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
imageData, // byte[] yuvData
width, // int dataWidth
height, // int dataHeight
0, // int left
0, // int top
width, // int width
height, // int height
false // boolean reverseHorizontal
);
return new BinaryBitmap(new HybridBinarizer(source));
} else {
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
imageData, // byte[] yuvData
width, // int dataWidth
height, // int dataHeight
scanAreaX * width / COMMON_WIDTH, // int left
scanAreaY * height / COMMON_HEIGHT, // int top
scanAreaWidth * width / COMMON_WIDTH, // int width
scaAreaHeight * height / COMMON_HEIGHT, // int height
false // boolean reverseHorizontal
);
return new BinaryBitmap(new HybridBinarizer(source));
}
}
/*...*/
iOS端
RNCameraManager.m
RCT_CUSTOM_VIEW_PROPERTY(scanAreaLimit, BOOL, RNCamera)
{
[view setScanAreaLimit:[RCTConvert BOOL:json]];
}
RCT_CUSTOM_VIEW_PROPERTY(scanAreaX, NSInteger, RNCamera)
{
[view setScanAreaX:[RCTConvert NSInteger:json]];
}
RCT_CUSTOM_VIEW_PROPERTY(scanAreaY, NSInteger, RNCamera)
{
[view setScanAreaY:[RCTConvert NSInteger:json]];
}
RCT_CUSTOM_VIEW_PROPERTY(scanAreaWidth, NSInteger, RNCamera)
{
[view setScanAreaWidth:[RCTConvert NSInteger:json]];
}
RCT_CUSTOM_VIEW_PROPERTY(scanAreaHeight, NSInteger, RNCamera)
{
[view setScanAreaHeight:[RCTConvert NSInteger:json]];
}
RNCamera.h
#define SCREEN_WIDTH [UIApplication sharedApplication].delegate.window.frame.size.width
#define SCREEN_HEIGHT [UIApplication sharedApplication].delegate.window.frame.size.height
#define COMMON_WIDTH 750
#define COMMON_HEIGHT 1334
@property(assign, nonatomic) BOOL scanAreaLimit;
@property(assign, nonatomic) NSInteger scanAreaX;
@property(assign, nonatomic) NSInteger scanAreaY;
@property(assign, nonatomic) NSInteger scanAreaWidth;
@property(assign, nonatomic) NSInteger scanAreaHeight;
RNCamera.m(核心修改)
//在startSession方法中加入下段代码
/*...*/
if (self.scanAreaLimit) {
self.scanAreaX = self.scanAreaX * SCREEN_WIDTH / COMMON_WIDTH;
self.scanAreaY = self.scanAreaY * SCREEN_HEIGHT / COMMON_HEIGHT;
self.scanAreaWidth = self.scanAreaWidth * SCREEN_WIDTH / COMMON_WIDTH;
self.scanAreaHeight = self.scanAreaHeight * SCREEN_HEIGHT / COMMON_HEIGHT;
self.metadataOutput.rectOfInterest = CGRectMake((self.scanAreaY)/(SCREEN_HEIGHT), (SCREEN_WIDTH - self.scanAreaX - self.scanAreaWidth)/SCREEN_WIDTH, self.scanAreaHeight/SCREEN_HEIGHT, self.scanAreaWidth/SCREEN_WIDTH);
}
/*...*/