美文网首页
[Android]拍照及相册选取图片功能

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

作者: 莫里亚蒂教授 | 来源:发表于2018-10-11 10:00 被阅读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”。
    

    相关文章

      网友评论

          本文标题:[Android]拍照及相册选取图片功能

          本文链接:https://www.haomeiwen.com/subject/gpvpaftx.html