一、写在前面
前几天也是分享了一些学习必备干货(还没关注的,赶紧入坑:传送门),也好久没有与大家探讨技术方案了,心里也是挺痒痒的,这不,一有点闲暇之时,就迫不及待把最近测出来的坑分享给大家。
提起Android调用系统相机拍照上传图片或者是显示图片,想必任何一位开发Android的朋友都不会陌生,基本这个功能已经涵盖各个应用了,今天,我就来给大家聊聊网上并不多见却有经常听到大家吐槽的问题。
二、拍照功能实现
对于拍照功能的实现方式我这里就不多谈了,无非两种,一种是利用相机的API来自定义相机,另一种是利用Intent调用系统指定的相机拍照。而这两种方式的实现网上搜索一大把,我就不在这里啰嗦了。
三、有没有相机可用?
前面讲到我们是调用系统指定的相机app来拍照,那么系统是否存在可以被我们调用的app呢?这个我们不敢确定,毕竟 Android 奇葩问题多,还真有遇到过这种极端的情况导致闪退的。虽然很极端,但作为客户端人员还是要进行处理,方式有二:
- 调用相机时,简单粗暴的 try-catch
- 调用相机前,检测系统有没有相机 app 可用
try-catch 这种粗暴的方式大家肯定很熟悉了,那么要如何检测系统有没有相机 app 可用呢?系统在 PackageManager 里为我们提供这样一个 API:
通过这样一个 API ,可以知道系统是否存在 action 为 MediaStore.ACTION_IMAGE_CAPTURE 的 intent 可以唤起的拍照界面,具体实现代码如下:
1 /** 2 * 判断系统中是否存在可以启动的相机应用 3 * 4 * @return 存在返回true,不存在返回false 5 */ 6 public boolean hasCamera() { 7 PackageManager packageManager = mActivity.getPackageManager(); 8 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 9 List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 10 return list.size() > 0; 11 }
四、拍出来的照片“歪了”!!!
经常会遇到一种情况,拍照时看到照片是正的,但是当我们的 app 获取到这张照片时,却发现旋转了 90 度(也有可能是180、270,不过90度比较多见,貌似都是由于手机传感器导致的)。很多童鞋对此感到很困扰,因为不是所有手机都会出现这种情况,就算会是出现这种情况的手机上,也并非每次必现。要怎么解决这个问题呢?从解决的思路上看,只要获取到照片旋转的角度,利用 Matrix 来进行角度纠正即可。那么问题来了,要怎么知道照片旋转的角度呢?细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据。
没错,这里面就有一个旋转角度,倘若拍照后保存的成像照片文件发生了角度旋转,这个图片的属性参数就能告诉我们到底旋转了多少度。只要获取到这个角度值,我们就能进行纠正的工作了。 Android 系统提供了 ExifInterface 类来满足获取图片各个属性的操作。
通过 ExifInterface 类拿到 TAG_ORIENTATION 属性对应的值,即为我们想要得到旋转角度。再根据利用 Matrix 进行旋转纠正即可。实现代码大致如下:
1 /** 2 * 获取图片的旋转角度 3 * 4 * @param path 图片绝对路径 5 * @return 图片的旋转角度 6 */ 7 public static int getBitmapDegree(String path) { 8 int degree = 0; 9 try { 10 // 从指定路径下读取图片,并获取其EXIF信息 11 ExifInterface exifInterface = new ExifInterface(path); 12 // 获取图片的旋转信息 13 int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); 14 switch (orientation) { 15 case ExifInterface.ORIENTATION_ROTATE_90: 16 degree = 90; 17 break; 18 case ExifInterface.ORIENTATION_ROTATE_180: 19 degree = 180; 20 break; 21 case ExifInterface.ORIENTATION_ROTATE_270: 22 degree = 270; 23 break; 24 } 25 } catch (IOException e) { 26 e.printStackTrace(); 27 } 28 return degree; 29 } 30 31 /** 32 * 将图片按照指定的角度进行旋转 33 * 34 * @param bitmap 需要旋转的图片 35 * @param degree 指定的旋转角度 36 * @return 旋转后的图片 37 */ 38 public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) { 39 // 根据旋转角度,生成旋转矩阵 40 Matrix matrix = new Matrix(); 41 matrix.postRotate(degree); 42 // 将原始图片按照旋转矩阵进行旋转,并得到新的图片 43 Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); 44 if (bitmap != null && !bitmap.isRecycled()) { 45 bitmap.recycle(); 46 } 47 return newBitmap; 48 }
ExifInterface 能拿到的信息远远不止旋转角度,其他的参数感兴趣的童鞋可以看看 API 文档。
五、拍完照怎么闪退了?
曾在小米和魅族的某些机型上遇到过这样的问题,调用系统相机拍照,拍完点击确定回到自己的app里面却莫名奇妙的闪退了。这种闪退有两个特点:
- 没有什么错误日志(有些机子啥日志都没有,有些机子会出来个空异常错误日志);
- 同个机子上非必现(有时候怎么拍都不闪退,有时候一拍就闪退);
对待非必现问题往往比较头疼,当初遇到这样的问题也是非常不解。上网搜罗了一圈也没方案,后来留意到一个比较有意思信息:有些系统厂商的 ROM 会给自带相机应用做优化,当某个 app 通过 intent 进入相机拍照界面时,系统会把这个 app 当前最上层的 Activity 销毁回收。(注意:我遇到的情况是有时候很快就回收掉,有时候怎么等也不回收,没有什么必现规律)为了验证一下,便在启动相机的 Activity 中对 onDestory 方法进行加 log 。果不其然,终于发现进入拍照界面的时候 onDestory 方法被执行了。所以,前面提到的闪退基本可以推测是 Activity 被回收导致某些非UI控件的成员变量为空导致的。(有些机子会报出空异常错误日志,但是有些机子闪退了什么都不报,是不是觉得很奇葩!)
既然涉及到 Activity 被回收的问题,自然要想起 onSaveInstanceState 和 onRestoreInstanceState 这对方法。去到 onSaveInstanceState 把数据保存,并在 onRestoreInstanceState 方法中进行恢复即可。大体代码思路如下:
1 @Override 2 protected void onSaveInstanceState(Bundle outState) { 3 super.onSaveInstanceState(outState); 4 mRestorePhotoFile = mCapturePhotoHelper.getPhoto(); 5 if (mRestorePhotoFile != null) { 6 outState.putSerializable(EXTRA_RESTORE_PHOTO, mRestorePhotoFile); 7 } 8 9 } 10 11 @Override 12 protected void onRestoreInstanceState(Bundle savedInstanceState) { 13 super.onRestoreInstanceState(savedInstanceState); 14 mRestorePhotoFile = (File) savedInstanceState.getSerializable(EXTRA_RESTORE_PHOTO); 15 mCapturePhotoHelper.setPhoto(mRestorePhotoFile); 16 }
对于 onSaveInstanceState 和 onRestoreInstanceState 方法的作用还不熟悉的童鞋,网上资料很多,可以自行搜索。
到这里,可能有童鞋要问,这种闪退并不能保证复现,我要怎么知道问题所在和是否修复了呢?我们可以去到开发者选项里开启不保留活动这一项进行调试验证。
它作用是保留当前和用户接触的 Activity ,并将目前无法和用户交互 Activity 进行销毁回收。打开这个调试选项就可以满足验证的需求,当你的 app 的某个 Activity 跳转到拍照的 Activity 后,这个 Activity 立马就会被系统销毁回收,这样就可以很好的完全复现闪退的场景,帮助开发者确认问题有没有修复了。
涉及到 Activity 被销毁,还想提一下代码实现上的问题。假设当前有两个 Activity ,MainActivity 中有个 Button ,点击可以调用系统相机拍照并显示到 PreviewActivity 进行预览。有下面两种实现方案:
- 方案一:MainActivity 中点击 Button 后,启动系统相机拍照,并在 MainActivity 的 onActivityResult 方法中获取拍下来的照片,并启动跳转到 PreviewActivity 界面进行效果预览;
- 方案二:MainActivity 中点击 Button 后,启动 PreviewActivity 界面,在 PreviewActivity 的 onCreate(或者onStart、onResume)方法中启动系统相机拍照,然后在 PreviewActivity 的 onActivityResult 方法中获取拍下来的照片进行预览;
上面两种方案得到的实现效果是一模一样的,但是第二种方案却存在很大的问题。因为启动相机的代码放在 onCreate(或者onStart、onResume)中,当进入拍照界面后,PreviewActivity 随即被销毁,拍完照确认后回到 PreviewActivity 时,被销毁的 PreviewActivity 需要重建,又要走一遍 onCreate、onStart、onResume,又调用了启动相机拍照的代码,周而复始的进入了死循环状态。为了避免让你的用户抓狂,果断明智的选择方案一。
以上这种情况提到调用系统拍照时,Activity就回收的情况,在小米4S和小米4 LTE机子上(MIUI的版本是7.3,Android系统版本是6.0)出现的概率很高。 所以,建议看到此文的童鞋也可以去验证适配一下。
六、图片无法显示
图片无法显示这个问题也是略坑,如何坑法?往下看,同样是在小米4S和小米4 LTE机子上(MIUI的版本是7.3,Android系统版本是6.0)出现概率很高的场景(当然,不保证其他机子没出现过)。按照我们前面提到的业务场景,调用相机拍照完成后,我们的 app 会有一个预览图片的界面。但是在用了小米的机子进行拍照后,自己 app 的预览界面却怎么也无法显示出照片来,同样是相当郁闷,郁闷完后还是要一步一步去排查解决问题的!为此,需要一步一步猜测验证问题所在。
- 猜测一:没有拿到照片路径,所以无法显示?
直接断点打 log 跟踪,猜测一很快被推翻,路径是有的。
- 猜测二:Bitmap太大了,无法显示?
直接在 AS 的 log 控制台仔细的观察了一下系统 log ,发现了一些蛛丝马迹
OpenGLRenderer: Bitmap too large to be uploaded into a texture
每次拍完照片,都会出现上面这样的 log ,果然,因为图片太大而导致在 ImageView 上无法显示。到这里有童鞋要吐槽了,没对图片的采样率 inSampleSize 做处理?天地良心啊,绝对做处理了,直接看代码:
1 /** 2 * 压缩Bitmap的大小 3 * 4 * @param imagePath 图片文件路径 5 * @param requestWidth 压缩到想要的宽度 6 * @param requestHeight 压缩到想要的高度 7 * @return 8 */ 9 public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 10 if (!TextUtils.isEmpty(imagePath)) { 11 if (requestWidth <= 0 || requestHeight <= 0) { 12 Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 13 return bitmap; 14 } 15 BitmapFactory.Options options = new BitmapFactory.Options(); 16 options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 17 BitmapFactory.decodeFile(imagePath, options); 18 options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 19 options.inJustDecodeBounds = false; 20 return BitmapFactory.decodeFile(imagePath, options); 21 22 } else { 23 return null; 24 } 25 } 26 27 public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { 28 final int height = options.outHeight; 29 final int width = options.outWidth; 30 int inSampleSize = 1; 31 Log.i(TAG, "height: " + height); 32 Log.i(TAG, " " + width); 33 if (height > reqHeight || width > reqWidth) { 34 35 final int halfHeight = height / 2; 36 final int halfWidth = width / 2; 37 38 while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { 39 inSampleSize *= 2; 40 } 41 42 long totalPixels = width * height / inSampleSize; 43 44 final long totalReqPixelsCap = reqWidth * reqHeight * 2; 45 46 while (totalPixels > totalReqPixelsCap) { 47 inSampleSize *= 2; 48 totalPixels /= 2; 49 } 50 } 51 return inSampleSize; 52 }
瞄了代码后,是不是觉得没有问题了?没错,inSampleSize 确确实实经过处理,那为什么图片还是太大而显示不出来呢? requestWidth、int requestHeight 设置得太大导致 inSampleSize 太小了?不可能啊,我都试着把长宽都设置成 100 了还是没法显示!干脆,直接打印 inSampleSize 值,一打印,inSampleSize 值居然为 1 。 我去,彻底打脸了,明明说好的处理过了,居然还是 1 !!!!为了一探究竟,干脆加 log 。
1 public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 2 if (!TextUtils.isEmpty(imagePath)) { 3 Log.i(TAG, "requestWidth: " + requestWidth); 4 Log.i(TAG, "requestHeight: " + requestHeight); 5 if (requestWidth <= 0 || requestHeight <= 0) { 6 Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 7 return bitmap; 8 } 9 BitmapFactory.Options options = new BitmapFactory.Options(); 10 options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 11 BitmapFactory.decodeFile(imagePath, options); 12 Log.i(TAG, "original height: " + options.outHeight); 13 Log.i(TAG, "original " + options.outWidth); 14 options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 15 Log.i(TAG, "inSampleSize: " + options.inSampleSize); 16 options.inJustDecodeBounds = false; 17 return BitmapFactory.decodeFile(imagePath, options); 18 19 } else { 20 return null; 21 } 22 }
运行打印出来的日志如下:

图片原来的宽高居然都是 -1 ,真是奇葩了!难怪,inSampleSize 经过处理之后结果还是 1 。狠狠的吐槽了之后,总是要回来解决问题的。那么,图片的宽高信息都丢失了,我去哪里找啊? 像下面这样?
1 public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 2 ... 3 BitmapFactory.Options options = new BitmapFactory.Options(); 4 options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 5 Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options); 6 bitmap.getWidth(); 7 bitmap.getHeight(); 8 ... 9 } else { 10 return null; 11 } 12 }
no,此方案行不通,inJustDecodeBounds = true 时,BitmapFactory 获得 Bitmap 对象是 null;那要怎样才能获图片的宽高呢?前面提到的 ExifInterface 再次帮了我们大忙,通过它的下面两个属性即可拿到图片真正的宽高。
顺手吐槽一下,为什么高不是 TAG_IMAGE_HEIGHT 而是 TAG_IMAGE_LENGTH。改良过后的代码实现如下:
1 public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 2 if (!TextUtils.isEmpty(imagePath)) { 3 Log.i(TAG, "requestWidth: " + requestWidth); 4 Log.i(TAG, "requestHeight: " + requestHeight); 5 if (requestWidth <= 0 || requestHeight <= 0) { 6 Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 7 return bitmap; 8 } 9 BitmapFactory.Options options = new BitmapFactory.Options(); 10 options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 11 BitmapFactory.decodeFile(imagePath, options); 12 Log.i(TAG, "original height: " + options.outHeight); 13 Log.i(TAG, "original " + options.outWidth); 14 if (options.outHeight == -1 || options.outWidth == -1) { 15 try { 16 ExifInterface exifInterface = new ExifInterface(imagePath); 17 int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 18 int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 19 Log.i(TAG, "exif height: " + height); 20 Log.i(TAG, "exif " + width); 21 options.outWidth = width; 22 options.outHeight = height; 23 } catch (IOException e) { 24 e.printStackTrace(); 25 } 26 } 27 options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 28 Log.i(TAG, "inSampleSize: " + options.inSampleSize); 29 options.inJustDecodeBounds = false; 30 return BitmapFactory.decodeFile(imagePath, options); 31 32 } else { 33 return null; 34 } 35 }
再看一下,打印出来的log,
以上总结了这么些身边童鞋经常问起,但网上又不多见的适配问题,希望可以帮到一些开发童鞋少走弯路。文中多次提到小米的机子,并不代表只有MIUI上有这样的问题存在,仅仅只是因为我身边带的几部机子大都是小米的。对待适配问题,在搜索引擎都无法提供多少有效的信息时,我们只能靠断点、打log、观察控制台的日志、以及API文档来寻找一些蛛丝马迹作为突破口,相信办法总比困难多。
以上内容基本来自:http://www.jianshu.com/p/f269bcda335f
至于为什么基本全文copy,是因为我觉得作者已经讲的特别清楚了,我没必要做二次重复,也只是给大家分享一下。
那下面就让我来补充一下不一样的开发情景。
七、如果同事写好了压缩九宫格显示图片呢?
这时候你们大框架已经搞定了,只需要你传回一个文件的path,你可能会这样写:(下面所有代码都在onActivityResult方法)
if (null != imageGridAdapter.uri) { final String url = PhotoUtil.getImageUrlFromActivityResult(this, imageGridAdapter.uri);//这个方法可以拿到图片的path Log.e(TAG, "onActivityResult: url:" + url); if (!TextUtils.isEmpty(url)) { file = new File(url); size = file.length(); Log.e(TAG, "onActivityResult: size:" + size); if (size > 0 ){ ImageItem imageItem = new ImageItem(); imageItem.ImageId = ImageItem.NEW_ID; imageItem.PhotoPath = url; imageGridAdapter.getmDataList().add(imageItem); imageGridAdapter.notifyDataSetChanged(); imageGridAdapter.uri = null; } } }
好像没啥问题呀,拿到图片的path,new一个文件,如果文件size=0,则不显示,大于0说明图片存在,则显示图片,提示UI刷新。
嗯,的确,绝大部分手机都测试通过了,然而在坑爹的部分MIUI系统上出现了,返回size为0,进不到判断循环,自然不会显示那个图片。
这时候肯定要到google上去搜上一圈,一圈下来收获不少,却没找到真正解决的办法。
这里有位朋友就说啦,这个size为0,但是间隔一定的时间就可以new出size不为0的file,而这个时间是不固定的。
可见遇上这个坑的小伙伴也是尽心尽力,想方设法。
所以我也尝试了这个方法,考虑到不能休眠主线程,就采用新开一个线程来延时处理,采用加载框,一旦size不为0了,才进行显示处理。
可能图片出问题,所以我们设置一个最大休眠时间。代码为:(其中flag和time为全局变量)
1 flag = true; 2 time = 0; 3 final String url = PhotoUtil.getImageUrlFromActivityResult(this, 4 imageGridAdapter.uri); 5 if (size <= 0) { // 如果size小于0出现了,则使用加载框 6 new Thread(new Runnable() { 7 @Override 8 public void run() { 9 runOnUiThread(new Runnable() { 10 @Override 11 public void run() { 12 showLoading(SendQuestionActivity.this);// 该方法为显示加载框 13 } 14 }); 15 while (flag) { 16 // 设置延迟步长是0.5s 17 try { 18 Thread.sleep(500); 19 time += 0.5; 20 file = null; 21 file = new File(url); 22 size = file.length(); 23 Log.e(TAG, "onActivityResult: size:" + size); 24 if (size > 0 || time >= 10) { 25 flag = false; 26 } 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 } 31 runOnUiThread(new Runnable() { 32 @Override 33 public void run() { 34 stopLoading();//该方法为取消加载框 35 } 36 }); 37 38 } 39 }).start(); 40 } 41 ImageItem imageItem = new ImageItem(); 42 imageItem.ImageId = ImageItem.NEW_ID; 43 imageItem.PhotoPath = url; 44 imageGridAdapter.getmDataList().add(imageItem); 45 imageGridAdapter.notifyDataSetChanged(); 46 imageGridAdapter.uri = null;
这样问题是得到解决啦,但是用户取消的时候size也为0呀,这样无疑是画蛇添足,用户取消拍照都要显示加载框10s,想想都可怕!!!
又听说在new File的前面加上下面的这句话可以解决,倒腾一番,然并卵。
// 小米4 LTE MIUI 7.0 版本下,file的size始终为0;通过提前获取ExifInterface信息,保证文件确实写入到外存 try { ExifInterface exifInterface = new ExifInterface(url); } catch (Exception e) { e.printStackTrace(); }
额,等等。上面说了,ExifInterface可以拿到图片的宽高等参数,那我是不是可以直接通过判断图片宽高是否为0来判断用户是否拍照呢?如果width和height为0,说明用户取消了拍照,不显示。否则显示图片,心动不如行动,直接上代码。
1 flag = true; 2 Log.e(TAG, "onActivityResult: uri:" + imageGridAdapter.uri); 3 if (null != imageGridAdapter.uri) { 4 final String url = PhotoUtil.getImageUrlFromActivityResult(this, 5 imageGridAdapter.uri); 6 Log.e(TAG, "onActivityResult: url:" + url); 7 boolean flag = true; 8 // 小米4 LTE MIUI 7.0 版本下,file的size始终为0;通过提前获取ExifInterface信息,保证文件确实写入到外存 9 try { 10 ExifInterface exifInterface = new ExifInterface(url); 11 int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 12 int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 13 Log.e(TAG, "onActivityResult: height:" + height); 14 Log.e(TAG, "onActivityResult: " + width); 15 if (height == 0 && width == 0) { 16 flag = false; 17 } 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } 21 if (!TextUtils.isEmpty(url)) { 22 file = new File(url); 23 size = file.length(); 24 Log.e(TAG, "onActivityResult: size:" + size); 25 /** 26 * MIUI8.0上面方案无法解决,经测试发现在一定时间后能保证size不为0 27 * 奇怪的发现当size为0的时候依然可以拿到图片,多款手机测试通过 28 */ 29 if (flag) { 30 ImageItem imageItem = new ImageItem(); 31 imageItem.ImageId = ImageItem.NEW_ID; 32 imageItem.PhotoPath = url; 33 imageGridAdapter.getmDataList().add(imageItem); 34 imageGridAdapter.notifyDataSetChanged(); 35 imageGridAdapter.uri = null; 36 } 37 38 } 39 40 }
OK,终于解决了。希望能帮到大家!