概述
与调用系统摄像头拍照类似的,从系统相册选择照片的核心代码也仅仅只是一句呼唤系统Intent:
Intent intent = new Intent("android.intent.action.GET_CONTENT");
不过我们同样也要围绕这句核心代码做很多准备工作。
第一步:获取权限
从系统相册获取照片,需要对存储器的读写操作权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
注意:
比较悲剧的是,这两个权限在Android6.x之后也被划分为高危系统权限,因此仅仅只是在AndroidManifest.xml文件中声明一下是没有用的,还需要转本编写代码进行动态运行时权限申请。
详细信息可以参考我的这篇文章:
在我的应用场景中,我的界面是这个样子的:
我在界面上用TableLayout做了一个容器,每当从相册选择一张照片后,这张照片就会被放置在这个TableLayout中。如果选择了多张照片,那么就会像上图那样形成一个列表。
这样的界面结构其实非常简单,不过我还是把代码粘帖在这里:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context="com.yumi.mibao.easyasset.activities.TakePhotoActivity"
android:background="@color/metro_white">
<Button
android:id="@+id/BUTTON_COMPLETE"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="@color/yumi_red"
android:textColor="@color/metro_white"
android:text="@string/str_complete"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
app:layout_constraintLeft_toRightOf="@+id/BUTTON_TAKE_PHOTO"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/BUTTON_TAKE_PHOTO"
android:layout_width="68dp"
android:layout_height="48dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:background="@color/metro_grass"
android:text="@string/str_take_photo"
android:textColor="@color/metro_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/BUTTON_ALBUM"
android:layout_marginStart="8dp"
android:onClick="takePohto"/>
<Button
android:id="@+id/BUTTON_ALBUM"
android:layout_width="68dp"
android:layout_height="48dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:background="@color/metro_blue"
android:text="@string/str_album"
android:textColor="@color/metro_white"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="8dp"
android:onClick="pickPhotoFromAlbum"
app:layout_constraintLeft_toLeftOf="parent" />
<ScrollView
android:layout_width="0dp"
android:layout_height="495dp"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.0"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/BUTTON_COMPLETE">
<TableLayout
android:id="@+id/TABLE_LAYOUT_TAKE_PHOTO_LIST"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_gravity="top"
android:layout_marginTop="8dp"
android:gravity="top" />
</ScrollView>
</android.support.constraint.ConstraintLayout>
在上面的代码中,可以看到id为“BUTTON_ALBUM”的按钮就对应了图片中的“相册”按钮,在这个按钮的onclick事件中,我触发了一个方法“pickPhotoFromAlbum”,就是这个方法会调起手机的拍照功能。具体实现往下看。
第三步:编写事件触发方法
3.1定义用于Intent回调的请求码
由于呼出系统拍照功能的Intent是采用startActivityForResult的模式来进行,所以需要明确的请求码来标记操作的类型,这样才能够在Intent的回调方法中进行相应的数据处理:
private final int PICK_FROM_ALBUN = 3;//从相册选择
实际上,我们还会用到一个裁剪图片的请求码,定义如下:
private final int CROP_PHOTO = 2;//切图操作
在后续的步骤中,我们会用到这个请求码
3.2定义被选中的照片的保存路径和文件名
/*
* 从相册选择所得到的图像的保存路径
*/
private Uri imageUri;
/*
* 从相册选择的照片的文件名
*/
private String fileName;
3.3编写方法,呼出系统Intent,从相册选择照片
先来看一下选择相册的界面是什么样子的:
/**
* 点击按钮从手机相册中获取图片
* @param view
*/
public void pickPhotoFromAlbum(View view){
/*
* 用时间戳的方式来命名图片文件,这样可以避免文件名称重复
*/
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
Date date = new Date(System.currentTimeMillis());
this.fileName = "easyassetFromAlbum"+format.format(date);
/*
* 创建一个File对象,用于存放选到的照片
*/
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
File outputImage = new File(path,this.fileName+".jpg");
/*
* 以防万一,看一下这个文件是不是存在,如果存在的话,先删除掉
*/
if(outputImage.exists()){
outputImage.delete();
}
try {
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
/*
* 将File对象转换为Uri对象,以便拍照后保存
*/
this.imageUri = Uri.fromFile(outputImage);
/*
* 启动系统的选择界面
*/
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.putExtra("scale", true);//设置可以缩放
intent.putExtra("crop", true);//设置可以裁剪
intent.setType("image/*");//设置需要从系统选择的内容:图片
intent.putExtra(MediaStore.EXTRA_OUTPUT, this.imageUri);//设置输出位置
startActivityForResult(intent, this.PICK_FROM_ALBUN);//开始选择
}
可以看到,在代码的最后,采用startActivityForResult的形式呼出了系统Intent,并且将请求码“PICK_FROM_ALBUM”作为参数发送了过去。因此,我们必须编写onActivityResult方法,来处理回调业务。
3.4编写Intent回调方法
/**
* 因为启动系统Intent使用的forResult模式,因此需要onActivityResult方法来接受回调参数
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
super.onActivityResult(requestCode,resultCode,data);
if (resultCode != RESULT_OK) {
Toast.makeText(this, "获取图片出现错误", Toast.LENGTH_SHORT).show();
}
else{
switch(requestCode) {
/*
* case PICK_FROM_ALBUM 代表从选择相册的intent返回之后
* 完成从相册中选择照片后,就要将图片生成bitmap对象,然后显示在界面上了
*/
case PICK_FROM_ALBUN:
this.cropPhoto(data.getData());
break;
default:
break;
}
}
}
可以看到,在switch中我们捕获了请求码“PICK_FROM_ALBUM”,然后立即调用了 一个方法“cropPhoto”,实际上这是一个用来裁剪图片的方法。
3.5编写图片裁剪方法
/**
* 打开裁剪图片的系统界面
*/
private void cropPhoto(Uri imageUri){
/*
* 准备打开系统自带的裁剪图片的intent
*/
Intent intent = new Intent("com.android.camera.action.CROP"); //打开系统自带的裁剪图片的intent
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("scale", true);
/*
* 设置裁剪区域的宽高比例
*/
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
/*
* 设置裁剪区域的宽度和高度
*/
intent.putExtra("outputX", 340);
intent.putExtra("outputY", 340);
/*
* 指定裁剪完成以后的图片所保存的位置
*/
intent.putExtra(MediaStore.EXTRA_OUTPUT, this.imageUri);
Toast.makeText(this, "剪裁图片", Toast.LENGTH_SHORT).show();
/*
* 以广播方式刷新系统相册,以便能够在相册中找到刚刚所拍摄和裁剪的照片
*/
Intent intentBc = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intentBc.setData(this.imageUri);
this.sendBroadcast(intentBc);
/*
* 以forResult模式启动系统自带的裁剪图片的intent
*/
startActivityForResult(intent, CROP_PHOTO); //设置裁剪参数显示图片至ImageView
}
可以看到,在上述方法的最后,我们启动了系统自带图片裁剪Intent,并且将请求码“CROP_PHOTO”以startActivityForResult的形式传递过去。
手机打开的且图界面是这样的:
3.6在Intent的回调方法中,增加对切图回调的处理
/*
* case CROP_PHOTO 代表从裁剪照片的intent返回之后
* 完成裁剪照片后,就要将图片生成bitmap对象,然后显示在界面上面了
*/
case CROP_PHOTO:
try {
/*
* 将图片转换成Bitmap对象
*/
Bitmap bitmap = BitmapFactory.decodeStream(this.getContentResolver().openInputStream(this.imageUri));
Toast.makeText(this, this.imageUri.toString(), Toast.LENGTH_SHORT).show();
/*
* 在界面上显示图片
*/
this.addPhotoToActivity(bitmap);
} catch(FileNotFoundException e) {
e.printStackTrace();
}
break;
所以,整个回调方法看起来是这样的:
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
super.onActivityResult(requestCode,resultCode,data);
if (resultCode != RESULT_OK) {
Toast.makeText(this, "获取图片出现错误", Toast.LENGTH_SHORT).show();
}
else{
switch(requestCode) {
/*
* case CROP_PHOTO 代表从裁剪照片的intent返回之后
* 完成裁剪照片后,就要将图片生成bitmap对象,然后显示在界面上面了
*/
case CROP_PHOTO:
try {
/*
* 将图片转换成Bitmap对象
*/
Bitmap bitmap = BitmapFactory.decodeStream(this.getContentResolver().openInputStream(this.imageUri));
Toast.makeText(this, this.imageUri.toString(), Toast.LENGTH_SHORT).show();
/*
* 在界面上显示图片
*/
this.addPhotoToActivity(bitmap);
} catch(FileNotFoundException e) {
e.printStackTrace();
}
break;
/*
* case PICK_FROM_ALBUM 代表从选择相册的intent返回之后
* 完成从相册中选择照片后,就要将图片生成bitmap对象,然后显示在界面上了
*/
case PICK_FROM_ALBUN:
this.cropPhoto(data.getData());
break;
default:
break;
}
}
}
可以看到,在完成切图后,直接调用了一个方法“addPhotoToActivity”,来将选择的照片显示在界面上。
3.7显示图片
/**
* 将拍照和裁剪后所得到的照片,罗列在界面上
*/
private void addPhotoToActivity(Bitmap bitMap){
/*
* 首先获取到用来显示照片的容器
* 该容易是一个TableLayout
*/
TableLayout tableLayout = (TableLayout)this.findViewById(R.id.TABLE_LAYOUT_TAKE_PHOTO_LIST);
/*
* 创建一个TableRow对象
* 每一行TableRow对象都用来存放一张照片,以及该照片的上传情况信息
* 将这个TableRow放入TableLayout中
*/
TableRow tableRow = new TableRow(this);
tableRow.setPadding(0,0,0,8);//设置每一行的下间距
tableLayout.addView(tableRow);
/*
* 创建一个ImageView对象
* 将这个对象放入TableRow中
* 并在这个对象上显示刚刚拍照所得到的照片
*/
ImageView imageView = new ImageView(this);
imageView.setLayoutParams(this.photoParams);
imageView.setImageBitmap(bitMap);
tableRow.addView(imageView);
/*
* 创建一个TextView对象
* 为这个对象设置一段“图片正在上传”的提示文字
* 并将这个TextView对象放入TableRow中
*/
TextView textView = new TextView(this);
textView.setLayoutParams(this.uploadStateMsgParam);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setText("正在上传照片...");
textView.setTextColor(ContextCompat.getColor(this,R.color.metro_blue));
tableRow.addView(textView);
}
在上述代码中,会用到两个界面组件的样式定义,如下:
/*
* 一组界面样式,分别是照片在TableRow中所占的宽度比重和照片上传状态的文字信息在TableRow中所占的宽度比重
*/
private TableRow.LayoutParams photoParams;
private TableRow.LayoutParams uploadStateMsgParam;
/**
* 初始化一些界面组件的样式
*/
private void initLayoutParams(){
/*
* 拍照所得到的图片被放置在界面上时,其在TableRow所占的宽度占比
*/
this.photoParams = new TableRow.LayoutParams(
TableRow.LayoutParams.WRAP_CONTENT,
268,
0.1f
);
/*
* 照片上传状态的文字信息被放置在界面上时,其在TableRow所占的宽度占比
*/
this.uploadStateMsgParam = new TableRow.LayoutParams(
TableRow.LayoutParams.WRAP_CONTENT,
268,
0.9f
);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_easy_asset_take_photo);
/*
* 调用方法,初始化界面组件的样式
*/
this.initLayoutParams();
}
至此所有的逻辑就全部实现了。