本篇是我对开发项目的拍照功能过程中,对Camera拍照使用的总结。由于camera2是在api level 21(5.0.1)才引入的,而Camera到6.0仍可使用,所以暂未考虑camera2。


文档中的Camera

要使用Camera,首先我们先看一下文档(http://androiddoc.qiniudn.com/reference/android/hardware/Camera.html)中是怎么介绍的。相对于其他绝大多数类,文档对Camera的介绍还是比较详尽的,包含了使用过程中所需要的步骤说明,当然,这也表明了它在实际使用中的繁琐。

首先,需要在AndroidManifest.xml中声明以下权限和特性:

 <uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

然后,拍照的话,需要以下十步:

1. 通过open(int)方法得到一个实例

2. 通过getParameters()方法得到默认的设置

3. 如果有必要,修改上面所返回的Camera.Parameters对象,并调用setParameters(Camera.Parameters) 进行设置

4. 如果有需要,调用setDisplayOrientation(int)设置显示的方向

5. 这一步很重要,通过setPreviewDisplay(SurfaceHolder)传入一个已经初始化了的SurfaceHolder,否则无法进行预览。

6. 这一步也很重要,通过startPreview()开始更新你的预览界面,在你拍照之前,它必须开始。

7. 调用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)进行拍照,等待它的回调

8. 拍照之后,预览的展示会停止。如果想继续拍照,需要先再调用startPreview()

9. 调用stopPreview()停止预览。

10. 非常重要,调用release()释放Camera,以使其他应用也能够使用相机。你的应用应该在onPause()被调用时就进行释放,在onResume()时再重新open()

上面就是文档中关于使用Camera进行拍照的介绍了。接下来说一下我的使用场景。

我的使用场景



这是项目的界面需求。下面一个圆的拍照按钮,然后是一个取消按钮,上面是预览界面(SurfaceView)加个取景框。再上面就是一块黑的了。点拍照,拍照之后,跳到一个裁剪图片的界面,所以不会有连续拍多次照片的场景。

取景框什么的这里略过不谈,布局文件也相对比较简单,下面直接看Java代码里对Camera的使用。

实际使用及填坑

SurfaceHolder的回调

我在Activity中实现SurfaceHolder.Callback接口。然后在onCreate(Bundle)方法中,添加SurfaceHolder的回调。

        SurfaceHolder holder = mSurfaceView.getHolder();
holder.addCallback(this);

它的回调方法有3个,分别是surface被创建时的回调surfaceCreated(SurfaceHolder),surface被销毁时的回调surfaceDestroyed(SurfaceHolder)以及surface改变时的回调surfaceChanged(SurfaceHolder holder, int, int, int)。这里我们只关注创建和销毁时的回调,定义一个变量用于标志它的状态。

    private boolean mIsSurfaceReady;

    @Override
public void surfaceCreated(SurfaceHolder holder) {
mIsSurfaceReady = true;
startPreview();
} @Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsSurfaceReady = false;
}

其中的startPreview()方法将在下面讲到。

打开相机

然后是打开相机。这些代码在我定义的openCamera方法中。

        if (mCamera == null) {
try {
mCamera = Camera.open();
} catch (RuntimeException e) {
if ("Fail to connect to camera service".equals(e.getMessage())) {
//提示无法打开相机,请检查是否已经开启权限
} else if ("Camera initialization failed".equals(e.getMessage())) {
//提示相机初始化失败,无法打开
} else {
//提示相机发生未知错误,无法打开
}
finish();
return;
}
}

打开相机失败的话,我们无法进行下一步操作,所以在提示之后会直接把界面关掉。

拍照参数

        final Camera.Parameters cameraParams = mCamera.getParameters();
cameraParams.setPictureFormat(ImageFormat.JPEG);
cameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);

分别设置图片格式,以及对焦模式。然后因为我这里是竖屏拍照,所以还需要对Camera旋转90度。

cameraParams.setRotation(90);

注意:涉及到旋转的有两个方法,一个是旋转相机,一个是旋转预览。这里设置的是对相机的旋转。

继续注意:由于机型兼容的问题,这里设置旋转之后,有些手机照片来的照片就是竖的了,但是有些手机(比如万恶的三星)拍出来的照片还是横的,但是它们在照片的Exif信息中有相关的角度属性。所以对于拍出来的照片还是横着的,我们在裁剪时再继续处理。关于照片的旋转处理,后续博客中会讲到。

尺寸参数

这里还是Camera的参数设置,但是我把它单独抽出来是因为,它不像上面设置的参数那样简单直接,而需要进行计算。下面是我们需要注意的问题:

  1. 首先,相机的宽高比例主要有两种,一种是16:9,一种是4:3。
  2. 其次,我们需要SurfaceView的比例与Camera预览尺寸的比例一样,才不会导致预览出来的结果是变形的。
  3. 由于机型分辨率的问题,再加上我们的SurfaceView不是满屏的(即使满屏,还要考虑一些虚拟导航栏和各种奇葩分辨率的机型),16:9的比例我们需要上是不会用到的了,我们会让Camera预览的尺寸比例与SurfaceView的大小比例一样。
  4. 要特别注意,一些手机,如果设置预览的大小与设置的图片大小相差太大(但宽高比例相同)的话,拍出来的照片可能范围也不一样。比如你拍的时候明明是一幅画包括画框,保存的图片却只有画框里的内容。

下面的代码还是写在我们的openCamera()方法中。由于我们需要能够获取到SurfaceView的大小,所以openCamera()是这样调用的:

    @Override
protected void onResume() {
super.onResume();
mSurfaceView.post(new Runnable() {
@Override
public void run() {
openCamera();
}
});
}

它可以保证在openCamera()被调用时surfaceView一定是绘制完成了的。

然后在openCamera()的后续代码中,先获取surfaceView的宽高比例。注意,对于surfaceView我开始在布局上写的是高度占满剩下的空间。

    <SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/bottom"/>

这时候得到的宽高比就是我们所能接受的最小比例了。

        // 短边比长边
final float ratio = (float) mSurfaceView.getWidth() / mSurfaceView.getHeight();

然后获取相机支持的图片尺寸,找出最适合的尺寸。

        // 设置pictureSize
List<Camera.Size> pictureSizes = cameraParams.getSupportedPictureSizes();
if (mBestPictureSize == null) {
mBestPictureSize =findBestPictureSize(pictureSizes, cameraParams.getPictureSize(), ratio);
}
cameraParams.setPictureSize(mBestPictureSize.width, mBestPictureSize.height);

findBestPictureSize的代码如下。注意,因为我们是旋转了相机的,所以计算的时候,对surfaceView的比例是宽除以高,而对Camera.Size则是高除以宽。

    /**
* 找到短边比长边大于于所接受的最小比例的最大尺寸
*
* @param sizes 支持的尺寸列表
* @param defaultSize 默认大小
* @param minRatio 相机图片短边比长边所接受的最小比例
* @return 返回计算之后的尺寸
*/
private Camera.Size findBestPictureSize(List<Camera.Size> sizes, Camera.Size defaultSize, float minRatio) {
final int MIN_PIXELS = 320 * 480; sortSizes(sizes); Iterator<Camera.Size> it = sizes.iterator();
while (it.hasNext()) {
Camera.Size size = it.next();
//移除不满足比例的尺寸
if ((float) size.height / size.width <= minRatio) {
it.remove();
continue;
}
//移除太小的尺寸
if (size.width * size.height < MIN_PIXELS) {
it.remove();
}
} // 返回符合条件中最大尺寸的一个
if (!sizes.isEmpty()) {
return sizes.get(0);
}
// 没得选,默认吧
return defaultSize;
}

接下来是设置预览图片的尺寸:

        // 设置previewSize
List<Camera.Size> previewSizes = cameraParams.getSupportedPreviewSizes();
if (mBestPreviewSize == null) {
mBestPreviewSize = findBestPreviewSize(previewSizes, cameraParams.getPreviewSize(),
mBestPictureSize, ratio);
}
cameraParams.setPreviewSize(mBestPreviewSize.width, mBestPreviewSize.height);

根据图片尺寸,以及SurfaceView的比例来计算preview的尺寸。


/**
* @param sizes
* @param defaultSize
* @param pictureSize 图片的大小
* @param minRatio preview短边比长边所接受的最小比例
* @return
*/
private Camera.Size findBestPreviewSize(List<Camera.Size> sizes, Camera.Size defaultSize,
Camera.Size pictureSize, float minRatio) {
final int pictureWidth = pictureSize.width;
final int pictureHeight = pictureSize.height;
boolean isBestSize = (pictureHeight / (float)pictureWidth) > minRatio;
sortSizes(sizes); Iterator<Camera.Size> it = sizes.iterator();
while (it.hasNext()) {
Camera.Size size = it.next();
if ((float) size.height / size.width <= minRatio) {
it.remove();
continue;
} // 找到同样的比例,直接返回
if (isBestSize && size.width * pictureHeight == size.height * pictureWidth) {
return size;
}
} // 未找到同样的比例的,返回尺寸最大的
if (!sizes.isEmpty()) {
return sizes.get(0);
} // 没得选,默认吧
return defaultSize;
}

上面的两个findBestxxx方法,可以自己根据业务需要进行调整。整体思路就是先对尺寸排序,然后遍历排除掉不满足条件的尺寸,如果找到比例一样的,则直接返回。如果遍历完了仍没找到,则返回最大的尺寸,如果发现都排除完了,只能返回默认的那一个了。

然后,我们还要再根据previewSize来重新设置我们的surfaceView的大小,以使它们的比例完全一样,才不会导致预览时变形。

        ViewGroup.LayoutParams params = mSurfaceView.getLayoutParams();
params.height = mSurfaceView.getWidth() * mBestPreviewSize.width / mBestPreviewSize.height;
mSurfaceView.setLayoutParams(params);

再下来就是把参数设置过去:

mCamera.setParameters(cameraParams);

然后预览。

预览

由于相机打开会需要一些时间,而surfaceHolder的回调也需要一些时间。我希望的是当相机准备完成可以回调并且surface也创建完毕的时候,就可以马上预览(尽量减小进入界面后可能会有的黑一下的时间),所以这里我的代码如下:

        if (mIsSurfaceReady) {
startPreview();
}

同时在surface被创建的时候,也会调用一下这个startPreview()方法。

startPreview()代码如下,在camera初始化之后,首先设置SurfaceHolder对象,然后对预览旋转90度,然后开始预览。

    private void startPreview() {
if (mCamera == null) {
return;
}
try {
mCamera.setPreviewDisplay(mSurfaceView.getHolder());
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
BugReport.report(e);
}
}

自动对焦

我希望在点击预览图的时候能够进行自动对焦。由于在界面上我在surfaceview之上放了一个取景框View,所以我直接对这个View设置一个点击事件,进行触发自动对焦。

自动对焦的代码如下:

    /**
* 请求自动对焦
*/
private void requestFocus() {
if (mCamera == null || mWaitForTakePhoto) {
return;
}
mCamera.autoFocus(null);
}

这里我只需要相机能够对焦,并不是要在对焦成功之后才进行拍照,所以回调我传了一个null。

之所以这样使用是因为,之前我写的是对焦成功之后才拍照,但是会有两个问题:一是对焦会有一个过程,这样对完焦之后才拍照会慢,二是可能在点拍照的时候预览的界面正是我们想要的,但是一对焦,可能对焦失败,导致没有拍照或者是拍出来的是模糊的。

拍照

拍照也是异步回调,并且会需要点时间,所以这里我定义了一个mWaitForTakePhoto变量,表示正在拍照,还没完成。在拍照的过程中,不允许重新对焦或重新拍照。

    private void takePhoto() {
if (mCamera == null || mWaitForTakePhoto) {
return;
}
mWaitForTakePhoto = true;
mCamera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
onTakePhoto(data);
mWaitForTakePhoto = false;
}
});
}

保存照片。这里返回的data可以直接写入文件,就是一张jpg图了。

    private void onTakePhoto(byte[] data) {
final String tempPath = mOutput + "_";
FileOutputStream fos = null;
try {
fos = new FileOutputStream(tempPath);
fos.write(data);
fos.flush();
//启动我的裁剪界面
} catch (Exception e) {
BugReport.report(e);
} finally {
IOUtils.close(fos);
}
}

相机的打开与关闭以及Activity的生命周期

    @Override
protected void onResume() {
super.onResume();
mSurfaceView.post(new Runnable() {
@Override
public void run() {
openCamera();
}
});
} @Override
protected void onPause() {
super.onPause();
closeCamera();
}

关闭相机时,首先要取消掉自动对焦,否则如果正好在自动对焦,又关掉相机,会引发异常。接着停止preview,然后再释放:

    private void closeCamera() {
if (mCamera == null) {
return;
}
mCamera.cancelAutoFocus();
stopPreview();
mCamera.release();
mCamera = null;
}

总结

1,该类的全部代码见:https://gist.github.com/msdx/f8ca0fabf0092f67d829 。没有Demo项目,没有Demo项目,没有Demo项目。

2,文档很重要。

3,我不保证我的代码完全没问题,至少我现在没发现。如果有出现什么问题,欢迎提出。

4,注意相机打开和释放。

5,注意不同机型的相机旋转设置。特别是三星。

6,尺寸计算,previewSize的比例一定要和surfaceView可以显示的比例一样,才不会变形。

7,本文原创,转载请注明在CSDN博客上的出处。

Android开发技巧——Camera拍照功能的更多相关文章

  1. Android开发技巧——大图裁剪

    本篇内容是接上篇<Android开发技巧--定制仿微信图片裁剪控件> 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪. 裁剪控件的简单使 ...

  2. Java乔晓松-android中调用系统拍照功能并显示拍照的图片

    android中调用系统拍照功能并显示拍照的图片 如果你是拍照完,利用onActivityResult获取data数据,把data数据转换成Bitmap数据,这样获取到的图片,是拍照的照片的缩略图 代 ...

  3. 50个android开发技巧

    50个android开发技巧 http://blog.csdn.net/column/details/androidhacks.html

  4. Android开发技巧——使用PopupWindow实现弹出菜单

    在本文当中,我将会与大家分享一个封装了PopupWindow实现弹出菜单的类,并说明它的实现与使用. 因对界面的需求,android原生的弹出菜单已不能满足我们的需求,自定义菜单成了我们的唯一选择,在 ...

  5. Android开发技巧——实现可复用的ActionSheet菜单

    在上一篇<Android开发技巧--使用Dialog实现仿QQ的ActionSheet菜单>中,讲了这种菜单的实现过程,接下来将把它改成一个可复用的控件库. 本文原创,转载请注明出处: h ...

  6. Android开发技巧——高亮的用户操作指南

    Android开发技巧--高亮的用户操作指南 2015-12-15补记: 发现使用PopupWindow进行遮罩层的显示,在华为P7上会有问题.具体表现为:画出来的高亮部分会偏下.原因为:通过view ...

  7. Android开发技巧——自定义控件之增加状态

    Android开发技巧--自定义控件之增加状态 题外话 这篇本该是上周四或上周五写的,无奈太久没写博客,前几段把我的兴头都用完了,就一拖再拖,直到今天.不想把这篇拖到下个月,所以还是先硬着头皮写了. ...

  8. Android开发技巧——自定义控件之使用style

    Android开发技巧--自定义控件之使用style 回顾 在上一篇<Android开发技巧--自定义控件之自定义属性>中,我讲到了如何定义属性以及在自定义控件中获取这些属性的值,也提到了 ...

  9. Android开发技巧——自定义控件之自定义属性

    Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个 ...

随机推荐

  1. Python无法导入Cython的.pyx文件

    在import 相应包之前, 添加: import pyximport pyximport.install() 即可.

  2. 使用hue查看hdfs系统报无法访问:/user/hadoop。 Note: you are a Hue admin but not a HDFS superuser, "hdfs" or part of HDFS supergroup, "supergroup".

    出现这个问题,是因为默认的超级用户是hdfs ,我的是hadoop用户登录的, 也就是说首次登录hadoop这个用户是我的超级用户 此时只需要将hue.ini配置改为 然后重启即可.

  3. Python面向对象——重写与Super

    1本文的意义 如果给已经存在的类添加新的行为,采用继承方案 如果改变已经存在类的行为,采用重写方案 2图解继承.重写与Super 注:上面代码层层关联.super()可以用到任何方法里进行调用,本文只 ...

  4. 【转载】C++基本功和 Design Pattern系列 ctor & dtor

    最近实在是太忙了,无工夫写呀.只能慢慢来了.呵呵,今天Aear讲的是class.ctor 也就是constructor, 和  class.dtor, destructor. 相信大家都知道const ...

  5. MySQL之SQL语句的优化

    仅供自己学习 结论写在前面: 1.尽量避免进行全表扫描,可以给where和order by涉及的列上建立索引 2.尽量在where子句中使用 !=或<>操作符,因为这样会导致引擎放弃索引而 ...

  6. [LeetCode] Max Consecutive Ones 最大连续1的个数

    Given a binary array, find the maximum number of consecutive 1s in this array. Example 1: Input: [1, ...

  7. HDU 5909 Tree Cutting

    传送门 题意: 有一棵n个点的无根树,节点依次编号为1到n,其中节点i的权值为vi, 定义一棵树的价值为它所有点的权值的异或和. 现在对于每个[0,m)的整数k,请统计有多少T的非空连通子树的价值等于 ...

  8. 洛谷mNOIP模拟赛Day2-星空

    题目背景 pdf题面和大样例链接:http://pan.baidu.com/s/1cawM7c 密码:xgxv 命运偷走如果只留下结果, 时间偷走初衷只留下了苦衷. 你来过,然后你走后,只留下星空. ...

  9. bzoj 3745: [Coci2015]Norma

    Description Solution 考虑分治: 我们要统计跨越 \(mid\) 的区间的贡献 分最大值和最小值所在位置进行讨论: 设左边枚举到了 \(i\),左边 \([i,mid]\) 的最大 ...

  10. ●洛谷P3688 [ZJOI2017]树状数组

    题链: https://www.luogu.org/problemnew/show/P3688题解: 二维线段树. 先不看询问时l=1的特殊情况. 对于一个询问(l,r),如果要让错误的程序得到正确答 ...