Android开发之自定义相机、相册趟坑之旅

前言

最近在做的需求里涉及到了自定义相机和相册,遇到不少问题,这里开一篇总结一下,以后再有类似的需求可以少走弯路。也希望可以帮到有相关需求的朋友。

正文

1.自定义相机

由于交互设计的原因,不能直接调用系统相机,只能通过自定义相机的方式实现。
以前没写过相机,就想找个示例看看,github上一搜,有一个google的示例代码库:cameraview,star还挺多,然后便开始了我的噩梦之旅。
这个示例代码在我的开发机上跑得挺好,我想着需求里也只有一个拍照功能,基本满足需求了,就直接拿过来用,等我功能都开发完了,提测的时候,各种兼容问题,各种崩溃,那叫一个惨啊。一开始抱着对google的信仰,觉得肯定是国产厂商又乱改Rom了,先找找问题在哪吧,遨游在代码的海洋里,结果看到了这样一幕:

issue.png
正好issue里也有人提了这个问题,我就直接用这个来截图了,简而言之就是,这个库的代码和自己的文档都对不上,再一看最后更新时间一年多以前,突然有一种老房子年久失修的感觉。后悔当初用这个库之前没仔细看清楚。
想着自己改改吧,拿了几个有问题的OPPO,VIVO手机过来,改来改去,真的是按下葫芦起了瓢啊... 这个手机上好了,那个手机上又出问题了。
这里还有一个问题得说一下,关于相机的预览,这个是有固定的比例的,可以通过相应的API拿到,而且各个手机都不尽相同,目前测试来看4:3这个比例是几乎所有手机都支持的最通用的比例,如果设计要求展示的比例不是4:3,那就得手动裁剪成需要的比例了。
是不是听起来就挺麻烦的?而且还有各种设备不兼容的问题,有的设备启动相机就崩。权衡一番之后,果断抛弃信仰,换用了一个成熟的三方库:CameraKit
这个库就肥肠翅鸡了,文档清晰,功能齐全,兼容性良好,最最重要的是,可以设置任意大小的预览页面,拍照后会自动输出裁剪后的图片,正常的拍照需求都能搞定。我目前使用的是V0.13.0版本,最新的V0.13.1在部分OPPO手机上还是会出现崩溃,回退了一个版本就没问题了。拍照变得如此轻松。
经过这次,我终于深刻体会到了选择开源库还是要慎重,看看star,看看最后更新时间,看看issue,尽量选择一些成熟的库,能免去很多烦恼,人生苦短,对自己好一点。

2.权限问题

用到了相机,肯定会涉及到相机权限的申请,这里推荐AndPermission这个库,这是一个专门针对动态权限做处理的三方库,测试并兼容了大量的国产手机,并且流式API调用也很舒服。很多国产手机里有两套权限管理系统,一套原生的,一套自己的,如果用标准的API去处理权限问题,会遇到很多坑,比如:

  • 部分设备上使用SDK的Api判断是否有权限时,无论是否有权限都返回true。
  • 部分设备上无论用户点击同意还是拒绝都返回true。
  • 部分设备在申请权限时并不会弹出授权Dialog,而是在执行权限相关代码时才会弹出授权Dialog。

说的就是你OPPO、VIVO,一生无爱。

3.自定义相册

相册这块,系统相册只能选择一张图片,想要同时选择多张图片只能自定义相册。最核心代码的就是从ContentResolver里读取媒体库中的图片信息,然后进行过滤、筛选、分类,最终展示给用户。

    /**
     * 需要从数据库中获取的信息:
     * BUCKET_DISPLAY_NAME  文件夹名称
     * DATA  文件路径
     */
    private final String[] projection = new String[]{
            MediaStore.Images.Media.BUCKET_DISPLAY_NAME, 
            MediaStore.Images.Media.DATA};

    /**
     * 通过ContentResolver 从媒体数据库中读取图片信息
     */
    Cursor cursor = getContentResolver().query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,  //限制类型为图片
            projection,
            MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?",
            new String[]{"image/jpeg", "image/png"},  // 这里筛选了jpg和png格式的图片
            MediaStore.Images.Media.DATE_ADDED); // 排序方式:按添加时间排序

筛选、分类的代码我就不贴了,有需要可以参考TakePhoto这个库里的代码。自定义相册没遇到什么坑,只要把数据处理好,按照设计展示出来就行了。

4.图片压缩处理

项目中还需要对图片进行压缩,也顺带了解了一下图片压缩的方法,常见的方式就是像素压缩和质量压缩,这里就直接贴出代码了,注释里也写得很清楚了:

    /**
     * 像素压缩
     */
    private void compressImageByPixel(final String imgPath) {
        BitmapFactory.Options newOpts = new BitmapFactory.Options();
        newOpts.inJustDecodeBounds = true;//只读边,不读内容
        BitmapFactory.decodeFile(imgPath, newOpts);
        newOpts.inJustDecodeBounds = false;
        int width = newOpts.outWidth;
        int height = newOpts.outHeight;
        int be = 1; //缩放比例
        if (width >= height && width > maxHeightOrWidth) {//缩放比,用高或者宽其中较大的一个数据进行计算
            be = (width / maxHeightOrWidth);
            be++;
        } else if (width < height && height > maxHeightOrWidth) {
            be = (height / maxHeightOrWidth);
            be++;
        }
        newOpts.inSampleSize = be;//设置采样率
        newOpts.inPreferredConfig = Bitmap.Config.ARGB_8888;//该模式是默认的,可不设
        newOpts.inPurgeable = true;// 同时设置才会有效
        newOpts.inInputShareable = true;//当系统内存不够时候图片自动被回收
        Bitmap bitmap = BitmapFactory.decodeFile(imgPath, newOpts);

        try {
            File compressedFile = getCompressedImageFile();  //设置存储路径
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(compressedFile));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 质量压缩,可以指定压缩后的maxSize
     */
    private void compressImageByQuality(final Bitmap bitmap, int maxSize) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int options = 100;
        bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);//质量压缩方法,把压缩后的数据存放到baos中 (100表示不压缩,0表示压缩到最小)
        while (baos.toByteArray().length > maxSize) {//循环判断如果压缩后图片是否大于指定大小,大于继续压缩
            baos.reset();//重置baos即让下一次的写入覆盖之前的内容
            options -= 5;//图片质量每次减少5
            if (options <= 5) {
                options = 5;//如果图片质量小于5,为保证压缩后的图片质量,图片最底压缩质量为5
            }
            bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);//将压缩后的图片保存到baos中
            if (options == 5) {
                break;//如果图片的质量已降到最低则,不再进行压缩
            }
        }
        try {
            File compressedFile = getCompressedImageFile();//设置存储路径
            FileOutputStream fos = new FileOutputStream(compressedFile);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

注意:图片压缩还是挺耗时的,需要放在子线程中执行。另外还要记得判空,防止异常情况。

5.bitmap转RGB

项目里需要将bitmap转成RGB流传给一个图像检测的SDK,用于检测图片的明暗度,模糊度等,贴一下代码:

    private static byte[] bitmap2RGBA(String picturePath) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        /**
         * 颜色模式 ARGB_8888模式
         * inPreferredConfig 只是一个首选值,如果填ARGB_8888以外的其他值,系统检测到不符合,也会采用ARGB_8888
         * 所以这里固定为ARGB_8888
         */
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        Bitmap bitmap = BitmapFactory.decodeFile(picturePath, options);

        //返回可用于储存此位图像素的最小字节数
        int byteCount = bitmap.getByteCount();
        //使用allocate()静态方法创建字节缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(byteCount);
        //将位图的像素复制到指定的缓冲区
        bitmap.copyPixelsToBuffer(byteBuffer);

        //Bitmap像素点的色彩通道排列顺序是RGBA
        byte[] rgba = byteBuffer.array();

        byte[] rgbResult = new byte[rgba.length / 4 * 3];

        int count = rgba.length / 4;

        for (int i = 0; i < count; ++i) {
            //R
            rgbResult[i * 3] = rgba[i * 4];
            //G
            rgbResult[i * 3 + 1] = rgba[i * 4 + 1];
            //B
            rgbResult[i * 3 + 2] = rgba[i * 4 + 2];
        }
        return rgbResult;
    }
6.图片旋转问题

这也是一个挺有意思的问题,测试同事反馈,在几部小米手机上拍照时是竖屏拍的,但是展示的时候图片却是横屏展示。查了一下资料了解到,这是厂商在设计内部元件结构的时候,把相机镜头旋转后安装进去,所以拍出来的照片也是旋转过的,好在有方法可以读到图片旋转了多少度,自己手动处理一下即可:

    public static int readPictureDegree(String path) {
        int degree = 0;
        try {
            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;
                default:
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return degree;
    }

    public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) {
        if (bm == null) {
            return null;
        }
        Bitmap returnBm = null;

        // 根据旋转角度,生成旋转矩阵
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        try {
            // 将原始图片按照旋转矩阵进行旋转,并得到新的图片
            returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(),
                    bm.getHeight(), matrix, true);
        } catch (OutOfMemoryError e) {
        }
        if (returnBm == null) {
            returnBm = bm;
        }
        if (bm != returnBm) {
            bm.recycle();
        }
        return returnBm;
    }

原理也很简单,就是通过读取图片Exif(可交换图像文件格式)中的Orientation,得到图片的旋转角度,再给它旋转回来就ok了。

7.其他的一些坑

最无语的来了,测试给了一个步步高的什么学习平板,说这个设备上一点相机就崩溃,我拿过来调试,发现一直报连接不到相机服务的错误,但却不知道究竟是什么导致的,直到我把这个平板翻过来看了一眼,发现根本就没有摄像头......
还能怎么办,加个判断吧:

        int cameraCount = Camera.getNumberOfCameras();
        if (cameraCount == 0) {
            Toast.makeText(this, "该设备没有摄像头", Toast.LENGTH_SHORT).show();
            return;
        }

结语

一路磕磕碰碰,这个需求终于是做完了,也从中学到了不少东西。更深入一点的拍照裁剪等内容,有时间还需要去仔细研究一下,三方库用起来是很方便,但也得大致了解里面的实现原理。今天就总结到这里,如有错误,欢迎指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342