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, 100new 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, 00, 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;
        }

结语

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

推荐↓↓↓
安卓开发
上一篇:Android 去掉系统默认提示音、移除音量调节的进度条、增加音量调节提示音 下一篇:项目模块重构分享与思考