Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总

书史足自悦,安用勤与劬。这篇文章主要讲述Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总相关的知识,希望能为你提供帮助。

Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总

文章图片

图片选择器在手机应用中屡见不鲜, 设置头像、聊天传图等常见类似场景都需要使用。为了保持不同设备上体验的一致性和较好的兼容性, 比较稳妥的做法是在应用内自实现相机拍照、相册选图和图片裁剪功能。但是, 这个实现过程比较复杂, 费时费力。更多时候, 或者说在项目初期, 我们都会选择直接调用系统提供的这些功能来完成一个图片选择器。然而, 由于安卓设备的多样性, 总会遇到各种各样的兼容问题。本文就来总结总结, 调用系统相机、相册和裁剪功能实现图片选择器的过程中, 我们需要注意的一些地方。
示例代码 这里简单使用一个示例代码, 演示调用系统相机或相册, 获取图片, 然后使用系统裁剪功能处理图片, 并显示到一个 ImageButton 视图里面:
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; } }}

效果如图( 不同设备, 系统功能呈现有所不同) :
Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总

文章图片

看似完美, 你以为上述代码就能结束了的话, 那就大错特错啦! 这里面还有一些兼容问题要处理, 还有一些地方需要特殊说明。
拍照图片存储问题 调用系统相机实现拍照功能的核心代码如下:
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°。所以, 要求我们在拍照之后, 使用图片之前, 判断图片是否发生过旋转, 如果是, 要将照片旋转回来。
这是获取图片旋转角度的代码:
/** * 获取图片旋转角度 * @ 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 名字、值类型和作用:
  • crop: String 类型数据, 发送裁剪信号
  • aspectX 和 aspectY: int 类型数据, 设置裁剪框的 X 与 Y 值比例
  • outputX 和 outputY: int 类型数据, 设置裁剪输出的图片大小
  • scale: boolean 类型数据, 设置是否支持裁剪缩放
  • return-data: boolean 类型数据, 设置是否在 onActivityResult 方法的 intent 值中返回 Bitmap 对象
  • MediaStore.EXTRA_OUTPUT: Uri 类型数据, 设置是否将裁剪结果保存到指定文件中
需要注意的是:
第一, 设置 return-data 参数为 true 时, 返回的 Bitmap 对象也为缩略图, 获取方式与前面所述相机拍照获取 Bitmap 的方式一致;
第二, 调用系统相册并裁剪时, 如果使用MediaStore.EXTRA_OUTPUT参数, Uri 尽量不要设置为源文件对应的 Uri 值, 另做保存, 不损坏系统相册中的源图文件;
第三, 根据经验, outputX 与 outputY 值设置太大时, 容易出现卡屏现象;
第四, 可以不设置 outputX 与 outputY 参数, 使用户根据自身按比例自由裁剪, 就像示例代码这样。
setImageURI() 注意事项 你可能会用到 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 对象中解析出对应的图片文件路径, 便于上传至服务器等后续处理。
比如, 这个 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 文件编码处理 现在很多网络框架内部都做了封装处理, 上传图片时只需要传递一个文件路径即可。但是, 少数情况下, 根据服务器需要, 我们要对图片文件字节流编码后再上传。这是使用 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 压缩文件处理 当上传多张图片至服务器时, 为了提升传输效率, 往往会采用 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 开发, 和程序员的感悟~
【Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总】
Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总

文章图片


    推荐阅读