本篇是我对开发项目的拍照功能过程中,对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博客上的出处。

更多相关文章

  1. Android调用系统相册和相机选择图片并显示在imageview中
  2. Android相机、相册获取图片,解决相机拍照图片被压缩模糊的情况
  3. android相机如何只显示处理后的图像以及这里onPreviewFrame不被
  4. Android选择图片
  5. 国产神器天语Android双核手机W700线下赏机经历
  6. Android(安卓)短视频编辑开发之摄像头预览实时美颜(三)
  7. Android多点触控实现图片缩放预览
  8. [置顶] android头像相册/拍照选取,裁剪及上传综合案例
  9. Android实现从相册截图的功能

随机推荐

  1. Titanium 使用刘明星的Jpush module做and
  2. Android RabbitMQ使用之RabbitMQ安装及配
  3. Android中的常用的对话框
  4. 《阿里巴巴Android开发手册》v1.0.1更新,
  5. Android(安卓)monkey 命令详解
  6. Android修改字体样式
  7. android 数字证书具体应用机制
  8. android插件汇总
  9. 【Android】使用LiveData KTX Builder让
  10. Android(安卓)Studio 一起走过的那些坑