欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 拍照及相册选取图片功能,已适配Android6.0、7.0、8.0

程序员文章站 2024-03-23 10:06:04
...

更换头像或者上传图片功能已基本是每个 APP 所具备的基础功能了,但这对于开发者来说是一个很麻烦的事情,除机型之外,适配版本就至少要考虑这几种情况(6.0以下版本、6.0的动态权限、7.0的FileProvider、8.0的特殊情况)。

今天来个总结,方便自己也方便同行。


功能说明

本文的示例以下图为准:
Android 拍照及相册选取图片功能,已适配Android6.0、7.0、8.0

界面只有一个 ImageView,点击 ImageView 弹出 DialogFragment,分别是拍照和相册选择图片功能,其中都带有系统的裁剪功能,将裁剪后的图片显示在 ImageView 上。如果不需要裁剪功能,只需要将代码中的裁剪方法注释掉即可。

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);
    }

其中 imgName 是拍照图片的名字,一般以时间戳再加上自定义字符串命名;CAMERA_RESULT_CODE 是自定义的一个常量,作为拍照的请求码。

onActivityResult 处理拍照后的图片

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

将拍照后的图片创建成一个 File 对象,用来裁剪,裁剪的功能代码是和相册选取图片通用的。

裁剪代码

/**
     * 裁剪图片
     *
     * @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);
    }

其中的 CROP_RESULT_CODE 也是定义的静态常量,同拍照时定义的常量相同的作用。

如果裁剪时将 return-data 设置为 false,那么需要定义一个 Uri 来保存裁剪后的图片路径。

 // 裁剪属性 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);

图片裁剪完成回调

 case CROP_RESULT_CODE:
                // 裁剪时,这样设置 cropIntent.putExtra("return-data", true); 处理方案如下
                if (data != null) {
                    Bundle bundle = data.getExtras();
                    if (bundle != null) {
                        Bitmap bitmap = bundle.getParcelable("data");
                        imageView.setImageBitmap(bitmap);
                        // 把裁剪后的图片保存至本地 返回路径
                        String urlpath = FileUtilcll.saveFile(this, "crop.jpg", bitmap);
                        L.e("裁剪图片地址->" + urlpath);
                    }
                }

                // 裁剪时,这样设置 cropIntent.putExtra("return-data", false); 处理方案如下
//                try {
//                    ivHead.setImageBitmap(BitmapFactory.decodeStream(
// getActivity().getContentResolver().openInputStream(imageUri)));
//                } catch (FileNotFoundException e) {
//                    e.printStackTrace();
//                }
                break;

FileUtilcll 代码

/**
 * 图片文件操作
 */
public class FileUtilcll {

    /**
     * 将Bitmap 图片保存到本地路径,并返回路径
     *
     * @param fileName 文件名称
     * @param bitmap   图片
     * @param资源类型,参照 MultimediaContentType 枚举,根据此类型,保存时可自动归类
     */
    public static String saveFile(Context c, String fileName, Bitmap bitmap) {
        return saveFile(c, "", fileName, bitmap);
    }

    public static String saveFile(Context c, String filePath, String fileName, Bitmap bitmap) {
        byte[] bytes = bitmapToBytes(bitmap);
        return saveFile(c, filePath, fileName, bytes);
    }

    /**
     * Bitmap 转 字节数组
     *
     * @param bm
     * @return
     */
    public static byte[] bitmapToBytes(Bitmap bm) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bm.compress(CompressFormat.JPEG, 100, baos);
        return baos.toByteArray();
    }

    public static String saveFile(Context c, String filePath, String fileName, byte[] bytes) {
        String fileFullName = "";
        FileOutputStream fos = null;
        String dateFolder = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA)
                .format(new Date());
        try {
            String suffix = "";
            if (filePath == null || filePath.trim().length() == 0) {
                filePath = Environment.getExternalStorageDirectory() + "/cxs/" + dateFolder + "/";
            }
            File file = new File(filePath);
            if (!file.exists()) {
                file.mkdirs();
            }
            File fullFile = new File(filePath, fileName + suffix);
            fileFullName = fullFile.getPath();
            fos = new FileOutputStream(new File(filePath, fileName + suffix));
            fos.write(bytes);
        } catch (Exception e) {
            fileFullName = "";
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    fileFullName = "";
                }
            }
        }
        return fileFullName;
    }

}

到此,拍照功能就完成了,附带了裁剪。剩下的就是相册选取照片,这个也不难,固定代码,其他功能和拍照时相同的。

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

其中的 ALBUM_RESULT_CODE 同样是定义的常量。用来在 onActivityResult 中处理返回结果。

case ALBUM_RESULT_CODE:
                // 相册
                cropPic(data.getData());
                break;

和拍照公用裁剪代码,这里直接调用即可,其他逻辑不变。


6.0动态权限适配

除了上述功能保持不变之外,需要在代码中动态申请文件读写权限和相机权限。这里我将申请的时机写在了刚进入页面,而不是刚打开 APP,也不是点击按钮的时候。

逻辑

  • 如果用户点击了拒绝,但没有点击“不再询问”,这个时候再次进入界面继续弹框;
  • 如果用户点击了拒绝,且选择了“不再询问”,那么再次进入此界面将会弹框提示打开 APP 的详情界面,手动开启对应权限。

权限申请代码

/**
     * 初始化相机相关权限
     * 适配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);
            }
        }
    }

这里我将需要的权限统一写在了一个数组里面。其中的 REQUEST_PERMISSIONS 和上面拍照定义的常量功能相同。

权限申请回调

@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;
        }
    }

其中的 getAppDetailSettingIntent()方法代码如下:

/**
     * 获取 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;
    }

到这,权限这个适配问题就解决了,接下来该是 Android 7.0 的适配了,这里我们只需要修改拍照的功能即可,相册是没有问题的。


Android7.0 适配

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

拍照代码修改

/**
     * 打开系统相机
     */
    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);
        }
    }

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;
    }

拍照回调代码修改

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

                // 适配 Android7.0+
                cropPic(getImageContentUri(file));
                break;

getImageContentUri() 代码如下:

/**
     * 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>

其中这里的 android:authorities 属性值要和代码中 FileProvider.getUriForFile()的第二个参数值保持一致。

注意

  • 代码已全部给出,粘贴即可使用
  • 本次测试真机:华为5.1.1,一加8.0.0

总结

  1. 机型适配是个细活,但能锻炼排查问题和解决问题能力。
  2. 本文的代码有很多是可以抽取的,如果是在项目中使用,那么建议提取出特定功能的代码。