图片选择器在手机应用中家常便饭,设置头像、聊天传图等常见相似场景都须要使用。为了保持不一样设备上体验的一致性和较好的兼容性,比较稳妥的作法是在应用内自实现相机拍照、相册选图和图片裁剪功能。可是,这个实现过程比较复杂,费时费力。更多时候,或者说在项目初期,咱们都会选择直接调用系统提供的这些功能来完成一个图片选择器。然而,因为安卓设备的多样性,总会遇到各类各样的兼容问题。本文就来总结总结,调用系统相机、相册和裁剪功能实现图片选择器的过程当中,咱们须要注意的一些地方。java
这里简单使用一个示例代码,演示调用系统相机或相册,获取图片,而后使用系统裁剪功能处理图片,并显示到一个 ImageButton 视图里面:android
public class MainActivity extends FragmentActivity {
public static final int REQUEST_CAMERA = 1;
public static final int REQUEST_ALBUM = 2;
public static final int REQUEST_CROP = 3;
public static final String IMAGE_UNSPECIFIED = "image/*";
private ImageButton mPictureIb;
private File mImageFile;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPictureIb = (ImageButton) findViewById(R.id.ib_picture);
}
public void onClickPicker(View v) {
new AlertDialog.Builder(this)
.setTitle("选择照片")
.setItems(new String[]{"拍照", "相册"}, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if (i == 0) {
selectCamera();
} else {
selectAlbum();
}
}
})
.create()
.show();
}
private void selectCamera() {
createImageFile();
if (!mImageFile.exists()) {
return;
}
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);
}
private void selectAlbum() {
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_UNSPECIFIED);
startActivityForResult(albumIntent, REQUEST_ALBUM);
}
private void cropImage(Uri uri){
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(intent, REQUEST_CROP);
}
private void createImageFile() {
mImageFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".jpg");
try {
mImageFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "出错啦", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (RESULT_OK != resultCode) {
return;
}
switch (requestCode) {
case REQUEST_CAMERA:
cropImage(Uri.fromFile(mImageFile));
break;
case REQUEST_ALBUM:
createImageFile();
if (!mImageFile.exists()) {
return;
}
Uri uri = data.getData();
if (uri != null) {
cropImage(uri);
}
break;
case REQUEST_CROP:
mPictureIb.setImageURI(Uri.fromFile(mImageFile));
break;
}
}
}复制代码
效果如图(不一样设备,系统功能呈现有所不一样):程序员
看似完美,你觉得上述代码就能结束了的话,那就大错特错啦!这里面还有一些兼容问题要处理,还有一些地方须要特殊说明。数组
调用系统相机实现拍照功能的核心代码以下:缓存
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);复制代码
其中 MediaStore.EXTRA_OUTPUT
数据表示,拍照所得图片保存到指定目录下的文件(通常会在 SD 卡中建立当前应用的目录,并建立临时文件保存图片)。而后,在 onActivityResult 方法中根据文件路径获取图片。服务器
若是不为 intent 添加该数据的话,将在 onActivityResult 的 intent 对象中返回一个 Bitmap 对象,经过以下代码获取:微信
Bitmap bmp = data.getParcelableExtra("data");复制代码
值得注意的是,这里的 Bitmap 对象是拍照所得图片的一个缩略图,尺寸很小!系统这么作也是充分考虑到应用的内存占用问题。试想一下,现在手机设备中高清相机拍出来的照片,一张图的大小高达十几兆,若是返回这么大的图片,内存占用至关严重,况且不少时候知识临时使用而已。因此,调用系统相机时,通常都会添加 MediaStore.EXTRA_OUTPUT
参数,避免返回 Bitmap 对象。固然,这么作也能保证应用产生的数据,包括文件,都能存储在应用目录下,方便清理缓存时统一清除。网络
部分手机,好比三星手机,调用系统相机拍照所得的照片可能会发生自动旋转问题,常见为旋转 90°。因此,要求咱们在拍照以后,使用图片以前,判断图片是否发生过旋转,若是是,要将照片旋转回来。app
这是获取图片旋转角度的代码:框架
/** * 获取图片旋转角度 * @param path 图片路径 * @return */
private int parseImageDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}复制代码
这是根据指定角度旋转图片的代码:
/** * 图片旋转操做 * * @param bm 须要旋转的图片 * @param degree 旋转角度 * @return 旋转后的图片 */
private Bitmap rotateBitmap(Bitmap bm, int degree) {
Bitmap returnBm = null;
Matrix matrix = new Matrix();
matrix.postRotate(degree);
try {
returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
}
if (returnBm == null) {
returnBm = bm;
}
if (bm != returnBm) {
bm.recycle();
}
return returnBm;
}复制代码
在部分手机,调用系统拍照功能时,可能会发生横竖屏切换过程,致使返回应用时当前 Activity 发生销毁重建,各个生命周期又从新走了一遍。此时,一些应用内的变量数据可能丢失,使用时容易发生空值异常,进而致使 app 崩溃退出。
为了不这种现象,咱们须要在 AndroidManifest.xml 文件的对应 <activity>
标签中添加属性:
android:configChanges="orientation|screenSize"复制代码
这样,当发生屏幕旋转时,不会致使 Activity 销毁重建,而是执行 onConfigurationChanged()
方法:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}复制代码
示例中调用系统裁剪的代码以下:
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(intent, REQUEST_CROP);复制代码
能够看出,调用系统裁剪功能,须要设置一些 Extra 参数,不少人容易在这里产生疑惑,不知如何取舍,如何设值。这里列举一下经常使用的 Extra 名字、值类型和做用:
须要注意的是:
第一,设置 return-data 参数为 true 时,返回的 Bitmap 对象也为缩略图,获取方式与前面所述相机拍照获取 Bitmap 的方式一致;
第二,调用系统相册并裁剪时,若是使用MediaStore.EXTRA_OUTPUT参数,Uri 尽可能不要设置为源文件对应的 Uri 值,另作保存,不损坏系统相册中的源图文件;
第三,根据经验,outputX 与 outputY 值设置太大时,容易出现卡屏现象;
第四,能够不设置 outputX 与 outputY 参数,使用户根据自身按比例自由裁剪,就像示例代码这样。
你可能会用到 setImageURI()
方法给 ImageView 设置图片内容,这里也有一个地方须要注意。咱们先看一下这个方法的源码:
public void setImageURI(Uri uri) {
if (mResource != 0 ||
(mUri != uri &&
(uri == null || mUri == null || !uri.equals(mUri)))) {
updateDrawable(null);
mResource = 0;
mUri = uri;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}复制代码
能够看到,这里的 uri 参数在内部持有缓存变量,当屡次调用该方法而 uri 参数值不变时,图片展现内容不变。问题就在这,若是你屡次拍照或裁剪保存的图片文件路径相同时,虽然每次处理事后实际存储的文件内容发生变化,但因为路径相同,uri 参数一致,致使屡次调用 setImageURI()
设置图片内容时,ImageView 显示内容不变!这也是为何示例代码中我用时间戳处理图片文件名的缘由所在,保证每次存储的图片路径不一样。
有时候,咱们须要根据 Uri 获取文件路径。好比若是你不须要使用裁剪功能的话,调用系统相册选择图片后返回的就是一个 Uri 对象,咱们须要从这个 Uri 对象中解析出对应的图片文件路径,便于上传至服务器等后续处理。
好比,这个 Uri 对象多是:
content://media/external/images/media/3066
不少朋友相信有过这样的经验,使用 toString() 或者 getPath() 方法获取 Uri 对象所对应的文件路径,其实这是错误的!经过 getPath() 获取的结果字符串是:
media/external/images/media/3066
而正确的获取方式是:
private String parseFilePath(Uri uri) {
String[] filePathColumn = { MediaStore.Images.Media.DATA };
Cursor cursor = getContentResolver().query(uri, filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String picturePath = cursor.getString(columnIndex);
cursor.close();
return picturePath;
}复制代码
其对应的文件路径应该是这个样子的:
/storage/emulated/0/Pictures/Screenshots/S70302-131606.jpg
如今不少网络框架内部都作了封装处理,上传图片时只须要传递一个文件路径便可。可是,少数状况下,根据服务器须要,咱们要对图片文件字节流编码后再上传。这是使用 Base64 编码并根据字节数组获取字符串的处理过程:
public static String fileToBase64String(String filePath) {
File photoFile = new File(filePath);
try {
FileInputStream fis = new FileInputStream(photoFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream(10000);
byte[] buffer = new byte[1000];
while (fis.read(buffer)!=-1) {
baos.write(buffer);
}
baos.close();
fis.close();
return Arrays.toString(Base64.encode(baos.toByteArray(), Base64.DEFAULT));
}catch (IOException e) {
e.printStackTrace();
}
return null;
}复制代码
当上传多张图片至服务器时,为了提高传输效率,每每会采用 zip 格式压缩处理。这里提供一个递归压缩代码,方便你们有须要的时候借鉴参考:
public String zipCompass(String filePath){
File zipFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".zip");
try{
//指定了两个待压缩的文件,都在assets目录中
String[] filenames = new String[]{ "activity_main.xml", "strings.xml" };
FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos);
int i = 1;
//枚举filenames中的全部待压缩文件
while (i <= filenames.length){
//从filenames数组中取出当前待压缩的文件名,做为压缩后的名称,以保证压缩先后文件名一致
ZipEntry zipEntry = new ZipEntry(filenames[i - 1]);
//打开当前的zipEntry对象
zos.putNextEntry(zipEntry);
FileInputStream is = new FileInputStream(filePath);
byte[] buffer = new byte[8192];
int count = 0;
//写入数据
while ((count = is.read(buffer)) >= 0){
zos.write(buffer, 0, count);
}
zos.flush();
zos.closeEntry();
is.close();
i++;
}
zos.finish();
zos.close();
return zipFile.getAbsolutePath();
}
catch (Exception e){
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
return null;
}
}复制代码
说了这么多,别忘了在 AndroidManifest.xml 文件中添加系统权限(前面示例代码中没有考虑到 Android 6.0 运行时权限的问题,实际使用时注意添加处理):
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />复制代码
安卓笔记侠:专一于 Android 开发,和程序员的感悟~