上个月接了一个安卓端的物流扫码项目,需求听起来特别简单:把扫码功能做成一个页面。但需求方越说越细——扫描框要贴合UI稿、扫完不能直接退出、还要支持相册选图识别、同一个画面里多个码要能分别点选。于是我认真比较了一圈开源方案,最终在项目里集成了华为HMS Scan Kit,实现了完全自定义的扫码能力。这篇文章就是那次完整接入的记录,从依赖配置、相机权限、自定义扫描框绘制,到结果解析、相册识别、各种坑的排查,一次讲清楚。
无论你是刚接触安卓扫码开发,还是已经在用ZXing想换一个更省心的方案,这篇文章都值得花五分钟看完。
1. 为什么最终选择用Scan Kit做自定义扫码
1.1 默认扫码页与自定义页面的差距
华为Scan Kit给开发者的第一印象是“开箱即用”,因为官方提供了一个默认扫码页面,调用方式也简单:
ScanUtil.startScan(activity, REQUEST_CODE_SCAN, HmsScan.QRCODE_SCAN_TYPE);然后在onActivityResult里拿结果就行。这套默认页确实能用,扫码框、闪光灯、相册入口都齐了,但它的问题是:界面是固定的。你不能改扫码框的形状,不能把顶部标题换成自己的品牌栏,也不能在扫码中页里加自己的业务按钮。
而实际项目里,“能扫出来”远远不够。我那个物流项目,业务方要求扫码页顶部放一张配送员头像,底部放“手动输入”按钮,扫码框要设计成四个圆角的蓝色线段,识别成功后还要震动并弹出气泡。这些需求用默认页完全没法做,必须把预览层和识别引擎拆开用。
Scan Kit在这里做得比较聪明:默认页只是它提供的一个快捷入口,底层识别能力通过HmsScanAnalyzer暴露,开发者可以自己搭布局,把相机预览画面放到任意位置,再叠加自定义扫描框。识别引擎本身仍然由华为SDK托管,不需要自己去处理图像算法。
1.2 与ZXing、ZBar、ML Kit等方案的对比
选型的时候我也没打算一上来就定华为Scan Kit,先把市面上常用方案过了一遍。
| 方案 | 优点 | 缺点 |
|---|---|---|
| ZXing | 开源成熟、中文资料多 | 定制界面要改到怀疑人生,维护已经半停滞 |
| ZBar | 历史久、识别快 | 多年不维护,对现代安卓适配差 |
| Google ML Kit | 识别类型多、API友好 | 依赖Google Play服务,国内设备识别能力不稳定 |
| 华为Scan Kit | 离线识别、格式全、自定义能力强 | 依赖HMS Core,非华为设备需要引导安装 |
ZXing大家很熟了,玩安卓的基本都用过。它的问题不是不能用,而是定制成本高。眼睛、条码管理器、相机配置耦合在一起,改一个扫描框线宽要翻半天源码。ZBar就不用多说了,社区基本没什么更新,遇到Android 13、14的适配问题只能自己硬扛。Google ML Kit我一开始挺想用,毕竟接口干净,但国内网络环境你是知道的,模型和依赖下载偶尔会出幺蛾子,而且部分国产ROM对Google Play服务的支持并不理想。
华为Scan Kit最打动我的是离线识别和格式全。二维码、EAN系列、UPC码、Code 128、PDF417、DataMatrix这些常用格式全覆盖,官方宣称支持20多种。我在真机(华为P系列和一台小米)上实测,普通二维码识别大约是几十毫秒,基本没有延迟感。另外它对中文内容、反色码、残缺码都有专门优化,这些正是物流单上常见的恶路况。
缺点也很明显:依赖HMS Core。你可以在启动时判断服务是否存在,如果不存在,可以拉起安装引导或者降级到ZXing方案。考虑到国内华为/荣耀设备覆盖量很大,这个代价可以接受。
2. 工程接入与权限申请:最容易翻车的环节
2.1 Gradle依赖与HMS Core服务检查
接入第一步是配置华为Maven仓库。在项目根目录的build.gradle里加上:
allprojects { repositories { google() maven { url 'https://developer.huawei.com/repo/' } } }然后在应用模块的build.gradle中引入Scan Kit SDK:
dependencies { // Scan Kit 基础能力,含自定义识别 implementation 'com.huawei.hms:scan:2.4.0.300' }我用的版本是2.4.0.300,现在华为开发者网站有更新版本,API大体一致,旧项目升级也不需要伤筋动骨。
依赖加完,很多人以为就能直接跑了,结果程序一启动就Crash,常见的报错是找不到HMS Core SDK类。原因很直白:扫码SDK需要依赖Huawei Mobile Services(HMS Core)运行,如果你的设备上没装,或者装的是旧版,就会初始化失败。
所以接入后的第一件事,是在进入扫码页前做服务可用性检查:
int result = HuaweiMobileServicesUtil.isHuaweiMobileServicesAvailable(context); if (result == ConnectionResult.SUCCESS) { // 可以正常启动扫码 } else { // 提示用户安装或更新HMS Core // 也可以降级到ZXing备用方案 }这一步千万别省。我当时在一台已经刷过机、没有GMS也没有HMS的测试机上跑,直接被这个坑卡了半天,因为没有检查服务,整个页面启动即崩。
2.2 相机权限和存储权限的运行时申请
自定义扫码需要相机权限,这个所有扫码方案都绕不开。Android 6.0以后要动态申请,不能再只在Manifest里写uses-permission就完事。
在AndroidManifest.xml里声明:
<uses-permission android:name="android.permission.CAMERA" />然后在进入扫码页前动态申请:
private void requestCameraPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_CODE); } else { startScanEngine(); } }这里有个听上去耸人听闻但其实很常见的崩溃:如果相机权限没拿到,直接调用LensEngine.run(),会抛出IllegalStateException,页面直接闪退。所以权限回调里也要做好保护:
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CAMERA_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startScanEngine(); } else { Toast.makeText(this, "需要相机权限才能扫码", Toast.LENGTH_SHORT).show(); } } }至于存储权限,我的建议是:尽量别碰。如果只是做相册选图识别,直接用系统自带的照片选择器(Photo Picker),不需要申请READ_EXTERNAL_STORAGE或READ_MEDIA_IMAGES,既省事又符合现在安卓的隐私设计趋势。后面讲相册识别的时候我再细说。
2.3 Theme和Activity配置的必要细节
扫码页Activity不需要特殊主题,普通App主题就能跑。但有一项必须设置:屏幕方向。
在Manifest里锁定竖屏:
<activity android:name=".ScanActivity" android:screenOrientation="portrait" android:configChanges="orientation|screenSize|keyboardHidden" />为什么强调这个?因为Scan Kit的相机预览方向和扫码框角点坐标,默认是建立在竖屏逻辑下的。如果你不锁定方向,手机一转屏,相机传感器坐标和屏幕坐标之间的映射关系就会错乱。轻则扫码框与画面错位,重则识别结果里的角点信息完全对不上。我见过有人为了“适配横屏”折腾了一周,后来直接用portrait锁死,项目顺利上线。如果你的产品经理要求必须支持横屏扫码,建议单独评估方向映射的工程量,不要硬扛。
3. 自定义扫码页面的核心实现:预览、识别与绘制
3.1 LensEnginePreview的基本用法
自定义扫码页面的核心是LensEnginePreview。它是Scan Kit提供的一个专用View,内部封装了TextureView和相机生命周期管理。我们只需要把它放到布局最底层,再在上面叠加自己的扫描框View。
布局文件大致这样:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000"> <com.huawei.hms.mlsdk.common.LensEnginePreview android:id="@+id/preview" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.yourpackage.widget.ScanFrameView android:id="@+id/frame_view" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 顶部业务栏 / 底部按钮 随便加 --> </FrameLayout>LensEnginePreview这个View很关键,它会把相机画面按设备屏幕比例做一个缩放居中显示,不会像普通SurfaceView那样直接拉伸变形。但前提是父布局需要一个和Activity同尺寸的容器,FrameLayout就够。
3.2 HmsScanAnalyzer选项配置与扫描类型
识别引擎是HmsScanAnalyzer,它决定你扫什么码、怎么扫。创建代码如下:
HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions.Creator() .setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE, HmsScan.EAN13_SCAN_TYPE) .setMultiMode(true) .setResultType(HmsScan.RESULT_TYPE_ALL) .create(); HmsScanAnalyzer analyzer = new HmsScanAnalyzer(options);几个参数我解释一下:
setHmsScanTypes:指定要识别的码制。如果你只需要二维码,就只传QRCODE_SCAN_TYPE;如果还需要条码,就传EAN13_SCAN_TYPE、CODE128_SCAN_TYPE等。识别类型越少,单帧处理越快。如果不传,默认识别全部类型,方便但会增加耗时。setMultiMode(true):开启多码模式。同一帧画面里出现多个二维码时,它会都返回给你。如果不开,它只随机返回其中一个码的结果。setResultType:决定返回的结果字段。用RESULT_TYPE_ALL可以获得原始值、码类型、边框坐标等完整信息。
创建完Analyzer,下一步创建LensEngine。LensEngine才是真正驱动相机一帧一帧往Analyzer里送数据的引擎:
LensEngine lensEngine = new LensEngine.Factory(context, analyzer) .setLensType(LensEngine.BACK_LENS) .applyDisplayDimension(1280, 720) .applyFPS(25) .enableAutomaticFocus(true) .create();这里重点说下applyDisplayDimension(1280, 720)。这个值代表相机预览分辨率,不是越高越好。预览分辨率过高,会占用大量CPU带宽,识别不会因为分辨率翻倍而变快,甚至可能更慢。720p对普通大小的码已经绰绰有余,物流面单这种大尺寸条码,1080p和720p几乎没有区别。如果你扫的是特别小的码,可以调到1920x1080再对比观察。
3.3 自定义扫描框View的绘制思路
很多人以为自定义扫码就是找个贴图盖在预览上,其实最核心的是扫描框View的绘制。我习惯用一个单独的ScanFrameView,继承View,重写onDraw。
扫描框视觉上通常包含两部分:半透明遮罩和四角亮线。
半透明遮罩的实现思路是:把整个屏幕涂一层半透明黑,然后“挖”出中间扫码框区域。很多新手会直接往画布上画四个矩形,这种画法在刘海屏、圆角屏上容易出现边缘漏色,而且还不好维护。更好的方式是用Path加EVEN_ODD填充规则:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); if (frameRect == null) { int frameWidth = (int) (width * 0.75f); int frameHeight = (int) (height * 0.45f); int left = (width - frameWidth) / 2; int top = (height - frameHeight) / 2; frameRect = new Rect(left, top, left + frameWidth, top + frameHeight); } // 半透明遮罩 Path maskPath = new Path(); maskPath.addRect(0, 0, width, height, Path.Direction.CW); maskPath.addRect(frameRect, Path.Direction.CW); maskPaint.setColor(Color.parseColor("#66000000")); maskPaint.setStyle(Paint.Style.FILL); canvas.drawPath(maskPath, maskPaint); // 四角亮线 drawCorners(canvas); }用EVEN_ODD或Path.Direction.CW加两个矩形的方法,能保证中间区域完全透明,外面统一变暗。视觉上扫起来非常干净,也不会因为屏幕比例不同出现奇怪的白边。
四角亮线的绘制就是画四条线段,线段端点带圆头。颜色可以用品牌色,宽度建议dp转像素,不要写死px,否则不同分辨率的手机上粗细不一致。
扫描框的尺寸设计也讲究:二维码建议正方,边长为屏幕宽度的0.7倍左右;一维条码建议成扁长矩形,宽度拉满屏幕的0.85倍,高度不用太高。物流项目里我做了两种模式,切到“条码模式”时扫描框自动变长,体验很好。
3.4 生命周期管理:start、stop、release三步缺一不可
扫码页的生命周期,是新手最容易踩坑的地方。LensEngine不是一次性的,它对应真实相机资源,必须在正确的时机启动和关闭。
我的标准模板:
@Override protected void onResume() { super.onResume(); if (hasCameraPermission()) { lensEngine.run(preview); } } @Override protected void onPause() { super.onPause(); if (lensEngine != null) { lensEngine.stop(); } closeTorchIfNeeded(); } @Override protected void onDestroy() { super.onDestroy(); if (lensEngine != null) { lensEngine.release(); } if (analyzer != null) { analyzer.close(); } }run必须在onResume里做,因为只有页面可见时相机才能安全打开。stop必须在onPause里做,否则切换到后台时相机还开着,其他App扫描就全是黑屏,有些系统甚至会直接给你发“相机被占用”的警告。release和close放在onDestroy,释放相机和识别引擎资源。
另外注意:Transactor的onAnalyze回调跑在子线程,如果你想在识别成功后更新UI(比如把扫码框变成绿色、弹窗提示),必须runOnUiThread或者用Handler.post,直接操作View会抛CalledFromWrongThreadException。
4. 识别结果的后处理:从原始数据到业务字段
4.1 HmsScan对象里到底装了哪些字段
当Analyzer识别到码后,会回调Transactor.onAnalyze(List<HmsScan> results)。每个HmsScan对象包含以下关键信息:
String originalValue = hmsScan.getOriginalValue(); int scanType = hmsScan.getScanType(); int scanTypeForm = hmsScan.getScanTypeForm(); List<Point> borderVertices = hmsScan.getBorderVertices(); int zoomValue = hmsScan.getZoomValue(); int angleValue = hmsScan.getAngleValue();getOriginalValue():最常见的,就是码内的字符串。比如二维码里存了一个JSON字符串,或一个URL。getScanType():返回码制,比如HmsScan.QRCODE_SCAN_TYPE、HmsScan.CODE128_SCAN_TYPE。getBorderVertices():返回码的四个顶点坐标。这个坐标是相机预览画面内的坐标,可以用来在界面上画出真实码的位置。getZoomValue():条码模式下,一些模糊条码需要缩放才能识别,这个值表示识别时使用的缩放倍数。getAngleValue():识别方向修正角度。
我通常会写一个简单的结果解析类,按业务前缀区分:
private void handleScanResult(HmsScan hmsScan) { String value = hmsScan.getOriginalValue(); if (TextUtils.isEmpty(value)) { return; } if (value.startsWith("LGS:")) { // 物流单号 parseLogisticsNo(value); } else if (value.startsWith("{")) { // JSON格式业务数据 parseJson(value); } else { // 普通文本/链接 rawResult = value; } }扫码不是扫出来就完事,后面接的是一大坨业务逻辑。原始值拿到手先别急着弹Toast,先判断格式,再走业务流程。
4.2 同一帧多码识别的场景处理
默认情况下,Scan Kit一帧画面里如果出现多个码,只返回其中一个。在物流场景里,一个托盘上可能同时贴着三四张面单,用户想扫哪张是用户的事,所以我们要开启多码模式。
开启方式前面已经说了:setMultiMode(true)。开启后,一次回调可能返回多个HmsScan对象,同时画面里每个码的borderVertices都在。
但多码模式带来一个新的问题:同一个码会在连续几十帧里反复回调,如果不做处理,会出现“连续弹窗三十次”的尴尬情况。
我的做法是维护一个“已处理码值集合”,并用时间戳限制:
private final Set<String> handledValues = new HashSet<>(); private long lastHandleTime; private void handleMultiScan(List<HmsScan> results) { long now = System.currentTimeMillis(); if (now - lastHandleTime < 1500) { return; } for (HmsScan scan : results) { String value = scan.getOriginalValue(); if (!handledValues.contains(value)) { handledValues.add(value); lastHandleTime = now; handleScanResult(scan); break; } } if (handledValues.size() > 100) { handledValues.clear(); } }4.3 识别横竖屏与相机方向的坑
这部分是真正的进阶内容,也是很多扫码方案之间拉开差距的地方。
相机传感器的“上”和屏幕的“上”通常不是同一个方向。即使Activity锁定竖屏,相机输出的画面仍然是横向的,Scan Kit内部做了转换,但HmsScan返回的borderVertices坐标是基于相机画面的,不一定直接等于屏幕坐标。
如果你只是弹出结果,不画框,那这个坑不明显。但如果你要做“识别到码后在码周围画一个框”这种体验,就必须做坐标方向映射。
我的经验是:不要自己拿三角函数硬算。安卓上可以用Matrix封装旋转和缩放,然后把相机坐标点顺序映射到View坐标。比如竖屏情况下,通常需要把相机坐标旋转90度,再做一个镜像判断(前置/后置不同)。
这个环节没法给一个万能公式,因为每台设备的相机方向和预览尺寸组合都可能不同。最简单可靠的方案是:先跑一个Demo,扫一张纸上的二维码,把识别到的borderVertices直接画到屏幕上,对比实际位置,看偏转多少,再写映射。一步到位的人很少,调试两三次就通了。
5. 相册选图识别的补充方案
5.1 用Photo Picker选图再扔给analyzeInBackground
除了相机实时扫码,业务上还要支持“相册识别”。比如用户收到一个快递单图片,可以直接从相册导入识别单号,不用再拿摄像头去照屏幕。
安卓上现在打开相册的首选方案是ActivityResultContracts.GetContent(),它不需要存储权限,也能自动适配各版本。
private final ActivityResultLauncher<String> pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> { if (uri != null) { decodeAndAnalyze(uri); } }); private void openPhotoPicker() { pickImageLauncher.launch("image/*"); }拿到Uri后,不能直接BitmapFactory.decodeFile(uri.getPath()),因为相册图片的分辨率动辄4000x3000,直接解码极容易OOM。先采样压缩:
private void decodeAndAnalyze(Uri uri) { try { ContentResolver resolver = getContentResolver(); BitmapFactory.Options boundsOptions = new BitmapFactory.Options(); boundsOptions.inJustDecodeBounds = true; InputStream boundsStream = resolver.openInputStream(uri); BitmapFactory.decodeStream(boundsStream, null, boundsOptions); if (boundsStream != null) { boundsStream.close(); } BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); decodeOptions.inSampleSize = calculateInSampleSize(boundsOptions, 2048, 2048); InputStream decodeStream = resolver.openInputStream(uri); Bitmap bitmap = BitmapFactory.decodeStream(decodeStream, null, decodeOptions); if (decodeStream != null) { decodeStream.close(); } analyzeBitmap(bitmap); } catch (IOException e) { e.printStackTrace(); } }calculateInSampleSize是标准写法,网上很多种,核心逻辑就是让图片长边不超过2048像素。这个尺寸对扫码识别已经足够,内存开销也小。
压缩完后,用HmsScanAnalyzer.analyzeInBackground(bitmap)异步识别:
private void analyzeBitmap(Bitmap bitmap) { HmsScanAnalyzer analyzer = new HmsScanAnalyzer( new HmsScanAnalyzerOptions.Creator() .setHmsScanTypes(HmsScan.ALL_SCAN_TYPE) .setMultiMode(false) .create()); Task<List<HmsScan>> task = analyzer.analyzeInBackground(bitmap); task.addOnSuccessListener(scanList -> { if (scanList != null && !scanList.isEmpty()) { handleScanResult(scanList.get(0)); } else { Log.d("ScanKit", "图片里没有识别到码"); } analyzer.close(); }); task.addOnFailureListener(e -> { Log.e("ScanKit", "图片识别失败", e); analyzer.close(); }); }注意:无论成功失败,都要记得关闭analyzer,避免资源泄漏。
5.2 大图内存优化与EXIF旋转修正
相册识别的大图问题,代码已经写过一遍,这里再说两个容易被忽略的细节。
第一个细节:BitmapFactory.Options.inSampleSize必须是2的幂次方,比如2、4、8、16。如果你写3,框架会向下取整到2,不会报错,但计算会不精确。我的calculateInSampleSize实现里会把它规范化。
第二个细节:EXIF方向信息。很多手机拍出来的照片,实际像素矩阵是横向的,只有靠EXIF里的Orientation标记才能旋转正确。如果你不处理,二维码图片显示是正的,但底层Bitmap可能是旋转了90度的,识别引擎对旋转并不是完全无感,尤其是模糊一点的码。
处理方式:
ExifInterface exif = new ExifInterface(inputStream); int orientation = exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); Matrix matrix = new Matrix(); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: matrix.postRotate(90); break; case ExifInterface.ORIENTATION_ROTATE_180: matrix.postRotate(180); break; case ExifInterface.ORIENTATION_ROTATE_270: matrix.postRotate(270); break; default: break; } Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);注意这里创建了新Bitmap,用完后记得recycle(),否则内存涨得也很快。对了,从analyzeInBackground返回后,原始Bitmap和旋转后的Bitmap都不再需要了,要在处理完结果后主动回收,或者交给GC前先recycle。
6. 我在实际项目里踩过的坑和最终建议
6.1 异常场景和解决方案一览
列一个排查表,里面对应的都是在项目里真遇到过的:
| 异常现象 | 根本原因 | 解决方案 |
|---|---|---|
| 启动扫码页黑屏并闪退 | 相机权限未动态申请或未实现权限回调 | 在onResume前检查权限,权限回调后再run |
| 在非华为设备上一进页面就崩溃 | 设备没有安装HMS Core或者版本过旧 | 启动前调HuaweiMobileServicesUtil.isHuaweiMobileServicesAvailable |
| 识别速度慢 | 预览分辨率设太高/识别类型全开 | 优先用1280x720,只配置需要的码制 |
| 识别到码但界面不刷新 | onAnalyze跑在子线程 | 用runOnUiThread更新UI |
| 扫码框和实际码位置错位 | 相机坐标方向映射没做 | 单独调试borderVertices,补Matrix旋转 |
| 切后台再回来自动扫码失效 | onPause没有stop,相机资源泄漏 | 严格按照onResume/onPause管理LensEngine |
| 相册大图OOM | 直接解码原图 | 先用inJustDecodeBounds采样,再解码 |
| 同一个码识别完又弹窗 | 没有去重 | 维护已处理码值集合,加时间戳限制 |
这里面最容易被忽视的是“非华为设备依赖HMS Core”那条。现在的用户手机五花八门,有鸿蒙、有MIUI、有ColorOS,虽然大部分国产机都能装HMS Core,但你不能假设所有设备预置了它。所以启动检查一定要做,而且要写好引导弹窗。
6.2 体验优化的几个小细节
扫码页的体验,往往决定了整个App给人的第一感觉,我觉得有几个细节值得单独强调。
第一个是扫描框动画。很多App的扫码框上下会有一条“激光线”扫描动画,实现起来不复杂,一个ValueAnimator从扫描框top动到bottom,再反向回来即可。动画时长建议1500到2000毫秒,太快会干扰用户注意力,太慢显得死板。
第二个是震动反馈。识别成功时用Vibrator震一下,哪怕只有几十毫秒,用户也能立刻感知“扫中了”。注意Android 13以后要用VibratorManager获取实例,老版本用Vibrator,别写错。
第三个是手电筒开关。Scan Kit没有直接给手电筒API,但可以通过系统摄像头服务控制。用CameraManager.getCameraCharacteristics找闪光灯,setTorchMode开关。不过在onPause里记得把手电筒关掉,不然切后台摄像头还在发热。
private void toggleTorch(boolean enable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { String cameraId = manager.getCameraIdList()[0]; manager.setTorchMode(cameraId, enable); } catch (Exception e) { e.printStackTrace(); } } }第四个是关于连续扫码。如果你的场景是“连续扫很多单”,扫码成功后不要关闭页面,而是把结果加到列表里,画一个对勾动画,然后继续扫描。这时handledValues集合要能自动清空,否则第二张同样的单号会被误判为重复。
还有一个体验细节:扫码页面底部一定会放“手动输入”入口。这个入口在物流场景里不是备选项,因为快递面单有时候真的会磨损、污损,扫不出来是常事。相册识别、手动输入、相机扫码三件事并行,用户才不会有“这个App好蠢”的抱怨。
整体用下来,华为Scan Kit的自定义能力确实比预期强。官方文档里“自定义扫码页”那一节其实写得很平淡,但你真正去用的时候会发现,它把相机采集、图像分析、结果回调完全拆开了,每一层都能按自己的需求替换。其实“自定义扫码”的难点从来不在识别算法,而在画面渲染、生命周期和权限这些工程细节上。把这篇文章里提到的权限、方向、去重、内存优化这几个坑全部绕开,你的自定义扫码功能就已经超过市面上很多扫码App的水准了。