Raven's Blog

Android 7.0后相机相册调用

1. 前言

需求提出:之前在帮助别人做东西的时候有这样一个功能,相机拍照到一个固定的目录,相册选取图片(单选、多选)。

Android6.0以后提出新的权限管理机制,7.0后相册访问机制也进一步更改。所以我们除了相机、相册功能外还要考虑权限问题。下面就稍微讲解一下如何实现这些功能。

先说明一点,本文所实现的代码没有兼容7.0以前,只实现了7.0之后的相关代码。

相关知识:

  1. 相机、相册权限请求
  2. 相机、相册调用
  3. 相机拍照后将图片移动一个固定的文件夹下(文件IO操作)

2. 代码实现

先来看一下实现的效果:

2.1 材料准备

为实现代码方便,这里引用两个开源库

//动态权限申请库
implementation 'pub.devrel:easypermissions:1.3.0'
// 多选相册(如果不实现多选,不用引用这个库,调用系统相册选择已经足够)
implementation 'com.github.HuanTanSheng:EasyPhotos:2.4.4'
// Glide 多选相册依赖,不用多选就不用添加下面两项
implementation 'com.github.bumptech.glide:glide:4.4.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.4.0'

注意要引用EasyPhotos相关库,需要在项目的gradle文件下增添一个仓库地址maven { url "https://jitpack.io" },最后的效果如下:

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

2.2 相关配置

在manifest文件中添加权限

 <!-- 读写权限 -->
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
 <!-- 调用相机权限 -->
 <uses-permission android:name="android.permission.CAMERA" />

再在manifest文件的application标签中添加以下代码:

 <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.raven.demo.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

注意这里的android:authorities="com.raven.demo.fileprovider"字段,需要将该字段中的com.raven.demo改为自己的包名。包名可在app模块的gradle文件中查看。

再在资源文件下建立file_paths.xml,如图:

添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>

2.3 界面设计与点击事件

为方便测试,这里仅给了三个Button,一个相机调用,一个单相片选取,一个多相片选取。

效果如下:

代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bt_camera"
        android:text="相机"
        android:layout_width="200dp"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/bt_single_pic"
        android:text="单相片选取"
        android:layout_width="200dp"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/bt_multi_pic"
        android:text="多相片选取"
        android:layout_width="200dp"
        android:layout_height="wrap_content" />
</Linearout>

初始化这几个button并添加相应的点击事件,加入一些辅助变量。

// 控件
Button btCamera,btSinglePic,btMultiPic;

//相机请求码
private static final int CAMERA_REQUEST_CODE = 1;

//单张请求码
private static final int SINGLE_REQUEST_CODE = 2;

// 多张请求码
private static final int MULTI_REQUEST_CODE = 3;

// 权限集合
private String[] permissions = {Manifest.permission.CAMERA,Manifest.permission.WRITE_EXTERNAL_STORAGE};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // 初始化view
    btCamera = findViewById(R.id.bt_camera);
    btSinglePic = findViewById(R.id.bt_single_pic);
    btMultiPic = findViewById(R.id.bt_multi_pic);

    // 增添点击事件
    btCamera.setOnClickListener(this);
    btSinglePic.setOnClickListener(this);
    btMultiPic.setOnClickListener(this);
}

@Override
public void onClick(View v) {
    int id = v.getId();
    if(R.id.bt_camera == id){
        // 调用系统相机
        callSystemCamera();
    }else if(R.id.bt_single_pic == id){
        // 调用单张相册
        callSinglePic();
    }else if(R.id.bt_multi_pic == id){
        // 调用多张相册选择
        callMultiPic();
    }
}

2.4 设置权限回调

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    //框架要求必须这么写
    EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}

2.5 实现三个功能

 /**
     * 调用系统相机,包含权限检测
     */
    private void callSystemCamera() {
        if (EasyPermissions.hasPermissions(this, permissions)) {
            //已经打开权限
            useCamera();
        } else {
            //没有打开相关权限、申请权限
            getPermission();
        }
    }

    /**
     * 调用相机
     */
    private void useCamera() {
        Intent intent = new Intent();
        //此处之所以诸多try catch,为了兼容
        intent.setAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
        startActivityForResult(intent, CAMERA_REQUEST_CODE);
    }

    /**
     * 调用系统相册,选择单张图片,包含权限检测
     */
    private void callSinglePic() {
        if (EasyPermissions.hasPermissions(this, permissions)) {
            //已经打开权限
            chooseSinglePhoto();
        } else {
            //没有打开相关权限、申请权限
            getPermission();
        }
    }

    /**
     * 选择单张图片
     */
    private void chooseSinglePhoto() {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_PICK);
        intent.setType("image/*");
        startActivityForResult(intent, SINGLE_REQUEST_CODE);
    }

    /**
     * 调用第三方相册,选择多张图片
     */
    private void callMultiPic() {
        if (EasyPermissions.hasPermissions(this, permissions)) {
            //已经打开权限
            chooseSMultiPhotos();
        } else {
            //没有打开相关权限、申请权限
            getPermission();
        }
    }

    private void chooseSMultiPhotos() {
        // 初始化相册引擎
        EasyPhotos.createAlbum(this, false, GlideEngine.getInstance())
                .setCount(20)        // 设置最大选取张数
                .setCleanMenu(false)
                .setPuzzleMenu(false)
                .start(MULTI_REQUEST_CODE);
    }

其中GlideEngine类我会在文末尾给出。

2.6 回调处理

/**
 * 处理相机或者相册的回调
 *
 * @param requestCode 请求码
 * @param resultCode  结果码
 * @param data        传出data
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    // 相机回调处理
    if (requestCode == CAMERA_REQUEST_CODE && resultCode == RESULT_CANCELED) {
        Toast.makeText(this,"拍照完成",Toast.LENGTH_SHORT).show();
    }

    // 单张回调处理
    if (requestCode == SINGLE_REQUEST_CODE && resultCode == RESULT_OK) {
        String photoPath = GetPhotoFromAlbum.getRealPathFromUri(this,
                data != null ? data.getData() : null);
        Toast.makeText(this,"选择的照片路径为"+photoPath,Toast.LENGTH_SHORT).show();
    }

        // 多张回调处理
    if (requestCode == MULTI_REQUEST_CODE && resultCode == RESULT_OK) {
        // 获取选择图片集合路径
        ArrayList<String> photosPath = data.getStringArrayListExtra(EasyPhotos.RESULT_PATHS);
        Toast.makeText(this, String.format("共选择了%d张图片", photosPath.size()),Toast.LENGTH_SHORT).show();
    }
    super.onActivityResult(requestCode, resultCode, data);
}

3. 相册、相机处理中的FAQ

Q1: 为什么我拍照后相册没有得到更新?

A1: 可以通过以下方法更新:

// 通知图库更新
MediaScannerConnection.scanFile(MainActivity.this, new String[]{path}, null,
    new MediaScannerConnection.OnScanCompletedListener() {
    public void onScanCompleted(String path, Uri uri) {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    mediaScanIntent.setData(uri);
    sendBroadcast(mediaScanIntent);
    }
});

其中path参数就是你新拍照后的照片路径。

Q2: 有没有办法设置相机拍照后的相片到一个指定相册?

A2: 说实话这也让我挺头疼的问题。因为在我搜索出来的结果中,Android似乎并没有提供这样的Api给我们。最后我的实现方法是将刚拍照的图片找出来移动到需要的目录去。这里给一个思路。

  1. 如何找到刚拍照的照片?可以通过时间戳的方式。在进入相机时记录一下时间,退出相机时记录一下时间,然后到系统的相册集中去找在这个时间段的所有照片。
  2. 使用Java IO的相关API去移动图片,注意最好在线程里面做,避免因为移动过多图片造成主线程卡死。

4. 代码中用到的其他文件

GlideEngine.java

package com.raven.mnist_recon;
import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.huantansheng.easyphotos.engine.ImageEngine;

import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;

/**
 * Glide4.x的加载图片引擎实现,单例模式
 * Glide4.x的缓存机制更加智能,已经达到无需配置的境界。如果使用Glide3.x,需要考虑缓存机制。
 * Created by huan on 2018/1/15.
 */

public class GlideEngine implements ImageEngine {
    //单例
    private static GlideEngine instance = null;
    //单例模式,私有构造方法
    private GlideEngine() {
    }
    //获取单例
    public static GlideEngine getInstance() {
        if (null == instance) {
            synchronized (GlideEngine.class) {
                if (null == instance) {
                    instance = new GlideEngine();
                }
            }
        }
        return instance;
    }

    /**
     * 加载图片到ImageView
     *
     * @param context   上下文
     * @param photoPath 图片路径
     * @param imageView 加载到的ImageView
     */
    @Override
    public void loadPhoto(Context context, String photoPath, ImageView imageView) {
        Glide.with(context).load(photoPath).transition(withCrossFade()).into(imageView);
    }

    /**
     * 加载gif动图图片到ImageView,gif动图不动
     *
     * @param context   上下文
     * @param gifPath   gif动图路径
     * @param imageView 加载到的ImageView
     *                  <p>
     *                  备注:不支持动图显示的情况下可以不写
     */
    @Override
    public void loadGifAsBitmap(Context context, String gifPath, ImageView imageView) {
        Glide.with(context).asBitmap().load(gifPath).into(imageView);
    }

    /**
     * 加载gif动图到ImageView,gif动图动
     *
     * @param context   上下文
     * @param gifPath   gif动图路径
     * @param imageView 加载动图的ImageView
     *                  <p>
     *                  备注:不支持动图显示的情况下可以不写
     */
    @Override
    public void loadGif(Context context, String gifPath, ImageView imageView) {
        Glide.with(context).asGif().load(gifPath).transition(withCrossFade()).into(imageView);
    }


    /**
     * 获取图片加载框架中的缓存Bitmap
     *
     * @param context 上下文
     * @param path    图片路径
     * @param width   图片宽度
     * @param height  图片高度
     * @return Bitmap
     * @throws Exception 异常直接抛出,EasyPhotos内部处理
     */
    @Override
    public Bitmap getCacheBitmap(Context context, String path, int width, int height) throws Exception {
        return Glide.with(context).asBitmap().load(path).submit(width, height).get();
    }


}

AlbumUtil.java

package com.raven.demo;

import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;

public class AlbumUtil {
    /**
     * 根据Uri获取图片的绝对路径
     *
     * @param context 上下文对象
     * @param uri     图片的Uri
     * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
     */
    public static String getRealPathFromUri(Context context, Uri uri) {
        int sdkVersion = Build.VERSION.SDK_INT;
        if (sdkVersion >= 19) { 
            return getRealPathFromUriAboveApi19(context, uri);
        } else {
            return getRealPathFromUriBelowAPI19(context, uri);
        }
    }

    /**
     * 适配api19以下(不包括api19),根据uri获取图片的绝对路径
     *
     * @param context 上下文对象
     * @param uri     图片的Uri
     * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
     */
    private static String getRealPathFromUriBelowAPI19(Context context, Uri uri) {
        return getDataColumn(context, uri, null, null);
    }

    /**
     * 适配api19及以上,根据uri获取图片的绝对路径
     *
     * @param context 上下文对象
     * @param uri     图片的Uri
     * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
     */
    @SuppressLint("NewApi")
    private static String getRealPathFromUriAboveApi19(Context context, Uri uri) {
        String filePath = null;
        if (DocumentsContract.isDocumentUri(context, uri)) {
            // 如果是document类型的 uri, 则通过document id来进行处理
            String documentId = DocumentsContract.getDocumentId(uri);
            if (isMediaDocument(uri)) {
                // 使用':'分割
                String id = documentId.split(":")[1];

                String selection = MediaStore.Images.Media._ID + "=?";
                String[] selectionArgs = {id};
                filePath = getDataColumn(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, selectionArgs);
            } else if (isDownloadsDocument(uri)) { 
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId));
                filePath = getDataColumn(context, contentUri, null, null);
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            // 如果是 content 类型的 Uri
            filePath = getDataColumn(context, uri, null, null);
        } else if ("file".equals(uri.getScheme())) {
            // 如果是 file 类型的 Uri,直接获取图片对应的路径
            filePath = uri.getPath();
        }
        return filePath;
    }

    /**
     * 获取数据库表中的 _data 列,即返回Uri对应的文件路径
     *
     */
    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
        String path = null;

        String[] projection = new String[]{MediaStore.Images.Media.DATA};
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                int columnIndex = cursor.getColumnIndexOrThrow(projection[0]);
                path = cursor.getString(columnIndex);
            }
        } catch (Exception e) {
            if (cursor != null) {
                cursor.close();
            }
        }
        return path;
    }

    /**
     * @param uri the Uri to check
     * @return Whether the Uri authority is MediaProvider
     */
    private static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri the Uri to check
     * @return Whether the Uri authority is DownloadsProvider
     */
    private static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }
}

本文所有代码都已上传至github,有需求朋友可自行下载查看。

地址:https://github.com/raven-misc/Android-Camera-Album

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »