昨天也是为你们分享了 7.0 相机适配,今天就来为你们讲讲 Android 之相机适配。git
提起 Android 调用系统相机拍照上传图片或者是显示图片,想必任何一位开发 Android 的朋友都不会陌生,基本这个功能已经涵盖各个应用了,今天,我就来给你们聊聊网上并很少见却有常常听到你们吐槽的问题。github
根据相机适配对图片的操做,因此有了这款图片压缩库:[https://github.com/nanchen2251/CompressHelper微信
对于拍照功能的实现方式我这里就很少谈了,无非两种,一种是利用相机的 API 来自定义相机,另外一种是利用 Intent 调用系统指定的相机拍照。而这两种方式的实现网上搜索一大把,我就不在这里啰嗦了。markdown
前面讲到咱们是调用系统指定的相机 APP 来拍照,那么系统是否存在能够被咱们调用的 APP 呢?这个咱们不敢肯定,毕竟 Android 奇葩问题多,还真有遇到过这种极端的状况致使闪退的。虽然很极端,但做为客户端人员仍是要进行处理,方式有二:app
try-catch 这种粗暴的方式你们确定很熟悉了,那么要如何检测系统有没有相机 APP 可用呢?系统在 PackageManager 里为咱们提供这样一个 API:框架
经过这样一个 API ,能够知道系统是否存在 Action 为 MediaStore.ACTION_IMAGE_CAPTURE
的 Intent 能够唤起的拍照界面,具体实现代码以下:ide
/** * 判断系统中是否存在能够启动的相机应用 * * @return 存在返回true,不存在返回false */ public boolean hasCamera() { PackageManager packageManager = mActivity.getPackageManager(); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return list.size() > 0; }
常常会遇到一种状况,拍照时看到照片是正的,可是当咱们的 APP 获取到这张照片时,却发现旋转了 90 度(也有多是 180、270,不过 90 度比较多见,貌似都是因为手机传感器致使的)。不少童鞋对此感到很困扰,由于不是全部手机都会出现这种状况,就算会是出现这种状况的手机上,也并不是每次必现。要怎么解决这个问题呢?从解决的思路上看,只要获取到照片旋转的角度,利用 Matrix 来进行角度纠正便可。那么问题来了,要怎么知道照片旋转的角度呢?细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据。
post
ExifInterface
类来知足获取图片各个属性的操做。
ExifInterface
类拿到
TAG_ORIENTATION
属性对应的值,即为咱们想要获得旋转角度。再根据利用
Matrix
进行旋转纠正便可。实现代码大体以下:
/** * 获取图片的旋转角度 * * @param path 图片绝对路径 * @return 图片的旋转角度 */ public static int getBitmapDegree(String path) { int degree = 0; try { // 从指定路径下读取图片,并获取其EXIF信息 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 bitmap 须要旋转的图片 * @param degree 指定的旋转角度 * @return 旋转后的图片 */ public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) { // 根据旋转角度,生成旋转矩阵 Matrix matrix = new Matrix(); matrix.postRotate(degree); // 将原始图片按照旋转矩阵进行旋转,并获得新的图片 Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } return newBitmap; }
ExifInterface
能拿到的信息远远不止旋转角度,其余的参数感兴趣的童鞋能够看看 API 文档。测试
曾在小米和魅族的某些机型上遇到过这样的问题,调用系统相机拍照,拍完点击肯定回到本身的 APP 里面却莫名奇妙的闪退了。这种闪退有两个特色:
没有什么错误日志(有些机子啥日志都没有,有些机子会出来个空异常错误日志);
同个机子上非必现(有时候怎么拍都不闪退,有时候一拍就闪退);优化
对待非必现问题每每比较头疼,当初遇到这样的问题也是很是不解。上网搜罗了一圈也没方案,后来留意到一个比较有意思信息:有些系统厂商的 ROM 会给自带相机应用作优化,当某个 APP 经过 Intent 进入相机拍照界面时,系统会把这个 APP 当前最上层的 Activity 销毁回收。(注意:我遇到的状况是有时候很快就回收掉,有时候怎么等也不回收,没有什么必现规律)为了验证一下,便在启动相机的 Activity 中对 onDestory()
方法进行加 Log 。果不其然,终于发现进入拍照界面的时候 onDestory()
方法被执行了。因此,前面提到的闪退基本能够推测是 Activity 被回收致使某些非UI控件的成员变量为空致使的。(有些机子会报出空异常错误日志,可是有些机子闪退了什么都不报,是否是以为很奇葩!)
既然涉及到 Activity 被回收的问题,天然要想起 onSaveInstanceState()
和 onRestoreInstanceState()
这对方法。去到 onSaveInstanceState()
把数据保存,并在 onRestoreInstanceState()
方法中进行恢复便可。大致代码思路以下:
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mRestorePhotoFile = mCapturePhotoHelper.getPhoto(); if (mRestorePhotoFile != null) { outState.putSerializable(EXTRA_RESTORE_PHOTO, mRestorePhotoFile); } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mRestorePhotoFile = (File) savedInstanceState.getSerializable(EXTRA_RESTORE_PHOTO); mCapturePhotoHelper.setPhoto(mRestorePhotoFile); }
对于 onSaveInstanceState()
和 onRestoreInstanceState()
方法的做用还不熟悉的童鞋,网上资料不少,能够自行搜索。
到这里,可能有童鞋要问,这种闪退并不能保证复现,我要怎么知道问题所在和是否修复了呢?咱们能够去到开发者选项里开启不保留活动这一项进行调试验证。
onActivityResult()
方法中获取拍下来的照片,并启动跳转到 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 的预览界面却怎么也没法显示出照片来,一样是至关郁闷,郁闷完后仍是要一步一步去排查解决问题的!为此,须要一步一步猜想验证问题所在。
OpenGLRenderer: Bitmap too large to be uploaded into a texture
每次拍完照片,都会出现上面这样的 log ,果真,由于图片太大而致使在 ImageView 上没法显示。到这里有童鞋要吐槽了,没对图片的采样率 inSampleSize
作处理?天地良心啊,绝对作处理了,直接看代码:
/** * 压缩Bitmap的大小 * * @param imagePath 图片文件路径 * @param requestWidth 压缩到想要的宽度 * @param requestHeight 压缩到想要的高度 * @return */ public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { if (!TextUtils.isEmpty(imagePath)) { if (requestWidth <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅得到图片宽高 BitmapFactory.decodeFile(imagePath, options); options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imagePath, options); } else { return null; } } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; Log.i(TAG, "height: " + height); Log.i(TAG, "width: " + width); if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } long totalPixels = width * height / inSampleSize; final long totalReqPixelsCap = reqWidth * reqHeight * 2; while (totalPixels > totalReqPixelsCap) { inSampleSize *= 2; totalPixels /= 2; } } return inSampleSize; }
瞄了代码后,是否是以为没有问题了?没错,inSampleSize
确确实实通过处理,那为何图片仍是太大而显示不出来呢? requestWidth
、requestHeight
设置得太大致使 inSampleSize
过小了?不可能啊,我都试着把长宽都设置成 100 了仍是无法显示!干脆,直接打印 inSampleSize
值,一打印,inSampleSize
值竟然为 1 。 我去,完全打脸了,明明说好的处理过了,竟然仍是 1 !!!!为了一探究竟,干脆加 log 。
public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { if (!TextUtils.isEmpty(imagePath)) { Log.i(TAG, "requestWidth: " + requestWidth); Log.i(TAG, "requestHeight: " + requestHeight); if (requestWidth <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅得到图片宽高 BitmapFactory.decodeFile(imagePath, options); Log.i(TAG, "original height: " + options.outHeight); Log.i(TAG, "original width: " + options.outWidth); options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 Log.i(TAG, "inSampleSize: " + options.inSampleSize); options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imagePath, options); } else { return null; } }
运行打印出来的日志以下:
inSampleSize
通过处理以后结果仍是 1 。狠狠的吐槽了以后,老是要回来解决问题的。那么,图片的宽高信息都丢失了,我去哪里找啊? 像下面这样?
public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { ... BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅得到图片宽高 Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options); bitmap.getWidth(); bitmap.getHeight(); ... } else { return null; } }
no,此方案行不通,inJustDecodeBounds = true
时,BitmapFactory
得到 Bitmap 对象是 null;
那要怎样才能获图片的宽高呢?前面提到的 ExifInterface
再次帮了咱们大忙,经过它的下面两个属性便可拿到图片真正的宽高。
TAG_IMAGE_HEIGHT
而是
TAG_IMAGE_LENGTH
。改良事后的代码实现以下:
public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { if (!TextUtils.isEmpty(imagePath)) { Log.i(TAG, "requestWidth: " + requestWidth); Log.i(TAG, "requestHeight: " + requestHeight); if (requestWidth <= 0 || requestHeight <= 0) { Bitmap bitmap = BitmapFactory.decodeFile(imagePath); return bitmap; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//不加载图片到内存,仅得到图片宽高 BitmapFactory.decodeFile(imagePath, options); Log.i(TAG, "original height: " + options.outHeight); Log.i(TAG, "original width: " + options.outWidth); if (options.outHeight == -1 || options.outWidth == -1) { try { ExifInterface exifInterface = new ExifInterface(imagePath); int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 Log.i(TAG, "exif height: " + height); Log.i(TAG, "exif width: " + width); options.outWidth = width; options.outHeight = height; } catch (IOException e) { e.printStackTrace(); } } options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 Log.i(TAG, "inSampleSize: " + options.inSampleSize); options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imagePath, options); } else { return null; } }
再看一下,打印出来的 log
以上总结了这么些身边童鞋常常问起,但网上又很少见的适配问题,但愿能够帮到一些开发童鞋少走弯路。文中屡次提到小米的机子,并不表明只有MIUI上有这样的问题存在,仅仅只是由于我身边带的几部机子大都是小米的。对待适配问题,在搜索引擎都没法提供多少有效的信息时,咱们只能靠断点、打 log、观察控制台的日志、以及 API 文档来寻找一些蛛丝马迹做为突破口,相信办法总比困难多。
以上内容采自:There
至于为何基本全文 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 为全局变量)
flag = true; time = 0; final String url = PhotoUtil.getImageUrlFromActivityResult(this, imageGridAdapter.uri); if (size <= 0) { // 若是size小于0出现了,则使用加载框 new Thread(new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { showLoading(SendQuestionActivity.this);// 该方法为显示加载框 } }); while (flag) { // 设置延迟步长是0.5s try { Thread.sleep(500); time += 0.5; file = null; file = new File(url); size = file.length(); Log.e(TAG, "onActivityResult: size:" + size); if (size > 0 || time >= 10) { flag = false; } } catch (InterruptedException e) { e.printStackTrace(); } } runOnUiThread(new Runnable() { @Override public void run() { stopLoading();//该方法为取消加载框 } }); } }).start(); } ImageItem imageItem = new ImageItem(); imageItem.ImageId = ImageItem.NEW_ID; imageItem.PhotoPath = url; imageGridAdapter.getmDataList().add(imageItem); imageGridAdapter.notifyDataSetChanged(); 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,说明用户取消了拍照,不显示。不然显示图片,心动不如行动,直接上代码。
flag = true; Log.e(TAG, "onActivityResult: uri:" + imageGridAdapter.uri); if (null != imageGridAdapter.uri) { final String url = PhotoUtil.getImageUrlFromActivityResult(this, imageGridAdapter.uri); Log.e(TAG, "onActivityResult: url:" + url); boolean flag = true; // 小米4 LTE MIUI 7.0 版本下,file的size始终为0;经过提早获取ExifInterface信息,保证文件确实写入到外存 try { ExifInterface exifInterface = new ExifInterface(url); int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 Log.e(TAG, "onActivityResult: height:" + height); Log.e(TAG, "onActivityResult: width:" + width); if (height == 0 && width == 0) { flag = false; } } catch (Exception e) { e.printStackTrace(); } if (!TextUtils.isEmpty(url)) { file = new File(url); size = file.length(); Log.e(TAG, "onActivityResult: size:" + size); /** * MIUI8.0上面方案没法解决,经测试发如今必定时间后能保证size不为0 * 奇怪的发现当size为0的时候依然能够拿到图片,多款手机测试经过 */ if (flag) { ImageItem imageItem = new ImageItem(); imageItem.ImageId = ImageItem.NEW_ID; imageItem.PhotoPath = url; imageGridAdapter.getmDataList().add(imageItem); imageGridAdapter.notifyDataSetChanged(); imageGridAdapter.uri = null; } } }
OK,终于解决了。但愿能帮到你们!
作不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注个人微信公众号,目前多运营 Android ,尽本身所能为你提高。若是你喜欢,为我点赞分享吧~