前言
最近在做的需求里涉及到了自定义相机和相册,遇到不少问题,这里开一篇总结一下,以后再有类似的需求可以少走弯路。也希望可以帮到有相关需求的朋友。
正文
1.自定义相机
由于交互设计的原因,不能直接调用系统相机,只能通过自定义相机的方式实现。
以前没写过相机,就想找个示例看看,github上一搜,有一个google的示例代码库:cameraview,star还挺多,然后便开始了我的噩梦之旅。
这个示例代码在我的开发机上跑得挺好,我想着需求里也只有一个拍照功能,基本满足需求了,就直接拿过来用,等我功能都开发完了,提测的时候,各种兼容问题,各种崩溃,那叫一个惨啊。一开始抱着对google的信仰,觉得肯定是国产厂商又乱改Rom了,先找找问题在哪吧,遨游在代码的海洋里,结果看到了这样一幕:
想着自己改改吧,拿了几个有问题的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;
}
结语
一路磕磕碰碰,这个需求终于是做完了,也从中学到了不少东西。更深入一点的拍照裁剪等内容,有时间还需要去仔细研究一下,三方库用起来是很方便,但也得大致了解里面的实现原理。今天就总结到这里,如有错误,欢迎指正。