修改cordova底层以支持input直接打开照相机或图片库
文章目录
需求背景
cordova环境下,调用拍照或相册的代码与纯h5环境下不一致,这使得我们要写很多额外代码。本文用以解决这个问题。
当前时间:20201230
cordova版本: v.9.0+
android sdk: >=24
h5环境打开照相机或图片库
h5环境下,由于现在各种浏览器都支持h5了,不论iOS还是Android,或者微信,使用如下代码,可以直接掉起对于照相机或相册的选择:
<input type="file" accept="image/*" />
使用如下代码,可以直接打开摄像头
<input type="file" accept="image/*" capture="camera"/>
cordova环境打开照相机或者图片库
你从别处查找到的方法,大多是使用cordova-plugin-camera
,然后在html
层面写一堆代码,包括显示出选择当前需要拍照还是打开图片库,如果要拍照,则打通过plugin
开摄像头,否则通过input本来的功能打开图片库选择图片。
改造思路
修改cordova底层代码,使得input
代码可以直接调用照相机,也就是说,和各大浏览器厂商达到一样的显示效果。这样开发h5或cordova将具有同样的显示效果。
cordova本质是webview,则可以参考webview的调用拍照的思路。
修改onShowFileChooser
核心代码是修改cordova
原文件SystemWebChromeClient.java
下的函数onShowFileChooser
,此函数将在type=file的input被点击时调用。
找到函数
public boolean onShowFileChooser(WebView webView,
final ValueCallback<Uri[]> filePathsCallback,
final WebChromeClient.FileChooserParams fileChooserParams)
修改其代码,在原有的parentEngine.cordova.startActivityForResult
之前加上判断,如果打开的类型是image/*
,则调用新添加的函数showImageSelectIntent
进行照相机和相册的筛选。
...
boolean isCapture = fileChooserParams.isCaptureEnabled();
try {
String intent_type = intent.getType();
if (intent_type.equals("image/*")){
showImageSelectIntent(isCapture, filePathsCallback);
}else{
//正常筛选
parentEngine.cordova.startActivityForResult(new CordovaPlugin() {
...
其中isCapture
对应了input的属性capture="camera"
,传给函数之后代表了是否仅仅拍照。
增加showImageSelectIntent
函数showImageSelectIntent
实现如下
private String mCameraPhotoPath = null;
public void showImageSelectIntent(boolean onlyCapture, final ValueCallback<Uri[]> filePathsCallback){
try {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(parentEngine.cordova.getActivity().getPackageManager()) != null) {
// Create the File where the photo should go
File photoFile = null;
try {
photoFile = new File(Environment.getExternalStorageDirectory(),
"IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");
} catch (Exception ex) {
// Error occurred while creating the File
Log.e("", "Unable to create Image File", ex);
}
// Continue only if the File was successfully created
if (photoFile != null) {
mCameraPhotoPath = photoFile.getAbsolutePath();
Uri photoUri = FileProvider.getUriForFile(
parentEngine.cordova.getContext(),
parentEngine.cordova.getActivity().getPackageName() + ".provider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
takePictureIntent.putExtra(MediaStore.AUTHORITY, true);
takePictureIntent.putExtra("return-data", true);
takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
takePictureIntent = null;
}
}
Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
contentSelectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
contentSelectionIntent.setType("image/*");
Intent[] intentArray;
if (takePictureIntent != null) {
intentArray = new Intent[]{takePictureIntent};
} else {
intentArray = new Intent[2];
}
//发起选择
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
Intent f_intent = null;
if (onlyCapture) f_intent = takePictureIntent;
else f_intent = Intent.createChooser(chooserIntent, "Select images");
parentEngine.cordova.startActivityForResult(new CordovaPlugin() {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
Uri[] result = null;
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
////选择文件
if (intent.getClipData() != null) {
// handle multiple-selected files
final int numSelectedFiles = intent.getClipData().getItemCount();
result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = intent.getClipData().getItemAt(i).getUri();
LOG.d(LOG_TAG, "Receive file chooser URL: " + result[i]);
}
} else if (intent.getData() != null) {
// handle single-selected file
result = WebChromeClient.FileChooserParams.parseResult(resultCode, intent);
LOG.d(LOG_TAG, "Receive file chooser URL: " + result);
}
} else {
////照片
result = new Uri[]{Uri.parse("file:" + mCameraPhotoPath)};
}
}
filePathsCallback.onReceiveValue(result);
}
}, f_intent, IMAGE_FILECHOOSER_RESULTCODE);
}catch (Exception e){
LOG.e(LOG_TAG, "showImageSelectIntent error", e);
e.printStackTrace();
filePathsCallback.onReceiveValue(null);
}
}
如果出现一些错误提示,点击错误出,按下alt+Enter,按提示选择import相关东西即可。
修改cordova的build.gradle
增加以下一段配置,否则这一段代码会报错import androidx.core.content.FileProvider;
dependencies {
implementation 'androidx.core:core:1.3.2'
}
修改AndroidManifest.xml
在xml中增加provider,否则在targetSdkVersion>=24的编译环境/设备上会报错。
provider的作用是保存照片到指定位置。通过android:resource="@xml/file_provider_paths"
指定。
<application ...>
...
<provider android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true"
android:name="androidx.core.content.FileProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>
此配置对应前文中代码,绝大多数情况下,${applicationId}
和getPackageName()
拿到的是一个值,如果自己做了什么特殊的调整,请确保这两处依然能对应上。
Uri photoUri = FileProvider.getUriForFile(
parentEngine.cordova.getContext(),
parentEngine.cordova.getActivity().getPackageName() + ".provider",
photoFile);
此外还需要给manifest增加属性,并且给application增加属性。
在很多机型上,如果没有requestLegacyExternalStorage
这个属性,会导致拍照无法保存。
<manifest xmlns:tools="http://schemas.android.com/tools"
...>
<application
android:requestLegacyExternalStorage="true"
...>
...
增加文件file_provider_paths.xml
文件位置:src/main/res/xml/file_provider_paths.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root_path" path="."/>
</paths>
其与AndroidManifest.xml中的provider配合使得具有权限访问对应文件目录,才能顺利将拍照存成临时图片。
当前配置将会把文件写在其手机存储的根目录下,如果需要存在照片目录下还需要修改。
使用华为荣耀10测试,拍照之后,打开手机的文件管理–>我的手机,往下翻则会看见这些照片在根目录下。
如何把照片放在照片目录下
参考文档:FileProvider 路径配置策略的理解
Android FileProvider详细解析和踩坑指南
如果要放在DCIM(digital camera in memory)拍照目录下,则修改代码为
photoFile = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DCIM),
"IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");
如果要放在扩展相册目录下并且创建对应app的文件夹,则修改为
String dirname = "Cordova";
File destDir = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES), dirname);
if (!destDir.exists()) destDir.mkdir();
photoFile = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
dirname+"/IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");
需要import
import android.os.Environment;
import static android.os.Environment.*;
将图片添加到相册
这时候,我们会发现,图片并没有添加到相册中,也就是上一次拍照的结果虽然保存下来了,但是在下一次我们选择图片库的时候,并不会直接看到这个图片,还需要去文件夹中寻找,这非常不方便。
此时我们只需要将这个照片添加到相册,下次打开图片库的时候,就会看见之前的照片,而不需要重新拍摄了。
我们只需要在函数onActivityResult
中,result = new Uri[]{Uri.parse("file:" + mCameraPhotoPath)};
之前加上如下代码即可:
MediaScannerConnection.scanFile(parentEngine.cordova.getContext().getApplicationContext(),
new String[]{mCameraPhotoPath}, null,
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
}
});
参考资料:Android 新增一张图片 加入相册
优化——兼容打开摄像机
修改cordova底层以支持input直接打开摄像机或视频库
参考资料
Android开发深入理解WebChromeClient之onShowFileChooser或openFileChooser使用说明 - TeachCourse
webview开发中使用onShowFileChooser实现web页打开照相机或者图片浏览
webview开发中使用onShowFileChooser实现web页打开图库上传图片
webview开发中使用onShowFileChooser实现web页打开照相机或者图片浏览
解决exposed beyond app through ClipData.Item.getUri() 错误
android开发出现错误:Failed to find configured root that contains