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

标签: 需要 知道 android | 发表时间:2016-05-24 10:22 | 作者:iaiai
出处:http://www.iteye.com
说起调用系统相机来拍照的功能,大家肯定不陌生,现在所有应用都具备这个功能。例如最基本的,用户拍照上传头像。Android开发的孩纸都知道,碎片化给拍照这个功能的实现带来挺多头疼的问题。所以,我决定写写一些网上不多见但又经常听到童鞋们吐槽的问题。

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

通常情况下,我们调用拍照的业务场景是如下面这样的:
1.A 界面,点击按钮调用相机拍照;
2.A 界面得到拍完照片,跳转到 B 界面进行预览;
3.B 界面有个按钮,点击后触发某个业务流程来处理这张照片;
实现的大体流程代码如下:
//1. 调用相机  
   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);  
  
   //2. 拿到照片  
   @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);  
       }  
   }  
  
   //3. 各种各样处理这张图片的业务代码

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

有没有相机可用?
前面讲到我们是调用系统指定的相机App来拍照,那么系统是否存在可以被我们调用的App呢?这个我们不敢确定,毕竟 Android 奇葩问题多,还真有遇到过这种极端的情况导致闪退的。虽然很极端,但作为客户端人员还是要进行处理,方式有二:
  • 调用相机时,简单粗暴的 try-catch;
  • 调用相机前,检测系统有没有相机 App 可用;


try-catch 这种粗暴的方式大家肯定很熟悉了,那么要如何检测系统有没有相机 App 可用呢?系统在 PackageManager 里为我们提供这样一个 API。

通过这样一个 API ,可以知道系统是否存在 action 为 MediaStore.ACTION_IMAGE_CAPTURE 的 intent 可以唤起的拍照界面,具体实现代码如下:
/** 
    * 判断系统中是否存在可以启动的相机应用 
    * 
    * @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 来进行角度纠正即可。那么问题来了,要怎么知道照片旋转的角度呢? 细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据。

没错,这里面就有一个旋转角度,倘若拍照后保存的成像照片文件发生了角度旋转,这个图片的属性参数就能告诉我们到底旋转了多少度。只要获取到这个角度值,我们就能进行纠正的工作了。 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里面却莫名奇妙的闪退了。这种闪退有两个特点:

1.没有什么错误日志(有些机子啥日志都没有,有些机子会出来个空异常错误日志);
2.同个机子上非必现(有时候怎么拍都不闪退,有时候一拍就闪退)。
对待非必现问题往往比较头疼,当初遇到这样的问题也是非常不解。上网搜罗了一圈也没方案,后来留意到一个比较有意思信息:有些系统厂商的 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;  
    }

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

图片原来的宽高居然都是 -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 再次帮了我们大忙,通过它的下面两个属性即可拿到图片真正的宽高。

顺手吐槽一下,为什么高不是 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文档来寻找一些蛛丝马迹作为突破口,相信办法总比困难多。

以上的示例代码已经整理到:https://github.com/D-clock/AndroidStudyCode ,主要的代码在下面红圈部分。


感兴趣的童鞋可以自行查看!如有错误,欢迎大家指正!

已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [需要 知道 android] 推荐:

关于Android的.so文件所需要知道的

- - CSDN博客推荐文章
早期的Android系统几乎只支持ARMv5的CPU架构,你知道现在它支持多少种吗. Android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI.

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

- - 移动开发 - ITeye博客
说起调用系统相机来拍照的功能,大家肯定不陌生,现在所有应用都具备这个功能. 例如最基本的,用户拍照上传头像. Android开发的孩纸都知道,碎片化给拍照这个功能的实现带来挺多头疼的问题. 所以,我决定写写一些网上不多见但又经常听到童鞋们吐槽的问题. Android 程序上实现拍照功能的方式分为两种:第一种是利用相机的 API 来自定义相机,第二种是利用 Intent 调用系统指定的相机拍照.

Android M 新的运行时权限开发者需要知道的一切

- - SegmentFault 最新的文章
英文: http://inthecheesefactory.com/blog/things-you-need-to-know-about-android-m-permission-developer-edition/en. 译文: http://jijiaxin89.com/2015/08/30/Android-s-Runtime-Permission/.

Android系统其实不需要极度精简,您未必知道的瘦身之法

- - 一抹新绿
安卓系统其实不需要极度精简,至少我不推荐这种行为,只是一味的精简并不会给系统带来什么好处反而只有害处,因为许多系统应用跟设置里的选项有关联,应当尽量保证系统的完整性. 后台为什么会无缘无故多出很多进程呢. 如果你真的理解安卓进程管理机制和软件架构的话,就会发现各种Receiver,各种CHANGED事件其实才是主因,一切都是软件进程事件触发自行后台启动造成的.

起床特困户的福音,试试就知道了[Android] | 小众软件 > Android

- Stephanie - 小众软件
我微博里有位朋友每天起不来床,每天发微博抱怨. 后来老鼠给他推荐了“起床大作战”,结果是,反正老鼠再没在微博上听到他的抱怨,至于其他内容……保密. 那么,老鼠先介绍两款相似的应用,供大家实现 – “不起床,发微博,求羞辱”的伟大目标. 起床行动派 GetUp(只支持腾讯微博). 2.微博闹钟(只支持新浪微博),可以设置多个闹钟.

Android应用程序需不需要手动退出?

- Jackie - 互联网的那点事
不止一次,也不止一个人问过这个问题. 我们不妨从了解这个系统对于应用程序管理的一些内部机制开始说明原因. 对于Android系统而言,包含“进程”和“服务”. “进程”有正在运行的,也有刚刚离开在后台缓存的. “服务”是一个无界面、长时间运行的应用功能,并且不会轻易被终止. 我们知道,在Android中可以快速通过主页键(home)或者使用返回键(←)逐步离开应用程序.

Android应用程序是否需要手动退出

- Tomy - Tech2IPO
不止一次,也不止一个人问过这个问题. 我们不妨从了解这个系统对于应用程序管理的一些内部机制开始说明原因. 对于Android系统而言,包含“进程”和“服务”. “进程”有正在运行的,也有刚刚离开在后台缓存的. “服务”是一个无界面、长时间运行的应用功能,并且不会轻易被终止. 我们知道,在Android中可以快速通过主页键(home)或者使用返回键(←)逐步离开应用程序.

Android应用程序需不需要手动退出?

- jk - 网易用户体验设计中心博客
不止一次,也不止一个人问过这个问题. 我们不妨从了解这个系统对于应用程序管理的一些内部机制开始说明原因. 对于Android系统而言,包含”进程”和”服务”. ”进程”有正在运行的,也有刚刚离开在后台缓存的. ”服务”是一个无界面、长时间运行的应用功能,并且不会轻易被终止. 我们知道,在Android中可以快速通过主页键(home)或者使用返回键(←)逐步离开应用程序.

Android 4.0编译源代码需要16GB内存

- coofucoo - Solidot
新一代的智能手机也许更轻更小巧,但它们的操作系统则走向另一个极端. Android 4.0 Ice Cream Sandwich(ICS)编译源代码将需要工作站内存不低于16GB,是编译Gingerbread的两倍. 如果开发者想编译ICS,他需要准备:单次构建需25GB空间,同时编译所有AOSP(Android Open Source Project)配置需要80GB空间,16GB以上内存,在二个四核处理器2.4GHz机器上完成单次构建需5+小时CPU时间,经过时间(wall time)25+分钟.