添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
爱跑步的感冒药  ·  MessagePack-CSharp ...·  1 年前    · 

公司项目中有一个祖传的自定义相机,之前发现它在横拍的时候没有对图片进行旋转,对其进行一番修改之后在测试机上面测试成功。上线后又发现只有部分手机正确旋转了,经过一番努力之后,终于解决。并且在文末给出一个与网上千篇一律的照片旋转不同的比较 少见的快速旋转方案

写下此文章记录下问题的解决经过,一步步分析里面的问题。

只有部分手机正确旋转

这种做法只有部分手机获得了正确旋转的照片, 直接来看一下代码里面相机拍照回调方法onPictureTaken()的处理:

 public void onPictureTaken(byte[] data, Camera camera) {
                camera.stopPreview();
                //是否需要旋转
                boolean isOrientation = false;
                //用户是否横屏拍摄,true为横屏 0度左旋 90不旋 180右旋 270度旋
                if (CameraActivity.this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE
                        && (takePhotoOrientation <= 90+45)&&(takePhotoOrientation>=90-45)) {//不旋转
                    isOrientation = false;
                } else {
                    isOrientation = true;
               //旋转太过消耗时间,是储存的40倍时间左右,考虑后去掉旋转。
               //---反注释start---
                if (isOrientation) {
                    //横屏拍摄,需要旋转90度
                   byte[] bytes = ImageUtils.rotatePic(data,takePhotoOrientation-90);
                    if (bytes != null) {
                        data = bytes;
                //---反注释end---
                mPicPath = String.format("%s%s%s", AppConfig.getCarTradeFileDir("Camera"),
                            System.currentTimeMillis() + ".jpg", tempFileSuffex);
                FileSaveUtils.saveFile(data, mPicPath, new FileSaveUtils.SaveListener() {
                    @Override
                    public void saveComplete() {
                        finish();
复制代码

其实这里我做的就只是把作者关于旋转的代码反注释了。

分析一下里面做了些什么:

  • takePhotoOrientation这个变量是由一个重力监听器提供的,用它判断当前手机是否处于横拍状态,横拍需要旋转;
  • 然后就可以看到原代码作者非常贴心地在这里注释了旋转非常耗时,告诫我们不要去旋转它,然后注释掉了旋转代码,我又把他的注释解开了;
  • ImageUtils.rotatePic()旋转图片,里面走的核心代码就是网上一搜就是的旋转方案:
  • public static byte[] rotatePic(byte[] data, int degree) {
            byte[] bytes = null;
            try {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inPreferredConfig = Bitmap.Config.RGB_565;
                options.inJustDecodeBounds = false;
                options.inPurgeable = true;
                options.inInputShareable = true;
                Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);// data是字节数据,将其解析成位图
                bitmap = rotateBitmap(bitmap, degree);
                bytes = bitmap2Bytes(bitmap);
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
            } catch (Error e) {
                e.printStackTrace();
            return bytes;
        * 网上一搜就有的旋转代码
        public static Bitmap rotateBitmap(Bitmap bitmap, int degree) {
            Matrix matrix = new Matrix();
            matrix.postRotate(degree);
            Bitmap bm = null;
            try {
                bm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            } catch (Exception e) {
                bm = bitmap;
            return bm;
        public static byte[] bitmap2Bytes(Bitmap bm) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            return baos.toByteArray();
    
  • 保存byte[]数据。这里的照片保存方法内部实现也有问题,但是这里不是我们这篇文章的重点,所以忽略它先。
  • 看到这里,一些明眼的同学可能已经看出为什么作者说旋转用了40倍时间了,是的呢~

    ImageUtils.rotatePic()里面出了问题: 它先把byte[]解析成bitmap,然后旋转又create了一个bitmap,再把bitmap转换成byte[]来保存。这个过程有没有40倍我没有实践过,不过可以想象这耗时很长。留待优化。

    获取EXIF

    优化的事情先放下,先解决业务上的问题-旋转。

    那怎么才能知道照片的方向呢?小case,难不倒玩摄影的我,相机在拍照的时候都会有保存提个叫EXIF的照片信息,它里面存放着这张照片的诸多信息,例如光圈、焦距、快门时间等等,最重要的还有我们需要的旋转方向-orientation。有了它我们不就可以轻轻松松判断方向了!? 嘿嘿~我读:

        ExifInterface exifInterface = new ExifInterface(filepath);
        int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                                ExifInterface.ORIENTATION_NORMAL);
    复制代码

    emmmm... 这,这里我们不能马上用这个代码来读,如果你用了这个项目的代码,去读exif,你可能就要进入死胡同里面出不来了!

    先下载一个能看exif的app,我用的是photo exif editor,它打开是这样的:

    我波波可爱吗?

    如图exif一目了然,然而用photo exif editor去查看项目相机拍出来的图,所有exif数据都是空的,所以如果你用代码来读,永远都只会读出你填写的那个默认值。

    再去探究一下exif丢失的原因吧,开始我以为是Carema Api的问题,但在onPictureTaken()里面一回调就直接保存图片,exif是存在的,明显问题出在旋转代码里面,可以猜想是转换成bitmap的时候丢失了exif信息,经过一番资料查验,的确如此。

    什么是Exif,为什么会丢失Exif信息?

    从Exif2.2规范里面可以找到关于压缩图像文件数据的描述,其中有这么一幅图: 它描述了这么一个事实:压缩图像文件由标记码和压缩图像数据组成,其中标记码数据中包括Exif。很显然,bitmap作为一个图像类只包含了解压出的图像数据。

    照片转换成bitmap会丢失exif,所以编辑图片的时候需要把exif保存起来再修改后重新保存到照片中去。

    想要深入了解Exif的话这里有一个2.2版本的规范文档 Exif2.2传送门

    既然了解好了,读Exif吧!

        ExifInterface exifInterface = new ExifInterface(filepath);
        int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                                ExifInterface.ORIENTATION_NORMAL);
    复制代码

    我读~~~~↓

    小米:照片方向与手机方向一致,没有正确旋转,Exif 90°;

    VIVO:照片方向与手机方向不一致,正确旋转,Exif 90°。

    惊了!居然和预想的不一致。。出现这样的结果证明这又是手机系统内部出现的差异,安卓碎片化!

    工作陷入了停顿。。只能搬出大牛鱼哥~ 一番询问之后,鱼哥show his code解决了我的问题~再次感谢鱼哥

    配置相机参数

    Camera#Parameters这个类相信对于熟悉相机的同学不会陌生,它能够用来获取和配置相机参数:获取预览尺寸,设置闪光灯,对焦模式等等,都在这个通过这个类进行调节配置,我们要的修正方法竟然藏在这里面!

    public class IOrientationEventListener extends OrientationEventListener {
            public IOrientationEventListener(Context context) {
                super(context);
            @Override
            public void onOrientationChanged(int orientation) {
                if (ORIENTATION_UNKNOWN == orientation) {
                    return;
                Camera.CameraInfo info = new Camera.CameraInfo();
                Camera.getCameraInfo(defaultCameraId, info);
                orientation = (orientation + 45) / 90 * 90;
                int rotation = 0;
                if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                    rotation = (info.orientation - orientation + 360) % 360;
                } else {
                    rotation = (info.orientation + orientation) % 360;
                if (null != mCamera) {
                    Camera.Parameters parameters = mCamera.getParameters();
                    parameters.setRotation(rotation);//关键代码
                    mCamera.setParameters(parameters);
    复制代码

    获取实例之后分别在SurfaceHolder#Callback#surfaceCreated()和SurfaceHolder#Callback#surfaceDestroyed()调用一下实例的enable()和disable()即可统一拍照旋转问题

    由于onOrientationChanged()回调频繁,更加优化的做法可以在按下拍摄按钮之后才对相机进行设置。

    通过修改rotation的值可以发现,只要rotation的值一定,所有手机的拍照方向都是统一的,意味着厂商对相机设置的方向默认值碎片化。

    至此解决自定义相机拍照旋转问题。

    快速旋转照片

    通过上面的一大轮介绍,聪明的同学可能已经猜想出这个快速旋转的方案了--就是利用Exif。

    因为照片本身是有设备生成的Exif的,里面含有标记照片方向的orientation,图片加载框架会通过读取orientation来对照片进行显示,也就是说,通过修改Exif里orientation的值,即可以达到旋转照片的效果!而且只需要操作一个小参数就能完成,瞬间完成,速度杠杠的!再也不怕旋转图片慢啦!

    看一下Glide的处理: 对Exif解析确认图像方向

        //默认的图片头解析器
        public final class DefaultImageHeaderParser implements ImageHeaderParser {
            private int getOrientation(Reader reader, ArrayPool byteArrayPool) throws IOException {
                int exifSegmentLength = moveToExifSegmentAndGetLength(reader);
                if (exifSegmentLength == -1) {
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "Failed to parse exif segment length, or exif segment not found");
                    return UNKNOWN_ORIENTATION;
                byte[] exifData = byteArrayPool.get(exifSegmentLength, byte[].class);
                try {
                    return parseExifSegment(reader, exifData, exifSegmentLength);
                } finally {
                    byteArrayPool.put(exifData);
    复制代码

    逆时针方向旋转的代码:

     ExifInterface exifInterface = new ExifInterface(currentPath);
                        // 获取图片的旋转信息
                        int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                                ExifInterface.ORIENTATION_NORMAL);
                        LogUtils.v("exif orientation:" + orientation);
                        //根据当前图片方向设置想要的图片方向
                        switch (orientation) {
                            case ExifInterface.ORIENTATION_ROTATE_90://正常角度
                                exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL+"");
                                break;
                            case ExifInterface.ORIENTATION_ROTATE_180:
                                exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_ROTATE_90+"");
                                break;
                            case ExifInterface.ORIENTATION_ROTATE_270:
                                exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_ROTATE_180+"");
                                break;
                            case ExifInterface.ORIENTATION_NORMAL:
                                exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_ROTATE_270+"");
                                break;
                        exifInterface.saveAttributes();
    

    使用注意: 这种方案只能用于具有Exif的照片,旋转之后的照片显示需要注意缓存问题,每次旋转之后要更新缓存以正确显示照片。

    Glide在加载图片的时候直接通过signature()方法new ObjectKey传入文件的修改时间作缓存标记即可:

    Glide.with(context)
         .load(filePath)
         .signature(new ObjectKey(file.lastModified())
         .into(imageView);
    复制代码

    其他框架的话请自行查找啦~

    终于写完了,要是觉得有用或者让你拓展了知识就点个赞以示支持呗~

    谢谢你的观看和学习!下回再见~

    私信