一个多月没总结知识点了,差点连博客账号都忘了。。。好了,步入正题,在 Android 中调用摄像头拍照获取图片或者是从相册中选取图片是很常见的功能,比如某些 APP 上传头像的功能就是一个例子。 **因为 Android 7.0 的新特性规定,不同的应用之间不能再使用 file:// 类型的 Uri 共享数据了,否则会报异常,这就就是网上说的 Android 7.0 调用相机拍照崩溃的问题。官方推荐的做法是使用 FileProvider 来实现,**下面来看一下怎么实现这个 APP 中常见的功能:
一般来说,我们通过拍照来获取图片有以下步骤:
调用系统相机拍照 --> 调用系统裁剪程序裁剪裁照片并输出到指定目录 --> 读取裁剪后的图片
获取从相册选择的图片:
调用系统图库 --> 得到选择的图片的 Uri 并裁剪图片然后输出到指定目录 --> 读取裁剪后的图片
这里可能有些小伙伴会问了,为什么要调用系统的裁剪程序裁剪照片呢?因为现在的手机大多数像素比较高,拍出来的照片比较大,如果直接读取拍照后的照片,那么可能会发生栈溢出(就是应用内存不够用)。因此要将图片裁剪之后再读取。可能有些小伙伴对 Uri 的概念还不是很清楚,Uri 通俗来说就是指向某个文件的路径,可以看成文件绝对路径封装后的一个对象,我们可以通过 Uri 来访问其指向的文件。
这里还需要注意的是,Android 6.0 以后,有了新增了危险权限的概念,就是我们在使用这类权限的时候不仅要在 AndroidManifest 文件中声明,我们还需要在使用的时候向用户申请这个权限。如果对这个还不了解的小伙伴可以参考一下这篇文章:http://blog.csdn.net/hacker_zhidian/article/details/56058460
Ok,我们根据上面的步骤来看一下: 新建一个 Android 工程:
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
tools:context="com.company.zhidian.usercameraandalbum.MainActivity">
<Button
android:id="@+id/startCameraButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照获取图片"/>
<Button
android:id="@+id/choiceFromAlbumButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="从相册选择"/>
<ImageView
android:id="@+id/pictureImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
简单的布局,下面是 MainActivity.java:
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private Button startCameraButton = null;
private Button choiceFromAlbumButton = null;
private ImageView pictureImageView = null;
private static final int TAKE_PHOTO_PERMISSION_REQUEST_CODE = 0; // 拍照的权限处理返回码
private static final int WRITE_SDCARD_PERMISSION_REQUEST_CODE = 1; // 读储存卡内容的权限处理返回码
private static final int TAKE_PHOTO_REQUEST_CODE = 3; // 拍照返回的 requestCode
private static final int CHOICE_FROM_ALBUM_REQUEST_CODE = 4; // 相册选取返回的 requestCode
private static final int CROP_PHOTO_REQUEST_CODE = 5; // 裁剪图片返回的 requestCode
private Uri photoUri = null;
private Uri photoOutputUri = null; // 图片最终的输出文件的 Uri
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startCameraButton = (Button) findViewById(R.id.startCameraButton);
startCameraButton.setOnClickListener(clickListener);
choiceFromAlbumButton = (Button) findViewById(R.id.choiceFromAlbumButton);
choiceFromAlbumButton.setOnClickListener(clickListener);
pictureImageView = (ImageView) findViewById(R.id.pictureImage);
/*
* 先判断用户以前有没有对我们的应用程序允许过读写内存卡内容的权限,
* 用户处理的结果在 onRequestPermissionResult 中进行处理
*/
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 申请读写内存卡内容的权限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_SDCARD_PERMISSION_REQUEST_CODE);
}
}
private View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// 调用相机拍照
if(v == startCameraButton) {
// 同上面的权限申请逻辑
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
/*
* 下面是对调用相机拍照权限进行申请
*/
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA,}, TAKE_PHOTO_PERMISSION_REQUEST_CODE);
} else {
startCamera();
}
// 从相册获取
} else if(v == choiceFromAlbumButton) {
choiceFromAlbum();
}
}
};
/**
* 拍照
*/
private void startCamera() {
/**
* 设置拍照得到的照片的储存目录,因为我们访问应用的缓存路径并不需要读写内存卡的申请权限,
* 因此,这里为了方便,将拍照得到的照片存在这个缓存目录中
*/
File file = new File(getExternalCacheDir(), "image.jpg");
try {
if(file.exists()) {
file.delete();
}
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
/**
* 因 Android 7.0 开始,不能使用 file:// 类型的 Uri 访问跨应用文件,否则报异常,
* 因此我们这里需要使用内容提供器,FileProvider 是 ContentProvider 的一个子类,
* 我们可以轻松的使用 FileProvider 来在不同程序之间分享数据(相对于 ContentProvider 来说)
*/
if(Build.VERSION.SDK_INT >= 24) {
photoUri = FileProvider.getUriForFile(this, "com.zhi_dian.provider", file);
} else {
photoUri = Uri.fromFile(file); // Android 7.0 以前使用原来的方法来获取文件的 Uri
}
// 打开系统相机的 Action,等同于:"android.media.action.IMAGE_CAPTURE"
Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 设置拍照所得照片的输出目录
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePhotoIntent, TAKE_PHOTO_REQUEST_CODE);
}
/**
* 从相册选取
*/
private void choiceFromAlbum() {
// 打开系统图库的 Action,等同于: "android.intent.action.GET_CONTENT"
Intent choiceFromAlbumIntent = new Intent(Intent.ACTION_GET_CONTENT);
// 设置数据类型为图片类型
choiceFromAlbumIntent.setType("image/*");
startActivityForResult(choiceFromAlbumIntent, CHOICE_FROM_ALBUM_REQUEST_CODE);
}
/**
* 裁剪图片
*/
private void cropPhoto(Uri inputUri) {
// 调用系统裁剪图片的 Action
Intent cropPhotoIntent = new Intent("com.android.camera.action.CROP");
// 设置数据Uri 和类型
cropPhotoIntent.setDataAndType(inputUri, "image/*");
// 授权应用读取 Uri,这一步要有,不然裁剪程序会崩溃
cropPhotoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 设置图片的最终输出目录
cropPhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT,
photoOutputUri = Uri.parse("file:////sdcard/image_output.jpg"));
startActivityForResult(cropPhotoIntent, CROP_PHOTO_REQUEST_CODE);
}
/**
* 在这里进行用户权限授予结果处理
* @param requestCode 权限要求码,即我们申请权限时传入的常量
* @param permissions 保存权限名称的 String 数组,可以同时申请一个以上的权限
* @param grantResults 每一个申请的权限的用户处理结果数组(是否授权)
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
// 调用相机拍照:
case TAKE_PHOTO_PERMISSION_REQUEST_CODE:
// 如果用户授予权限,那么打开相机拍照
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
Toast.makeText(this, "拍照权限被拒绝", Toast.LENGTH_SHORT).show();
}
break;
// 打开相册选取:
case WRITE_SDCARD_PERMISSION_REQUEST_CODE:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else {
Toast.makeText(this, "读写内存卡内容权限被拒绝", Toast.LENGTH_SHORT).show();
}
break;
}
}
/**
* 通过这个 activity 启动的其他 Activity 返回的结果在这个方法进行处理
* 我们在这里对拍照、相册选择图片、裁剪图片的返回结果进行处理
* @param requestCode 返回码,用于确定是哪个 Activity 返回的数据
* @param resultCode 返回结果,一般如果操作成功返回的是 RESULT_OK
* @param data 返回对应 activity 返回的数据
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(resultCode == RESULT_OK) {
// 通过返回码判断是哪个应用返回的数据
switch (requestCode) {
// 拍照
case TAKE_PHOTO_REQUEST_CODE:
cropPhoto(photoUri);
break;
// 相册选择
case CHOICE_FROM_ALBUM_REQUEST_CODE:
cropPhoto(data.getData());
break;
// 裁剪图片
case CROP_PHOTO_REQUEST_CODE:
File file = new File(photoOutputUri.getPath());
if(file.exists()) {
Bitmap bitmap = BitmapFactory.decodeFile(photoOutputUri.getPath());
pictureImageView.setImageBitmap(bitmap);
// file.delete(); // 选取完后删除照片
} else {
Toast.makeText(this, "找不到照片", Toast.LENGTH_SHORT).show();
}
break;
}
}
}
}
代码看起来多,但其实思路已经在文章开头已经讲了,因为我们的裁剪照片肯定要写内存卡,于是我们在 onCreate 方法中向用户申请授权写内存卡内容的权限。然后我们对两个按钮的点击事件进行处理,分别对应于拍照和从相册选择图片,因为调用系统相机拍照是危险权限,所以我们要向用户申请授权,这两个权限的授权结果会回调 onRequestPermissionsResult 方法,因此我们在这个方法中对用户的授权结果进行处理。
在 startCamera 方法中,我们对运行设备的 Android 版本进行判断,如果是 7.0 以上,那么我们需要使用 FileProvider 来获取照片输出的 Uri,否则的话用以前的方法获取就行了。 而 choiceFromAlbum 方法的逻辑就简单了,就是打开系统图库来给用户选取图片。
这两个方法的返回结果我们在 onActivityResult 方法中进行处理,这个方法当别的 Activity 返回给当前Activity 数据的时候就会被回调(即通过调用 startActivityForResult 方法来启动别的 Activity 时)。
对于拍照的结果,photoUri 代表的是拍照得到的照片的 Uri,而对于从相册中选择图片,其返回的 Intent 类型的数据中就是用户选取的图片的 Uri,因为我们通过 data.getData() 方法来获取这个 Uri, 最后,对于两种获取图片的方法,我们都需要调用 cropPhoto 方法来对得到的图片进行裁剪,并且最后裁剪的图片输出到内存卡中以便读取。
对于 cropPhoto 方法的返回结果,我们用一个 File 对象来判断图片是不是存在,如果存在我们将其读取,这里笔者把 file.delete();
注释掉了,以便于待会观察裁剪得到的图片,如果没有注释掉的话我们用完这个图片之后,它就会被删除。
因为我们在 startCamera 方法中使用了 FileProvider ,既然使用了 ContentProvider ,那么肯定要对其进行注册了:
<provider
android:authorities="com.zhi_dian.provider"
android:name="android.support.v4.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path">
</meta-data>
</provider>
其中,android:authorities 的值要和
photoUri = FileProvider.getUriForFile(this, "com.zhi_dian.provider", file);
的第二个参数值相同,android:name 的值是固定的,因为这是我们使用的 FileProvider 的来源
<meta-data
中 android:resource 的值是我们在 res 文件夹下创建的 xml 文件夹的 file_path.xml 文件:
下面是 file_path 文件的内容:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="image" path="" />
</paths>
其中, name 属性可以随便填,path 属性代表FileProvider 共享的文件路径,空字符串代表共享 sd 卡上的所有文件,当然你也可以填我们拍照得到的图片路径,这样就是只共享我们拍照的到的照片。 最后,当然,别忘了在 Androidmanifest 中申请拍照和写内存卡内容的权限:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Ok,主要的内容就是这些了,下面来看一下运行结果:
其实应用启动的时候就会有一次权限申请,即对写内存卡的权限的申请,但是由于录制软件问题没录到。
同时我们可以在设备的文件管理中看到这个文件:
好了,一个简单的通过拍照和相册选取照片的应用就完成了,通过这个,我们可以完成一些 APP 中常用的功能。 如果博客中有什么不正确的地方,还请多多指点,如果觉得我写的不错,那么请点个赞支持我吧。下面是上面例子的源码:
Android 中拍照、相册选择、裁剪照片(兼容Android 7.0)
谢谢观看。。。