修改cordova底层以支持input直接打开照相机或图片库

修改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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值