Android开发之路程序员工具类

如何封装一个可扩展的Android二维码扫描框架

2019-03-16  本文已影响436人  JamFF

Android二维码扫描的解决方案,都是基于Google的zxing,网上一些主流的开源库,主要分为有两种方式。

  1. android-zxingLibrary,自定义控件方式,优点集成方便,缺点是过度封装,在兼容Android版本上存在问题,例如开启摄像头、闪光灯API变更时,或者UI定制化时,需要修改源码,不方便。
  2. ZXingProject,在zxing基础上进行精简,缺点是没有封装,Activity、UI、业务逻辑、资源文件耦合性高,代码移植不便。

功能分析

  1. ZXingProject 的基础上,封装一个二维码扫描的 jar 包。
  2. 摄像头扫描二维码
  3. 相册扫描二维码
  4. 闪光灯的开启关闭

其中摄像头、相册、闪光灯等 API 等调用放在项目中,不去污染 Library 库,使 Library 具有更强的适用性。

封装 Library

1. 分析现有工程

先看下 ZXingProject 的项目结构。

2. 移除 Library 冗余代码

下面是移除后的代码结构。



移除 CaptureActivity 后,CaptureActivityHandler 和 DecodeHandler 相关代码会报错,新增 Constants ,CaptureCallback 两个接口。

ZXingProject 中 Handler 的 what 是控件 id,这里使用 Constants 中定义的常量替代 。

public interface Constants {

    int DECODE = 10001;
    int DECODE_FAILED = 10002;
    int DECODE_SUCCEEDED = 10003;
    int QUIT = 10004;
    int RESTART_PREVIEW = 10005;
    int RETURN_SCAN_RESULT = 10006;
}

使用接口 CaptureCallback 解藕 ZXingProject 中的 CaptureActivity。

public interface CaptureCallback {

    Rect getCropRect();// 获取矩形

    Handler getHandler();// 获取Handler

    CameraManager getCameraManager();// 获取CameraManager

    /**
     * 扫码成功之后回调的方法
     *
     * @param result
     * @param bundle
     */
    void handleDecode(Result result, Bundle bundle);

    /**
     * {@link android.app.Activity#setResult(int, Intent)}
     *
     * @param resultCode The result code to propagate back to the originating
     *                   activity, often RESULT_CANCELED or RESULT_OK
     * @param data       The data to propagate back to the originating activity.
     */
    void setResult(int resultCode, Intent data);

    /**
     * {@link android.app.Activity#finish()}
     */
    void finish();
}

运行时权限

需要两组权限,摄像头和内部存储。

// 获得运行时权限
private void getRuntimePermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        String[] perms = {Manifest.permission.CAMERA,
                Manifest.permission.WRITE_EXTERNAL_STORAGE};
        if (checkSelfPermission(perms[0]) == PackageManager.PERMISSION_DENIED
                || checkSelfPermission(perms[1]) == PackageManager.PERMISSION_DENIED) {
            requestPermissions(perms, 200);
        } else {
            jumpScanPage();
        }
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    switch (requestCode) {
        case 200: {
            for (int i : grantResults) {
                if (i == PackageManager.PERMISSION_DENIED) {
                    Toast.makeText(this, "权限拒绝", Toast.LENGTH_SHORT).show();
                    return;
                }
            }
            jumpScanPage();
            break;
        }
    }
}

项目集成

摄像头扫描二维码

1. 扫描界面保持屏幕常亮

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    setContentView(R.layout.activity_capture);
}

2. 扫描动画
选择属性动画,原因是可以暂停,方便在 onResumeonPause 处理。

private void initScan() {
    ImageView scanLine = findViewById(R.id.scan_line);

    // 扫描线性动画(属性动画可暂停)
    float curTranslationY = scanLine.getTranslationY();
    objectAnimator = ObjectAnimator.ofFloat(scanLine, "translationY",
            curTranslationY, Utils.dp2px(this, 170));
    // 动画持续的时间
    objectAnimator.setDuration(4000);
    // 线性动画 Interpolator 匀速
    objectAnimator.setInterpolator(new LinearInterpolator());
    // 动画重复次数
    objectAnimator.setRepeatCount(ObjectAnimator.INFINITE);
    // 动画如何重复,从下到上,还是重新开始从上到下
    objectAnimator.setRepeatMode(ValueAnimator.RESTART);
}

3. 开始扫描

private void startScan() {
    inactivityTimer = new InactivityTimer(this);
    beepManager = new BeepManager(this);

    if (isPause) {
        // 如果是暂停,扫描动画应该要暂停
        objectAnimator.resume();
        isPause = false;
    } else {
        // 开始扫描动画
        objectAnimator.start();
    }

    // 初始化相机管理
    cameraManager = new CameraManager(this);
    handler = null; // 重置handler
    if (isHasSurface) {
        initCamera(scanPreview.getHolder());
    } else {
        // 等待surfaceCreated来初始化相机
        scanPreview.getHolder().addCallback(this);
    }
    // 开启计时器
    if (inactivityTimer != null) {
        inactivityTimer.onResume();
    }
}

4. 初始化 SurfaceView

@Override
public void surfaceCreated(SurfaceHolder holder) {
    if (holder == null) {
        Log.e("netease >>> ", "SurfaceHolder is null");
        return;
    }

    if (!isHasSurface) {
        isHasSurface = true;
        initCamera(holder);
    }
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    isHasSurface = false;
}

5. 暂停扫描
由于相机、动画、记时器都是消耗性能的,需要在 onPause 中进行释放处理,同样在 onResume 中执行 startScan()

private void pauseScan() {
    if (handler != null) {
        // handler退出同步并置空
        handler.quitSynchronously();
        handler = null;
    }
    // 计时器的暂停
    if (inactivityTimer != null) {
        inactivityTimer.onPause();
    }
    // 关闭蜂鸣器
    beepManager.close();
    // 关闭相机管理器驱动
    cameraManager.closeDriver();
    if (!isHasSurface) {
        // remove等待
        scanPreview.getHolder().removeCallback(this);
    }
    // 动画暂停
    objectAnimator.pause();
    isPause = true;
}

6. 初始化相机

private void initCamera(SurfaceHolder surfaceHolder) {
    if (surfaceHolder == null) {
        throw new IllegalStateException("SurfaceHolder is null");
    }
    if (cameraManager.isOpen()) {
        Log.e(TAG, "surfaceCreated: camera is open");
        return;
    }

    try {
        cameraManager.openDriver(surfaceHolder);
        if (handler == null) {
            handler = new CaptureActivityHandler(this, cameraManager, DecodeThread.ALL_MODE);
        }
        initCrop();
    } catch (IOException ioe) {
        Log.w(TAG, ioe);
        Utils.displayFrameworkBugMessageAndExit(this);
    } catch (RuntimeException e) {
        // Barcode Scanner has seen crashes in the wild of this variety:
        // java.lang.RuntimeException: Fail to connect to camera service
        Log.w(TAG, "Unexpected error initializing camera", e);
        Utils.displayFrameworkBugMessageAndExit(this);
    }
}

7. 绘制扫描框矩形

private void initCrop() {
    // 获取相机的宽高
    int cameraWidth = cameraManager.getCameraResolution().y;
    int cameraHeight = cameraManager.getCameraResolution().x;

    // 获取布局中扫描框的位置信息
    int[] location = new int[2];
    scanCropView.getLocationInWindow(location);

    int cropLeft = location[0];
    int cropTop = location[1] - Utils.getStatusBarHeight(this);

    // 获取截取的宽高
    int cropWidth = scanCropView.getWidth();
    int cropHeight = scanCropView.getHeight();

    // 获取布局容器的宽高
    int containerWidth = scanContainer.getWidth();
    int containerHeight = scanContainer.getHeight();

    // 计算最终截取的矩形的左上角顶点x坐标
    int x = cropLeft * cameraWidth / containerWidth;
    // 计算最终截取的矩形的左上角顶点y坐标
    int y = cropTop * cameraHeight / containerHeight;

    // 计算最终截取的矩形的宽度
    int width = cropWidth * cameraWidth / containerWidth;
    // 计算最终截取的矩形的高度
    int height = cropHeight * cameraHeight / containerHeight;

    // 生成最终的截取的矩形
    mCropRect = new Rect(x, y, width + x, height + y);
}

8. 扫描成功的回调

@Override
public void handleDecode(Result result, Bundle bundle) {
    // 扫码成功之后回调的方法
    if (inactivityTimer != null) {
        inactivityTimer.onActivity();
    }
    // 播放蜂鸣声
    beepManager.playBeepSoundAndVibrate();
    // 将扫码的结果返回到MainActivity
    Intent intent = new Intent();
    intent.putExtra(Utils.BAR_CODE, result.getText());
    Utils.setResultAndFinish(CaptureActivity.this, RESULT_OK, intent);
}

相册扫描二维码

// 跳转到图片选择
public static void openAlbum(Activity activity) {
    Intent intent = new Intent(Intent.ACTION_PICK);
    intent.setType("image/*");
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        activity.startActivityForResult(intent, SELECT_PIC_KITKAT);
    } else {
        activity.startActivityForResult(intent, SELECT_PIC);
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
    // 相册返回
    if (requestCode == Utils.SELECT_PIC_KITKAT // 4.4及以上图库
            && resultCode == Activity.RESULT_OK) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                showProgressDialog();
                Uri uri = data.getData();
                String path = Utils.getPath(CaptureActivity.this, uri);
                Result result = Utils.scanningImage(path);
                Intent intent = new Intent();
                if (result == null) {
                    intent.putExtra(Utils.BAR_CODE, "未发现二维码/条形码");
                } else {
                    // 数据返回
                    intent.putExtra(Utils.BAR_CODE, Utils.recode(result.getText()));
                }
                Utils.setResultAndFinish(CaptureActivity.this, RESULT_OK, intent);
                dismissProgressDialog();
            }
        }).start();
    }
}

闪光灯

final TextView tvLight = findViewById(R.id.tv_light);
ToggleButton tbLight = findViewById(R.id.tb_light);

// 闪光灯控制
tbLight.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (isChecked) {
            tvLight.setText("关灯");
            Utils.openFlashlight(cameraManager);
        } else {
            tvLight.setText("开灯");
            Utils.closeFlashlight();
        }
    }
});

生成 jar 包

调试完毕最后,需要将 Library 生成 jar,在 build.gradle 中增加 makeJar 任务。

apply plugin: 'com.android.library'

android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    task makeJar(type: Copy) {
        // 删除存在的
        delete 'build/libs/zxing.jar'
        // 设置拷贝的文件
        from('build/intermediates/packaged-classes/release/')
        // 打进jar包后的文件目录
        into('build/libs/')
        // 将classes.jar放入build/libs/目录下
        // include ,exclude参数来设置过滤
        //(我们只关心classes.jar这个文件)
        include('classes.jar')
        // 重命名
        rename('classes.jar', 'zxing.jar')
    }

    makeJar.dependsOn(build)
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.google.zxing:core:3.3.3'
}

最后在 AS 右侧的 Gradle 中运行 makeJar ,就可以看到生成的 build/libs/zxing.jar了。

:libirary -> Tasks -> other 双击makejar,生成jar

DONE

完工,这个 jar 包可以集成到多个项目的二维码扫描,不会对项目造成污染,并且将摄像头,相册,闪光灯,蜂鸣声,UI等抽离,扩展性强。

github项目地址

上一篇 下一篇

猜你喜欢

热点阅读