你须要知道的Android拍照适配方案

拍照功能实现git

Android 程序上实现拍照功能的方式分为两种:第一种是利用相机的 API 来自定义相机,第二种是利用 Intent 调用系统指定的相机拍照。下面讲的内容都是针对第二种实现方式的适配。github

一般状况下,咱们调用拍照的业务场景是以下面这样的:app

A 界面,点击按钮调用相机拍照; A 界面获得拍完照片,跳转到 B 界面进行预览; B 界面有个按钮,点击后触发某个业务流程来处理这张照片; 实现的大致流程代码以下:ide

//一、调用相机
 File mPhotoFile = new File(folder,filename);
 Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
 Uri fileUri = Uri.fromFile(mPhotoFile);
 captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
 mActivity.startActivityForResult(captureIntent, CAPTURE_PHOTO_REQUEST_CODE);
 
 //二、拿到照片
 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (requestCode == CapturePhotoHelper.CAPTURE_PHOTO_REQUEST_CODE && resultCode == RESULT_OK) {
            File photoFile = mCapturePhotoHelper.getPhoto();//获取拍完的照片
            if (photoFile != null) {
                  PhotoPreviewActivity.preview(this, photoFile);//跳转到预览界面
            }
            finish();
      } else {
            super.onActivityResult(requestCode, resultCode, data);
      }
 }
 
//三、各类各样处理这张图片的业务代码

到这里基本科普完了如何调用系统相机拍照,相信这些网上一搜一大把的代码,不少童鞋都能看懂。post

拍出来的照片“歪了”!!!优化

常常会遇到一种状况,拍照时看到照片是正的,可是当咱们的 app 获取到这张照片时,却发现旋转了 90 度(也有多是180、270,不过90度比较多见,貌似都是因为手机传感器致使的)。不少童鞋对此感到很困扰,由于不是全部手机都会出现这种状况,就算会是出现这种状况的手机上,也并不是每次必现。要怎么解决这个问题呢?从解决的思路上看,只要获取到照片旋转的角度,利用 Matrix 来进行角度纠正便可。那么问题来了,要怎么知道照片旋转的角度呢?细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据this

输入图片说明

没错,这里面就有一个旋转角度,假若拍照后保存的成像照片文件发生了角度旋转,这个图片的属性参数就能告诉咱们到底旋转了多少度。只要获取到这个角度值,咱们就能进行纠正的工做了。 Android 系统提供了 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 方法的做用还不熟悉的童鞋,网上资料不少,能够自行搜索。

到这里,可能有童鞋要问,这种闪退并不能保证复现,我要怎么知道问题所在和是否修复了呢?咱们能够去到开发者选项里开启不保留活动这一项进行调试验证

输入图片说明

它做用是保留当前和用户接触的 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 作处理?天地良心啊,绝对作处理了,直接看代码:

/**
 * 压缩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、int 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;
     }
 }

运行打印出来的日志以下:

输入图片说明

图片原来的宽高竟然都是 -1 ,真是奇葩了!难怪,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 再次帮了咱们大忙,经过它的下面两个属性便可拿到图片真正的宽高

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文档来寻找一些蛛丝马迹做为突破口,相信办法总比困难多。

以上的示例代码已经整理到:https://github.com/D-clock/AndroidStudyCode

相关文章
相关标签/搜索