项目用到了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,实在没办法。
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中可用,原作者具体是谁搞不清了,感谢网友的奉献,代码写法大家都差不多。