Android 8.0 WebView 拍照、简易预览、二维码扫描 各种问题解决

项目用到了WebView包装HTML5做成app使用,其中有页面用到了二维码和拍照上传功能。本人从未做过android,短时间内完成,只能靠“热心网友”帮忙了,网上也铺天盖地各种demo和文章。

但是对于高版本,特别是android 8.0以上,网上的各种现成的Demo都不好用,各种问题。现在我成功了解决了这些问题,并汇总供初学者参考。


这个是我完成的Demo的CSDN下载链接,如果觉得这个写得比较繁琐,在本文的末尾处,我将再提供一个最简单的拍照预览demo。至于预览页面样式很丑之类的,不能怪我,我不是专业做UI的,demo只是展示功能。


1. 权限问题

这个问题也是困扰最多的一个问题,各种Android版本对于权限的限制和提示不一样,特别是在我的Mate9(Android 8.0)上一度不提示,后台也不报错,但是浏览器就是在拍照后无法访问图片,搞了很久才知道是没有在代码里加动态权限检查。AndroidManifest.xml文件只是注册了需要用哪些权限并不是app获取到的权限,高版本android在安装app时忽略提示权限,所以一般app首次安装打开后,都会提示你需要什么权限。

由此,建议在首页的Activity的onCreate方法里,增加权限检查。

checkSelfPermission(Manifest.permission.CAMERA);

checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);

等等,如果只用到了拍照,那么这2个就够了,可存比可读,不用再赋读取权限。


对于权限检查,有很多问题需要考虑,比如禁止后,怎么办?如果用户勾选“禁止后不再询问”,怎么办?

我这边的处理是如下: 

Activity的回调方法onRequestPermissionsResult 

    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        HandleMainActivityResult.onRequestPermissionsResult(this,requestCode,permissions,grantResults);
    }
处理权限提示等,基本都在HandleMainActivityResult.onRequestPermissionsResult方法

//权限回调方法(默认知道某个requestCode赋值几个权限,数组大小为几)
    public static void onRequestPermissionsResult(final MainActivity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == ActivityResultConst.CODE_FOR_WRITE_PERMISSION) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                boolean isShow = true;
                Log.e(TAG, "grantResults.length:"+grantResults.length);
                String tipTitle = "权限不可用";
                if(grantResults.length == 2){
                    if (grantResults[0] != PackageManager.PERMISSION_GRANTED && grantResults[1] != PackageManager.PERMISSION_GRANTED) {
                        tipTitle = "相机和存储权限不可用";
                    } else if(grantResults[0] != PackageManager.PERMISSION_GRANTED){
                        tipTitle = "相机权限不可用";
                    } else if(grantResults[1] != PackageManager.PERMISSION_GRANTED){
                        tipTitle = "存储权限不可用";
                    } else {
                        //权限已经全部赋值成功
                        isShow = false;
                    }
                    // 判断用户是否 点击了不再提醒。(检测该权限是否还可以申请)
                    boolean completeForbidden1 = activity.shouldShowRequestPermissionRationale(permissions[0]);
                    boolean completeForbidden2 = activity.shouldShowRequestPermissionRationale(permissions[1]);
                    Log.e(TAG, permissions[0] + " isCompleteForbidden: " + completeForbidden1 + ";" + permissions[1] + " isCompleteForbidden: " + completeForbidden2);
                    if(!completeForbidden1 || !completeForbidden2){
                        //用户点击了禁止后不再询问,建议直接提示并退出
                        Toast.makeText(activity.getApplicationContext(), "以后可在-应用设置-权限管理-中,手动开启权限", Toast.LENGTH_SHORT).show();
                    } else {
                        if (isShow) {
                            new AlertDialog.Builder(activity)
                                    .setTitle(tipTitle)
                                    .setMessage("由于手机助手需要拍照上传和扫描二维码功能,请开启权限;\n否则,您将无法正常使用")
                                    .setPositiveButton("立即开启", new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            PermissionHandler.checkPermissionForCameraAndWriteStorage(activity);
                                        }
                                    })
                                    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            //Toast.makeText(activity.getApplicationContext(), "以后可在-应用设置-权限-中,手动开启权限", Toast.LENGTH_SHORT).show();
                                        }
                                    }).setCancelable(false).show();
                        }
                    }
                } else {
                    PermissionHandler.checkPermissionForCameraAndWriteStorage(activity);
                }
            }
        }
    }
注:shouldShowRequestPermissionRationale方法返回true表示用户彻底禁止了询问

如果用户禁止了某个权限,而你程序又运行到此处怎么办?后台会直接报错,我的处理办法是try catch 然后进行权限提示(注意不是权限检查,因为这里不应该进行检查也不适合进行检查)。看我的webChromeClient子类代码:

    @Override
    public boolean onShowFileChooser(WebView webView,
                                     ValueCallback<Uri[]> filePathCallback,
                                     FileChooserParams fileChooserParams) {
        mainActivity.setmUploadCallbackAboveL(filePathCallback);
        try {
            PhotoUtil.take(mainActivity);
        } catch (java.lang.SecurityException e){
            Log.e(TAG,e.getMessage(),e);
            if (e.getMessage() != null && e.getMessage().indexOf("Permission Denial") != -1) {
                String tipTitle = "缺少权限";
                if (e.getMessage().indexOf("android.permission.WRITE_EXTERNAL_STORAGE") != -1) {
                    tipTitle = "缺少存储权限";
                } else if(e.getMessage().indexOf("android.permission.CAMERA") != -1){
                    tipTitle = "缺少相机权限";
                }
                new AlertDialog.Builder(mainActivity)
                        .setTitle("温馨提示")
                        .setMessage(tipTitle + ",可在-应用设置-权限管理-中,手动开启")
                        .setPositiveButton("知道了", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                //Do Nothing
                            }
                        }).setCancelable(false).show();
            }
        }
        return true;
    }

也许有更好的办法,也许有第三方框架可以解决这个问题,但是作为初学者的我,目前先这么处理吧。


这里有个问题需要讨论:能不能在使用时才增加权限检查,而不是(首次)打开app首页时检查?

告诉你,这个是不行的!在使用时,动态检查权限,虽然app会跳出权限提示的dialog提示,但是Activity的onRequestPermissionsResult方法回调会出问题,导致你页面调整异常。这也就是为什么大部分app都是安装后,首次打开,会进行一连串权限提醒的原因。别问我为什么知道,我被坑了很久。


2.调用相机问题

如果是android 低版本,随便调(单独调用相机,不浏览相册),几行代码即可:

Intent intentCamera = new Intent();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
        }
        intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
        //将拍照结果保存至photo_file的Uri中,不保留在相册中
        intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        startActivityForResult(intentCamera, FILECHOOSER_RESULTCODE);
如果是android版本,比如 android 7.0以上,那么需要做判断

        //兼容android 7.0+版本的照相机调用代码
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (intent.resolveActivity(mainActivity.getPackageManager()) != null) {
             /*获取当前系统的android版本号*/
            int currentapiVersion = android.os.Build.VERSION.SDK_INT;
            Log.e(TAG,"currentapiVersion====>"+currentapiVersion);
            if (currentapiVersion<24){
                intent.putExtra(MediaStore.EXTRA_OUTPUT, mainActivity.getImageUri());
                mainActivity.startActivityForResult(intent, ActivityResultConst.CAMERA_RESULTCODE);
            }else {
                ContentValues contentValues = new ContentValues(1);
                contentValues.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
                Uri uri = mainActivity.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
                mainActivity.startActivityForResult(intent, ActivityResultConst.CAMERA_RESULTCODE);
            }
        } else {
            Toast.makeText(mainActivity, "照相机不存在", Toast.LENGTH_SHORT).show();
        }

android 7.0以上无法调用相机,网上有网友这么建议,在onCreate方法里加入如下代码:

//android 7.0系统解决拍照的问题
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
builder.detectFileUriExposure();
我测试过,是可以的,虽然不知道暴力的原理是什么,建议还是不要这么用。

如果需要调用相机或者打开相册,那么可以这么搞:

//调用照相机和浏览图片库代码
        Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
        captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mainActivity.getImageUri());

        Intent Photo = new Intent(Intent.ACTION_PICK,
                android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);

        Intent chooserIntent = Intent.createChooser(Photo, "Image Chooser");
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[]{captureIntent});

        mainActivity.startActivityForResult(chooserIntent, ActivityResultConst.CAMERA_RESULTCODE);
照相后,Activity里的onActivityResult回调方法里面,还需要广播通知一下,刷新图库,调用方法代码如下:

    private static void updatePhotos(MainActivity activity) {
        // 该广播即使多发(即选取照片成功时也发送)也没有关系,只是唤醒系统刷新媒体文件
        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        intent.setData(activity.getImageUri());
        activity.sendBroadcast(intent);
    }
如果是android 高版本,高配置手机的话,照相机拍得图分辨率非常高,图片就非常大,特别是进行多图预览或上传时,后台容易报,app变卡顿:(我在进行多个图片不压缩上传时,就出现过此问题)

I/Choreographer: Skipped 179 frames!  The application may be doing too much work on its main thread.

这种情况建议压缩图片,比如压缩成800*480,最多几百K,基本不会出问题。压缩代码见我上传的demo:

    @SuppressWarnings("null")
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private static void onActivityResultAboveL(MainActivity activity, int requestCode, int resultCode, Intent data) {
        if (requestCode != ActivityResultConst.CAMERA_RESULTCODE
                || activity.getmUploadCallbackAboveL() == null) {
            return;
        }
        Uri[] results = null;
        if (resultCode == Activity.RESULT_OK) {
            if (data == null) {
                results = new Uri[]{activity.getImageUri()};
            } else {
                String dataString = data.getDataString();
                ClipData clipData = data.getClipData();

                if (clipData != null) {
                    results = new Uri[clipData.getItemCount()];
                    for (int i = 0; i < clipData.getItemCount(); i++) {
                        ClipData.Item item = clipData.getItemAt(i);
                        results[i] = item.getUri();
                    }
                }

                if (dataString != null)
                    results = new Uri[]{Uri.parse(dataString)};
            }
        }
        if (results != null) {
            //压缩
            results = PhotoUtil.doCompressImageForActivityResult(activity,results);
            activity.getmUploadCallbackAboveL().onReceiveValue(results);
            activity.setmUploadCallbackAboveL(null);
        } else {
            //压缩
            results = new Uri[]{activity.getImageUri()};
            results = PhotoUtil.doCompressImageForActivityResult(activity,results);
            activity.getmUploadCallbackAboveL().onReceiveValue(results);
            activity.setmUploadCallbackAboveL(null);
        }
        return;
    }
如果你不需要压缩,那么注释掉的这一行,results传原值给getmUploadCallbackAboveL().onReceiveValue()即可
results = PhotoUtil.doCompressImageForActivityResult(activity,results);


最后还有几个小问题,需要注意一下:
a. 二维码扫描之后,如果要返回,需要考虑是不是goback到父的父页面,不然扫描完一点击手机的返回键,直接就打开了照相机。


b. 我在AndroidManifest.xml文件给webview加了一个背景图片。
代码如 android:roundIcon="@mipmap/ic_launcher_round"


c. 我对MainActivity进行了一些简答的封装,不至于让全部的代码,都写在MainActivity.java中。
封装得很没有水平,用j2ee的思维在弄android,实在没办法。


d.  MainActivity的onActivityResult回调方法,在处理不同的行为请求返回时,照理说都应该有个requestCode,比如相机处理化代码:
ContentValues contentValues = new ContentValues(1);
                contentValues.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
                Uri uri = mainActivity.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
                mainActivity.startActivityForResult(intent, ActivityResultConst.CAMERA_RESULTCODE);

ActivityResultConst.CAMERA_RESULTCODE是我定义的常量类属性,不同的数字代表不同的请求码。
但是问题来了,扫描二维码,我用页面JavaScript调用BarcodeCallBack类实现的,好像没有进行Activity的Intent操作,无法设置或取得请求码,怎么办呢,我用了个非常挫的办法,在MainActivity里定义了一个Map,然后扫描二维码初始化时手动往这里面放一个常量,作为请求码,在onActivityResult方法里处理判断:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        //自定义Activity返回结果码
        Integer extraRequestCode = (Integer) extra.get(ActivityResultConst.REQUEST_CODE_KEY);
        if(extraRequestCode == null || extraRequestCode == 0){
            //如果没有自定义返回结果码,则使用onActivityResult的参数
            extraRequestCode = requestCode;
        }
        //根据不同的返回码,执行不同的结果处理(一般指回调操作)
        if (extraRequestCode == ActivityResultConst.BARCODE_RESULTCODE) {
            HandleMainActivityResult.onBarcodeResult(this, requestCode, resultCode, data);
        }
        if (extraRequestCode == ActivityResultConst.CAMERA_RESULTCODE) {
            HandleMainActivityResult.onCameraResult(this,requestCode,resultCode,data);
        }
        extra.clear();
    }


e. 发现了一个比较好玩的事情:如果在AndroidManifest.xml里未注册相机权限,则似乎不需要动态检查相机权限,在安装时,权限列表里也未发现有相机权限,但是事实上,相机却可以正常调用。
如果在AndroidManifest.xml里注册相机权限,那么对不起,需要你在onCreate方法里动态检查相机权限,否则后台报错,相机无法调起。
说明相机这个权限很弱,默认可以让app使用,估计用一下,也不会怎么样,把系统搞崩溃吧。


f.如果你已经打开了照相机,这时,你按手机的回退键取消操作,再重新进来时,你会发现相机已经无法被调起。
原因是:因为对于页面表单<input type=file >来说,调用照相机相当于选择图片。而选择图片这个事件必须要有一个返回值,不然程序会一直处于等待状态,当你没有选定的时候你要传回一个null,否则程序就一直阻塞,就不能进行其它操作。
处理代码在Activity的方法onActivityResult(int requestCode, int resultCode, Intent data),对于我demo代码,也就是方法onCameraResult(MainActivity activity, int requestCode, int resultCode, Intent data)里加上取消操作的判断

    public static void onCameraResult(MainActivity activity, int requestCode, int resultCode, Intent data) {
        if(resultCode == Activity.RESULT_CANCELED) {
            //打开相机,若取消,必须要设置一个null返回值,不然页面会一直处于等待状态,阻塞住无法响应
            if(activity.getmUploadCallbackAboveL() != null){
                activity.getmUploadCallbackAboveL().onReceiveValue(null);
            }
            if(activity.getmUploadMessage() != null){
                activity.getmUploadMessage().onReceiveValue(null);
            }
            return;
        }
        updatePhotos(activity);
        if (null == activity.getmUploadMessage() && null == activity.getmUploadCallbackAboveL())
            return;
        Uri result = data == null || resultCode != Activity.RESULT_OK ? null : data.getData();
        if (activity.getmUploadCallbackAboveL() != null) {
            onActivityResultAboveL(activity, requestCode, resultCode, data);
        } else if (activity.getmUploadMessage() != null) {
            Log.e("result", result + "");
            if (result == null) {
                //低版本,暂时不做压缩处理
                activity.getmUploadMessage().onReceiveValue(activity.getImageUri());
                activity.setmUploadMessage(null);
                Log.e("imageUri", activity.getImageUri() + "");
            } else {
                //压缩
                result = PhotoUtil.doCompressImageForActivityResult(activity, data, null);
                activity.getmUploadMessage().onReceiveValue(result);
                activity.setmUploadMessage(null);
            }
        }
    }
这个代码可能在我上传的demo里未更新,请读者务必要留意。


上面就是我完成的demo遇到的各种问题的汇总介绍。

如果有人觉得繁琐,那么我再上传一个最简答的demo版本,拍照预览功能的。(上传自己用jquery弄下,图片都被你file标签获取到了)。  点击下载 最简易版本的demo

这个例子,是本人从网上荡的,经过改造后,在高版本android中可用,原作者具体是谁搞不清了,感谢网友的奉献,代码写法大家都差不多。








 


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值