Android Camera初步使用

/ 0评 / 0

前言

Android SurfaceView解析中我介绍了下如何使用SurfaceView,不过在我们日常的开发中,除非是游戏相关,一般都不会直接在SurfaceView上面绘制图像,而是让系统"绘制"在我们的SurfaceView中,这篇博客我就来介绍下如何让相机绘制在我们的SurfaceView上面,并实现如下功能。

目标:前后置相机预览、前后置相机拍照并保存为图片以及一张加了水印的图片。

知识点概括

1、相机的基本使用

2、相机前后置判断以及相机支持尺寸等获取

3、相机预览画面的比例适配

4、相机的拍照处理以及照片加水印

相机的基本使用

使用系统相机主要是android.hardware.Camera类,首先需要声明权限

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

1、获取相机,通过Camera.open()即可获取一个相机对象。

// open() 无参数 默认后置摄像头,没有后置则返回null
// open(int cameraId) 可选开启摄像头
// 后置摄像头:CameraInfo.CAMERA_FACING_BACK = 0
// 前置摄像头:CameraInfo.CAMERA_FACING_FRONT = 1
mCamera = Camera.open(CameraInfo.CAMERA_FACING_FRONT);

2、设置相机的参数,使用mCamera.getParameters();方法可以获取默认的相机参数,然后根据我们的需要进行设置,最后再使用mCamera.setParameters(param);方法设置参数

Camera.Parameters param = mCamera.getParameters();
//在这里进行设置
mCamera.setParameters(param);

3、设置相机的预览窗口,使用 mCamera.setPreviewDisplay(SurfaceHolder  surfaceHolder);方法告诉系统相机将画面绘制到我们的SurfaceView上面。关于SurfaceView的用法请看这里

4、开启/关闭相机预览,使用mCamera.startPreview();  /  mCamera.stopPreview();函数即可开始预览和关闭预览。

5、相机的销毁,由于在同一时刻,系统只能允许一个进程打开相机,所以在我们不需要使用相机的时候,我们需要将其销毁,销毁只需要调用mCamera.release();方法即可。关于销毁相机的时机,一般是在onPause()时销毁,在onResume()时重新打开。

下面贴出的代码分别对应相机的开启/关闭。其中用到的方法在后面介绍。

/**
 * 打开摄像头并开始预览
 */
public void startCamera() {
    if (!mIsCreated) {
        Log.e(TAG, "surfaceview not create!!!");
        return;
    }
    // 如果没有摄像头
    if (Camera.getNumberOfCameras() == 0) {
        Toast.makeText(getContext(), "没有发现摄像头设备", Toast.LENGTH_SHORT).show();
        return;
    }
    // 只有一个摄像头,那么就不切换摄像头了,
    if (Camera.getNumberOfCameras() == 1) {
        CameraInfo cameraInfo = new CameraInfo();
        Camera.getCameraInfo(0, cameraInfo);
        FACING_MODE = cameraInfo.facing;
    }
    // open() 无参数 默认后置摄像头,没有后置则返回null
    // open(int cameraId) 可选开启摄像头
    // 后置摄像头:CameraInfo.CAMERA_FACING_BACK = 0
    // 前置摄像头:CameraInfo.CAMERA_FACING_FRONT = 1
    mCamera = Camera.open(FACING_MODE);
    if (mCamera != null) {
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);
            Camera.Parameters param = mCamera.getParameters();
            outputDeviceInfo(param);
            param.setPictureFormat(ImageFormat.JPEG);
            // 设置大小和方向等参数
            Size bestPerviewSize = getBestSupportedSize(param.getSupportedPreviewSizes(), getWidth(), getHeight());
            param.setPreviewSize(bestPerviewSize.width, bestPerviewSize.height);
            Size bestPictureSize = getBestSupportedSize(param.getSupportedPictureSizes(), getWidth(), getHeight());
            param.setPictureSize(bestPictureSize.width, bestPictureSize.height);
            param.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
            // 后拍照的结果需要旋转90度
            if (FACING_MODE == CameraInfo.CAMERA_FACING_BACK) {
                param.setRotation(90);
            }
            // 前置拍照的结果需要旋转270度
            if (FACING_MODE == CameraInfo.CAMERA_FACING_FRONT) {
                param.setRotation(270);
            }
            // 预览需要旋转90度
            mCamera.setDisplayOrientation(90);
            mCamera.setParameters(param);
            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(getContext(), "打开摄像异常", Toast.LENGTH_SHORT).show();
            releaseCamera();
        }
    }
}

/**
 * 销毁摄像头
 */
public void releaseCamera() {
    if (mCamera != null) {
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }
}

6、拍照、拍照只需要调用 mCamera.takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)即可。需要注意的是,调用这个函数会让预览停止,所以在我们拍照完毕以后,需要再次调用mCamera.startPreview();方法开启预览。关于这个方法的介绍,将会在本文最后面详细介绍。

7、切换摄像头,切换摄像头只需要销毁原来的然后重新open一个即可。注意需要判断下摄像头的个数,万一只有一个就悲剧了

相机前后置判断以及相机支持尺寸等获取

1、判断相机的个数以及前后置判断

//相机个数
int numberOfCameras = Camera.getNumberOfCameras()
//通过Camera.getCameraInfo方法获取摄像头信息
for (int i = 0; i < numberOfCameras; i++) {
    CameraInfo cameraInfo = new CameraInfo();
    Camera.getCameraInfo(i, cameraInfo);
    // 前置摄像头 = 1
    if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
        Log.i(TAG, "cameraId = " + i + ",Camera is CameraInfo.CAMERA_FACING_FRONT," + "Camera orientation = "
                + cameraInfo.orientation);
    }
    // 后置摄像头 = 0
    else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
        Log.i(TAG, "cameraId = " + i + ",Camera is CameraInfo.CAMERA_FACING_FRONT," + "Camera orientation = "
                + cameraInfo.orientation);
    } else {
        Log.e(TAG, "unkonw Camera");
    }
}

2、摄像头的尺寸

我们可以通过如下代码获取摄像头支持的预览尺寸和相片尺寸,这些尺寸非常重要,因为在我们开启摄像头的时候,需要使用param.setPreviewSize(int width, int height)和param.setPictureSize(int width, int height)方法来设置宽高,如果随便设置,那么会造成黑屏或者直接崩溃。

Camera.Parameters param = mCamera.getParameters();
//设备支持的图片尺寸
List<Size> pictureSizes = param.getSupportedPictureSizes();
//设备支持的预览尺寸
List<Size> previewSizes = param.getSupportedPreviewSizes();

相机预览画面的比例适配

代码如下,首先对比所有支持的尺寸是否有和SurfaceView相同的,如果没有,则选择比例最接近SurfaceView的尺寸。

/**
 * 通过对比得到与宽高比最接近的尺寸(如果有相同尺寸,优先选择)
 * 
 * @param supportedSizeList 需要对比的预览尺寸列表
 * @param surfaceWidth 需要被进行对比的原宽
 * @param surfaceHeight 需要被进行对比的原高
 * @return 得到与原宽高比例最接近的尺寸
 */
protected Camera.Size getBestSupportedSize(List<Size> supportedSizeList, int surfaceWidth, int surfaceHeight) {
    Log.i(TAG, "surfaceWidth = " + surfaceWidth + ",surfaceHeight = " + surfaceHeight);
    int reqWidth = surfaceWidth;
    int reqHeight = surfaceHeight;
    boolean isPortrait = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
    // 当屏幕为垂直的时候需要把宽高值进行调换,保证宽大于高
    if (isPortrait) {
        reqWidth = surfaceHeight;
        reqHeight = surfaceWidth;
    }
    // 先查找preview中是否存在与surfaceview相同宽高的尺寸
    for (Camera.Size size : supportedSizeList) {
        if ((size.width == reqWidth) && (size.height == reqHeight)) {
            return size;
        }
    }

    // 如果没有尺寸相同的,则找与传入的宽高比最接近的size
    float reqRatio = ((float) reqWidth) / reqHeight;
    float curRatio, deltaRatio;
    float deltaRatioMin = Float.MAX_VALUE;
    Camera.Size retSize = null;
    for (Camera.Size size : supportedSizeList) {
        curRatio = ((float) size.width) / size.height;
        deltaRatio = Math.abs(reqRatio - curRatio);
        if (deltaRatio < deltaRatioMin) {
            deltaRatioMin = deltaRatio;
            retSize = size;
        }
    }
    Log.i(TAG, "retSize.width = " + retSize.width + ",retSize.height = " + retSize.height);
    return retSize;
}

相机的拍照处理以及照片加水印

在上面说到了,拍照只需要调用mCamera.takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)即可,三个参数分别对应对应相机拍照前,相机的原始数据回调,相机的拍摄结果数据回调,我们在拍摄结果回调中,将数据直接存文件即可实现照片的保存,当然,我们也可以对这个数据进行处理再保存,下面的代码分别保存了原始图片以及加水印后的图片。

 mCamera.takePicture(new ShutterCallback() {

        // 拍照之前的工作
        @Override
        public void onShutter() {
            Log.i(TAG, "before takePicture");
        }
    }, new PictureCallback() {

        // 照片的二进制数据
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            Log.i(TAG, "doing onPictureTaken");
        }
    }, new PictureCallback() {

        // 最终的照片数据
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            Log.i(TAG, "done onPictureTaken");
            outOriginPicture(data);
            outWatermarkPicture(data);
            Toast.makeText(getContext(), "保存原始图片和水印图片成功", Toast.LENGTH_SHORT).show();
            // 拍照以后会停止预览,所以继续预览
            mCamera.startPreview();
        }
    });
}

/**
 * 保存添加了水印的图片
 * 
 * @param data 图片的元素数据
 */
private void outWatermarkPicture(byte[] data) {
    File externalStorageDirectory = Environment.getExternalStorageDirectory();
    File picPath = new File(externalStorageDirectory, "img_watermake.jpg");
    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
    Bitmap createBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(createBitmap);
    canvas.drawBitmap(bitmap, 0, 0, null);
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.FILL);
    paint.setColor(Color.BLUE);
    paint.setDither(true);
    paint.setAntiAlias(true);
    paint.setTextSize(150);
    canvas.drawText("水印文字", 0, 150, paint);
    try {
        FileOutputStream out = new FileOutputStream(picPath);
        createBitmap.compress(CompressFormat.JPEG, 100, out);
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/**
 * 保存原始图片
 * 
 * @param data 原始图片的二进制数据
 */
private void outOriginPicture(byte[] data) {
    File externalStorageDirectory = Environment.getExternalStorageDirectory();
    File picPath = new File(externalStorageDirectory, "img.jpg");
    try {
        FileOutputStream out = new FileOutputStream(picPath);
        out.write(data);
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

那些坑

1、如果你的android:targetSdkVersion="23"或者更高,那么相机权限需要动态申请

2、预览尺寸设置很重要,解决方法参考上面的相机预览画面的比例适配

3、在上面的代码中,你一定看到了很多90度,270度,之所以要设置旋转这个度数,是因为Android默认横屏是0度,而我们一般是竖着拍,所以后置需要旋转90度,由于前置是后置的180度,所以前置需要旋转270度!!!

4、将相机所在的Activity设置为只能竖屏会让你觉得这个世界更美好!!!不然横竖屏切换会让你崩溃。

Demo下载地址:https://github.com/CB2Git/BlogDemoRepository/tree/master/TestSurfaceViewCamera

发表评论

您的电子邮箱地址不会被公开。