写在前面
昨天也是为大家分享了 7.0 相机适配,今天就来为大家讲讲 Android 之相机适配。
提起 Android 调用系统相机拍照上传图片或者是显示图片,想必任何一位开发 Android 的朋友都不会陌生,基本这个功能已经涵盖各个应用了,今天,我就来给大家聊聊网上并不多见却有经常听到大家吐槽的问题。
拍照功能实现
对于拍照功能的实现方式我这里就不多谈了,无非两种,一种是利用相机的 API 来自定义相机,另一种是利用 Intent 调用系统指定的相机拍照。而这两种方式的实现网上搜索一大把,我就不在这里啰嗦了。
有没有相机可用?
前面讲到我们是调用系统指定的相机 APP 来拍照,那么系统是否存在可以被我们调用的 APP 呢?这个我们不敢确定,毕竟 Android 奇葩问题多,还真有遇到过这种极端的情况导致闪退的。虽然很极端,但作为客户端人员还是要进行处理,方式有二:
- 调用相机时,简单粗暴的 try-catch
- 调用相机前,检测系统有没有相机 APP 可用
try-catch 这种粗暴的方式大家肯定很熟悉了,那么要如何检测系统有没有相机 APP 可用呢?系统在 PackageManager 里为我们提供这样一个 API:
通过这样一个 API ,可以知道系统是否存在 Action 为 MediaStore.ACTION_IMAGE_CAPTURE
的 Intent 可以唤起的拍照界面,具体实现代码如下:
拍出来的照片“歪了”!!!
经常会遇到一种情况,拍照时看到照片是正的,但是当我们的 APP 获取到这张照片时,却发现旋转了 90 度(也有可能是 180、270,不过 90 度比较多见,貌似都是由于手机传感器导致的)。很多童鞋对此感到很困扰,因为不是所有手机都会出现这种情况,就算会是出现这种情况的手机上,也并非每次必现。要怎么解决这个问题呢?从解决的思路上看,只要获取到照片旋转的角度,利用 Matrix 来进行角度纠正即可。那么问题来了,要怎么知道照片旋转的角度呢?细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据。
没错,这里面就有一个旋转角度,倘若拍照后保存的成像照片文件发生了角度旋转,这个图片的属性参数就能告诉我们到底旋转了多少度。只要获取到这个角度值,我们就能进行纠正的工作了。 Android 系统提供了 ExifInterface
类来满足获取图片各个属性的操作。
通过 ExifInterface
类拿到 TAG_ORIENTATION
属性对应的值,即为我们想要得到旋转角度。再根据利用 Matrix
进行旋转纠正即可。实现代码大致如下:
ExifInterface
能拿到的信息远远不止旋转角度,其他的参数感兴趣的童鞋可以看看 API 文档。
拍完照怎么闪退了?
曾在小米和魅族的某些机型上遇到过这样的问题,调用系统相机拍照,拍完点击确定回到自己的 APP 里面却莫名奇妙的闪退了。这种闪退有两个特点:
没有什么错误日志(有些机子啥日志都没有,有些机子会出来个空异常错误日志);
同个机子上非必现(有时候怎么拍都不闪退,有时候一拍就闪退);
对待非必现问题往往比较头疼,当初遇到这样的问题也是非常不解。上网搜罗了一圈也没方案,后来留意到一个比较有意思信息:有些系统厂商的 ROM 会给自带相机应用做优化,当某个 APP 通过 Intent 进入相机拍照界面时,系统会把这个 APP 当前最上层的 Activity 销毁回收。(注意:我遇到的情况是有时候很快就回收掉,有时候怎么等也不回收,没有什么必现规律)为了验证一下,便在启动相机的 Activity 中对 onDestory()
方法进行加 Log 。果不其然,终于发现进入拍照界面的时候 onDestory()
方法被执行了。所以,前面提到的闪退基本可以推测是 Activity 被回收导致某些非UI控件的成员变量为空导致的。(有些机子会报出空异常错误日志,但是有些机子闪退了什么都不报,是不是觉得很奇葩!)
既然涉及到 Activity 被回收的问题,自然要想起 onSaveInstanceState()
和 onRestoreInstanceState()
这对方法。去到 onSaveInstanceState()
把数据保存,并在 onRestoreInstanceState()
方法中进行恢复即可。大体代码思路如下:
对于 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太大了,无法显示?
直接在 Android Studio 的 log 控制台仔细的观察了一下系统 log ,发现了一些蛛丝马迹Bitmap too large to be uploaded into a texture``` 12每次拍完照片,都会出现上面这样的 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;
}
|
|
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;
}
}
|
|
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;
}
}
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;
}
}
|
|
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;
}
}
}
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;
|
|
// 小米4 LTE MIUI 7.0 版本下,file的size始终为0;通过提前获取ExifInterface信息,保证文件确实写入到外存
try {
ExifInterface exifInterface = new ExifInterface(url);
} catch (Exception e) {
e.printStackTrace();
}
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 资源分享。