[Android]拍照及相册选取图片功能

2018-10-11  本文已影响271人  莫里亚蒂教授

之前项目中经常会遇到相册选取图片的功能,都是从网上找到现成的模板进行应用,实际应用时会发现有很多问题,趁着这段时间比较充裕,总结一下Android的拍照及相册选取图片;

需要适配6.0之前的版本、7.0版本和8.0版本;估计9.0和8.0差不多,目前视为同样兼容,等我回来更用新机进行测试后再做确认;

针对6.0 以下版本

--拍照部分--

(1)需要配置权限:

    <uses-feature android:name="android.hardware.camera" />
    <!--相机权限-->
    <uses-permission android:name="android.permission.CAMERA" />
    <!--写入SD卡的权限:如果你希望保存相机拍照后的照片-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--读取SD卡的权限:打开相册选取图片所必须的权限-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

(2)拍照功能代码实现:

    /**
     * 打开系统相机  
     */
    private void openSysCamera() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
                new File(Environment.getExternalStorageDirectory(), imgName)));
        startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
    }

(3) 处理拍照后的图片<含剪裁图片>:

case CAMERA_RESULT_CODE:
                tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
                cropPic(Uri.fromFile(tempFile));
                break;

(4)裁剪代码实现:

 // 裁剪属性 cropIntent.putExtra("return-data", false); 时,使用自定义接收图片的Uri
    private static final String IMAGE_FILE_LOCATION = "file:///" + Environment.getExternalStorageDirectory().getPath() + "/temp.jpg";
    private Uri imageUri = Uri.parse(IMAGE_FILE_LOCATION);

    /**
     * 裁剪图片
     *
     * @param data
     */
    private void cropPic(Uri data) {
        if (data == null) {
            return;
        }
        Intent cropIntent = new Intent("com.android.camera.action.CROP");
        cropIntent.setDataAndType(data, "image/*");

        // 开启裁剪:打开的Intent所显示的View可裁剪
        cropIntent.putExtra("crop", "true");
        // 裁剪宽高比
        cropIntent.putExtra("aspectX", 1);
        cropIntent.putExtra("aspectY", 1);
        // 裁剪输出大小
        cropIntent.putExtra("outputX", 320);
        cropIntent.putExtra("outputY", 320);
        cropIntent.putExtra("scale", true);
        /**
         * return-data
         * 这个属性决定我们在 onActivityResult 中接收到的是什么数据,
         * 如果设置为true 那么data将会返回一个bitmap
         * 如果设置为false,则会将图片保存到本地并将对应的uri返回,当然这个uri得有我们自己设定。
         * 系统裁剪完成后将会将裁剪完成的图片保存在我们所这设定这个uri地址上。我们只需要在裁剪完成后直接调用该uri来设置图片,就可以了。
         */
        cropIntent.putExtra("return-data", true);
        // 当 return-data 为 false 的时候需要设置这句
        //cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        // 图片输出格式
        //cropIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        // 头像识别 会启动系统的拍照时人脸识别
        //cropIntent.putExtra("noFaceDetection", true);
        startActivityForResult(cropIntent, CROP_RESULT_CODE);
    }

--相册部分--

    /**
     * 打开系统相册
     */
    private void openSysAlbum() {
        Intent albumIntent = new Intent(Intent.ACTION_PICK);
        albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
        startActivityForResult(albumIntent, ALBUM_RESULT_CODE);
    }
    /**
     * 回调系统相册
     */
     case ALBUM_RESULT_CODE:
                // 相册
                cropPic(data.getData());
                break;

针对6.0加入了动态申请权限适配

6.0后需要进行动态权限申请,具体逻辑如下:
·如果用户点击了拒绝,但没有点击“不再询问”,这个时候再次进入 · 界面继续弹框;
·如果用户点击了拒绝,且选择了“不再询问”,那么再次进入此界面将会弹框提示打开 APP 的详情界面,手动开启对应权限。

(1)权限申请代码:
这里我们先放一个规范化的使用方法,后面我会把我常用的方法展示;

    /**
     * 初始化相机相关权限
     * 适配6.0+手机的运行时权限
     */
    private void initPermission() {
        String[] permissions = new String[]{Manifest.permission.CAMERA,
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE};
        //检查权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            // 之前拒绝了权限,但没有点击 不再询问 这个时候让它继续请求权限
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.CAMERA)) {
                Toast.makeText(this, "用户曾拒绝打开相机权限", Toast.LENGTH_SHORT).show();
                ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
            } else {
                //注册相机权限
                ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
            }
        }
    }

(2)权限申请回调:

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_PERMISSIONS:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    //成功
                    Toast.makeText(this, "用户授权相机权限", Toast.LENGTH_SHORT).show();
                } else {
                    // 勾选了不再询问
                    Toast.makeText(this, "用户拒绝相机权限", Toast.LENGTH_SHORT).show();
                    /**
                     * 跳转到 APP 详情的权限设置页
                     *
                     * 可根据自己的需求定制对话框,点击某个按钮在执行下面的代码
                     */
                    Intent intent = Util.getAppDetailSettingIntent(PhotoFromSysActivity.this);
                    startActivity(intent);
                }
                break;
        }
    }

    /**
     * 获取 APP 详情页面intent
     *
     * @return
     */
    public static Intent getAppDetailSettingIntent(Context context) {
        Intent localIntent = new Intent();
        localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (Build.VERSION.SDK_INT >= 9) {
            localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
            localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
        } else if (Build.VERSION.SDK_INT <= 8) {
            localIntent.setAction(Intent.ACTION_VIEW);
            localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
            localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
        }
        return localIntent;
    }

(3)下面就是我经常使用的第三方权限判断:
·使用前需要引用外部包:

    implementation 'com.github.hotchemi:permissionsdispatcher:2.1.3'

·后面即可直接调用方法,把需要的权限以数组的形式放入PERMISSION_STARTSPOT中。

 private static final String[] PERMISSION_STARTSPOT = new String[]{"android.permission.CAMERA"};

 if (PermissionUtils.hasSelfPermissions(this, PERMISSION_STARTSPOT)) {
                Navigator.navigateToWebActivity(this, Const.toBX, "填写报事报修单");
            } else {
                new AlertDialog.Builder(this)
                        .setTitle("申请拍照权限")
                        .setMessage("相机权限: 扫一扫二维码(要求)")
                        .setPositiveButton("确定", (dialog, which) -> ActivityCompat.requestPermissions(this, PERMISSION_STARTSPOT, REQUEST_STARTSPOT))
                        .setNegativeButton("取消", (dialog, which) -> dialog.dismiss())
                        .show();
            }

针对Android7.0及以上适配

由于在Android7.0上,google使用了新的权限机制,所以导致在调用相机的时候,如果传递的URI为”file://”类型,系统会抛出FileUriExposedException这个错误.具体堆栈信息如下:


异常截图

Android 7.0 就是 File 路径的变更,需要使用 FileProvider 来做,下面看拍照的代码。

(1)拍照代码修改实现:

    /**
     * 打开系统相机
     */
    private void openSysCamera() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
//                new File(Environment.getExternalStorageDirectory(), imgName)));
//        File file = new File(Environment.getExternalStorageDirectory(), imgName);
        try {
            file = createOriImageFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (file != null) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                imgUriOri = Uri.fromFile(file);
            } else {
                imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", file);
            }
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriOri);
            startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
        }
    }

(2)File 对象的创建和 拍照图片的 Uri 对象创建方式更改。创建原图像保存的代码如下:

    /**
     * 创建原图像保存的文件
     *
     * @return
     * @throws IOException
     */
    private File createOriImageFile() throws IOException {
        String imgNameOri = "HomePic_" + new SimpleDateFormat(
                "yyyyMMdd_HHmmss").format(new Date());
        File pictureDirOri = new File(getExternalFilesDir(
                Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/OriPicture");
        if (!pictureDirOri.exists()) {
            pictureDirOri.mkdirs();
        }
        File image = File.createTempFile(
                imgNameOri,         /* prefix */
                ".jpg",             /* suffix */
                pictureDirOri       /* directory */
        );
        imgPathOri = image.getAbsolutePath();
        return image;
    }

(3)拍照回调代码修改:

     case CAMERA_RESULT_CODE:
                // tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
                // cropPic(Uri.fromFile(tempFile));
                // 适配 Android7.0+
                cropPic(getImageContentUri(file));
                break;

    /**
     * 7.0以上获取裁剪 Uri
     *
     * @param imageFile
     * @return
     */
    private Uri getImageContentUri(File imageFile) {
        String filePath = imageFile.getAbsolutePath();
        Cursor cursor = getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID},
                MediaStore.Images.Media.DATA + "=? ",
                new String[]{filePath}, null);

        if (cursor != null && cursor.moveToFirst()) {
            int id = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.MediaColumns._ID));
            Uri baseUri = Uri.parse("content://media/external/images/media");
            return Uri.withAppendedPath(baseUri, "" + id);
        } else {
            if (imageFile.exists()) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.Images.Media.DATA, filePath);
                return getContentResolver().insert(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            } else {
                return null;
            }
        }
    }

上面只是拍照代码的修改,下面还有一个 FileProvider 问题需要做如下配置。

(1)在 res 目录下创建一个名为 xml 的文件夹,并在其下创建一个名为 file_paths.xml 文件,其内容如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path
        name="images"
        path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
    <external-path
        name="images"
        path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
    <external-files-path
        name="images"
        path="files/Pictures/OriPicture" />
    <root-path
        name="images"
        path="" />
    <root-path
        name="images"
        path="" />
</paths>

(2) 在 AndroidMainfest.xml 中的 application 节点下做如下配置:

<!--FileProvider共享文件、缓存-->
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.cxs.yukumenu.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

注意:我发现了authorities所填写的包名,需要有debug和release的区分,应用时需要注意。
(1)我当前项目是这样应用的,还是有待优化和封装的,如下:

    private Intent createCameraIntent() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File externalDataDir = Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DCIM);
        File cameraDataDir = new File(externalDataDir.getAbsolutePath() +
                File.separator + "browser-photos");
        cameraDataDir.mkdirs();
        mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator +
                System.currentTimeMillis() + ".jpg";
        // 中间的参数 authority 可以随意设置.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            uri = Uri.fromFile(new File(mCameraFilePath));
        } else {
            uri = FileProvider.getUriForFile(getActivity(), "cn.ebatech.propertyandroid.fileprovider", new File(mCameraFilePath));
        }
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        return cameraIntent;
    }

(2)回调结果,加入了Broadcast,实现如下:

      if (requestCode == TAKE_PHOTO) {
            File cameraFile = new File(mCameraFilePath);
            Uri result = resultCode == RESULT_OK && cameraFile.exists() ? Uri.fromFile(cameraFile) : null;
            if (result != null) {
                // Broadcast to the media scanner that we have a new photo
                // so it will be added into the gallery for the user.
                BaseApplication.getInstance().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
            }
            if (uploadMessageAboveL != null) {
                uploadMessageAboveL.onReceiveValue(result == null ? null : new Uri[]{result});
                uploadMessageAboveL = null;
            } else if (uploadMessage != null) {
                uploadMessage.onReceiveValue(result);
                uploadMessage = null;
            }
        }

延伸问题:(虽然我开发中未曾遇到,但是查找资料时有这方面的提示,于是乎总结进来,待用)

<IllegalArgumentException: Failed to find configured root that contains>

java.lang.RuntimeException: Unable to start activity ComponentInfo{.../....EditInfoActivity}: java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/Android/data/.../files/Cache/30001748.jpg
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2680)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2741)
    at android.app.ActivityThread.-wrap12(ActivityThread.java)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1492)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:160)
    at android.app.ActivityThread.main(ActivityThread.java:6139)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
...

(1)问题解决:
明明这个路径对应的文件是存在的,为什么会找不到呢,感觉到肯定是路径配置那里出问题,经过查找资料,才发现file_paths.xml中配置的external-path是如下作用:

该方式提供在外部存储区域根目录下的文件。
它对应Environment.getExternalStorageDirectory返回的路径:eg:”/storage/emulated/0”;

而external-files-path的作用是我需要的这种路径即:

该方式提供在应用的外部存储区根目录的下的文件。
它对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)返回的路径。
eg:”/storage/emulated/0/Android/data/com.jph.simple/files”。
上一篇下一篇

猜你喜欢

热点阅读