Android 使用 Camera API

原文地址:https://blog.csdn.net/timshinlee/article/details/54598740

谷歌官方文档地址:

https://developer.android.google.cn/training/camera/cameradirect.html 
https://developer.android.google.cn/guide/topics/media/camera.html#custom-camera 

============================================================================

安卓的camera API提供开发这 预览 拍照 录像等功能 , Android 5.0(API 21)后谷歌推出功能更加强大的Camera2 API 来代替旧的Camera API , 不过在硬件的能力等级较低时, camera2和camera几乎无异。本文介绍旧的camera API、的简单使用方法。

自定义相机的使用一般可以分为几个步骤: 
1. 检测相机并要求使用 
2. 创建继承SurfaceView和实现SurfaceHolder接口的Preview类 
3. 创建预览布局 
4. 设置拍照监听 
5. 拍照以及保存图片 
6. 释放相机 
注意,相机使用完毕必须调用Camera.release()方法释放,否则后续所有使用相机的请求都会失败而且可能导致应用关闭。

清单声明

使用相机必须添加权限:(如果是使用系统相机应用可以不添加)

<uses-permission android:name="android.permission.CAMERA" />

同时必须声明使用特性,具体可以指定特定特性,该项是用在Google Play过滤不符要求的设备。可以设置required属性为必要或不必要

<uses-feature android:name="android.hardware.camera" android:required="false"/>

如果需要保存到sd卡

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

如果需要录像

<uses-permission android:name="android.permission.RECORD_AUDIO" />

如果需要使用GPS定位信息

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />

检测相机

如果在清单文件中没有要求必须有相机,则需要在运行时检测是否有相机。Android 2.3(API 9)开始可以调用Camera.getNumberOfCameras()获取相机数目。

/** Check if this device has a camera */
private boolean checkCameraHardware(Context context) {
    if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
        // this device has a camera
        return true;
    } else {
        // no camera on this device
        return false;
    }
}

打开相机

正如系统相机所做的,打开相机的推荐方式是在onCreate()中开启一条子线程打开,因为打开相机的操作可能阻塞主线程。甚至打开相机的操作可以推迟到onResume()中进行,这样可以达到代码复用和简化操作的效果。 
如果相机正在被其他应用使用,则调用Camera.open()会抛出异常,所以需要添加try-catch语句。以下两段实例代码显示如何获取相机实例,永远记得必须检测并捕获异常。 
从API 9开始,android支持多个摄像头,所以如果无参调用open()会默认使用后置摄像头。可以调用Camera.open(int)方法指定摄像头。

/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance(){
    Camera c = null;
    try {
        c = Camera.open(); // attempt to get a Camera instance
    }
    catch (Exception e){
        // Camera is not available (in use or does not exist)
    }
    return c; // returns null if camera is unavailable
}
private boolean safeCameraOpen(int id) {
    boolean qOpened = false;

    try {
        releaseCameraAndPreview();
        mCamera = Camera.open(id);
        qOpened = (mCamera != null);
    } catch (Exception e) {
        Log.e(getString(R.string.app_name), "failed to open Camera");
        e.printStackTrace();
    }

    return qOpened;
}

private void releaseCameraAndPreview() {
    mPreview.setCamera(null);
    if (mCamera != null) {
        mCamera.release();
        mCamera = null;
    }
}

检查相机参数

可以调用Camera.getParameters()方法获取相机参数,从API 9开始可以调用 Camera.getCameraInfo()查看相机是前置或者后置,以及图像方向。

设置预览界面

使用SurfaceView绘制摄像头传递的图像。要开始图像预览,还需要一个实现android.view.SurfaceHolder.Callback接口的preview类,这个接口用来接收摄像头传递的图像信息。在开始预览之前,必须要把创建的这个preview类传递给Camera对象。 
【两页示例代码中使用两种方式创建这个preview类,一种是使用继承自SurfaceView的View,然后在xml或代码中把这个view添加到FrameLayout中。另一种是ViewGroup中包含SurfaceView,两种都实现了SurfaceHolder的回调接口】 
SurfaceHolder回调接口是用来捕捉创建和销毁view的过程,方便图片处理。 
方式一:

/** A basic Camera preview class */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private SurfaceHolder mHolder;
    private Camera mCamera;

    public CameraPreview(Context context, Camera camera) {
        super(context);
        mCamera = camera;

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        mHolder.addCallback(this);
        // deprecated setting, but required on Android versions prior to 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, now tell the camera where to draw the preview.
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // empty. Take care of releasing the Camera preview in your activity.
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // If your preview can change or rotate, take care of those events here.
        // Make sure to stop the preview before resizing or reformatting it.

        if (mHolder.getSurface() == null){
          // preview surface does not exist
          return;
        }

        // stop preview before making changes
        try {
            mCamera.stopPreview();
        } catch (Exception e){
          // ignore: tried to stop a non-existent preview
        }

        // set preview size and make any resize, rotate or
        // reformatting changes here

        // start preview with new settings
        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

        } catch (Exception e){
            Log.d(TAG, "Error starting camera preview: " + e.getMessage());
        }
    }
}
class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView mSurfaceView;
    SurfaceHolder mHolder;

    Preview(Context context) {
        super(context);

        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
...
}

必须先创建Camera实例,然后才创建对应的preview类。下面这个方法以上一步创建好的Camera实例为参数,以调用相机的开始预览方法结束。该方法可以用于用户改变相机的时候,具体实现会先停止预览,释放相机资源,然后重新开始预览。注意,也必须在Preview类的surfaceChanged()方法中重启预览

public void setCamera(Camera camera) {
    if (mCamera == camera) { return; }

    stopPreviewAndFreeCamera();

    mCamera = camera;

    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mSupportedPreviewSizes = localSizes;
        requestLayout();

        try {
            mCamera.setPreviewDisplay(mHolder);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        mCamera.startPreview();
    }
}

修改相机设置

修改预览效果在surfaceChanged()中进行,注意预览尺寸不可随意设置,必须使用Camera实例对象.getParameters().getSupportedPreviewSizes()中的值。(Android 7.0(API 24)开始支持多窗口特性,所以即使调用了 setDisplayOrientation()之后也不能肯定预览的纵横比和Activity的一致。取决于窗口大小和纵横比,可能需要在垂直布局中使用letterbox布局来适配宽相机预览,或者反之。) 
下面的实例代码实现了修改预览尺寸的效果,更多相机设置的修改可以参照系统相机的源码。

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    // Now that the size is known, set up the camera parameters and begin
    // the preview.
    Camera.Parameters parameters = mCamera.getParameters();
    parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
    requestLayout();
    mCamera.setParameters(parameters);

    // Important: Call startPreview() to start updating the preview surface.
    // Preview must be started before you can take a picture.
    mCamera.startPreview();
}

设置预览方向

大多数拍照应用把预览方向设为水平,因为这是相机传感器的自然方向。 
可以在清单文件中对应Activity标签添加android:screenOrientation=”landscape”来简化预览的渲染。但是也可以实现垂直方向预览,因为手机的方向记录在了EXIF header当中。2.2(API 8)开始可以调用setCameraDisplayOrientation()方法可以在不影响图像形成的情况下旋转预览方向。但是在API 14之前必须先停止预览,然后改变方向,最后重启预览。

布局设置

可以在FrameLayout中添加自定义的预览类

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
  <FrameLayout
    android:id="@+id/camera_preview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    />

  <Button
    android:id="@+id/button_capture"
    android:text="Capture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    />
</LinearLayout>
public class CameraActivity extends Activity {

    private Camera mCamera;
    private CameraPreview mPreview;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Create an instance of Camera
        mCamera = getCameraInstance();

        // Create our Preview view and set it as the content of our activity.
        mPreview = new CameraPreview(this, mCamera);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);
    }
}

拍照

在开始预览之后,可以调用 Camera.takePicture()拍照。

// Add a listener to the Capture button
Button captureButton = (Button) findViewById(id.button_capture);
captureButton.setOnClickListener(
    new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // get an image from the camera
            mCamera.takePicture(null, null, mPicture);
        }
    }
);

可以传递进去实现Camera.PictureCallback和Camera.ShutterCallback的对象。想要以JPEG格式接收数据的话,就必须要实现Camera.PictureCallback接口接收数据以及保存到文件中,以下是实现PictureCallBack的示例代码。

private PictureCallback mPicture = new PictureCallback() {

    @Override
    public void onPictureTaken(byte[] data, Camera camera) {

        File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
        if (pictureFile == null){
            Log.d(TAG, "Error creating media file, check storage permissions: " +
                e.getMessage());
            return;
        }

        try {
            FileOutputStream fos = new FileOutputStream(pictureFile);
            fos.write(data);
            fos.close();
        } catch (FileNotFoundException e) {
            Log.d(TAG, "File not found: " + e.getMessage());
        } catch (IOException e) {
            Log.d(TAG, "Error accessing file: " + e.getMessage());
        }
    }
};

如果想要持续拍照,可以创建实现了onPreviewFrame()的 Camera.PreviewCallback。也可以拍下选择的特定帧图片,或者延迟调用takePicture()

录像–暂时跳过

重启预览

拍完一张照片之后,必须要重启预览才能继续拍照。以下实例代码是在快门的点击事件中判断预览状态,从而确定是拍照还是重启预览。

@Override
public void onClick(View v) {
    switch(mPreviewState) {
    case K_STATE_FROZEN:
        mCamera.startPreview();
        mPreviewState = K_STATE_PREVIEW;
        break;

    default:
        mCamera.takePicture( null, rawCallback, null);
        mPreviewState = K_STATE_BUSY;
    } // switch
    shutterBtnConfig();
}

停止预览,释放相机

相机使用完毕必须记得释放,否则会导致下一次使用相机失败,包括本应用和其他应用都会。 
恰当时机是在预览destroyed的时候进行,下面的surfaceDestroyed()是preview类实现的方法。Activity.onPause()中也要记得释放。

public class CameraActivity extends Activity {
    private Camera mCamera;
    private SurfaceView mPreview;
    private MediaRecorder mMediaRecorder;

    ...

    @Override
    protected void onPause() {
        super.onPause();
        releaseMediaRecorder();       // if you are using MediaRecorder, release it first
        releaseCamera();              // release the camera immediately on pause event
    }

    private void releaseMediaRecorder(){
        if (mMediaRecorder != null) {
            mMediaRecorder.reset();   // clear recorder configuration
            mMediaRecorder.release(); // release the recorder object
            mMediaRecorder = null;
            mCamera.lock();           // lock camera for later use
        }
    }

    private void releaseCamera(){
        if (mCamera != null){
            mCamera.release();        // release the camera for other applications
            mCamera = null;
        }
    }
}

stopPreviewAndFreeCamera()也在上面的setCamera()中调用了,所以初始化相机的时候必须记得先停止预览。【好像实例代码surfaceDestroyed()中没有释放相机】

public void surfaceDestroyed(SurfaceHolder holder) {
    // Surface will be destroyed when we return, so stop the preview.
    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();
    }
}

/**
 * When this function returns, mCamera will be null.
 */
private void stopPreviewAndFreeCamera() {

    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        mCamera.release();

        mCamera = null;
    }
}

保存媒体文件

图片和录像必须保存在sd卡中以节省系统空间以及方便在没有手机的情况下使用。作为一名开发者,只有两个标准路径可以考虑作为保存目录 
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES):该方法返回一个标准的公开的保存图片录像的路径,一般是/mnt/sdcard/Pictures。所有应用都可以修改该路径下的文件。即使某应用被删除,该应用保存到该路径的媒体文件仍然保持不变。为了避免与原有文件混淆,可以在这个目录下创建一个本应用的子目录来保存媒体文件 
- Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) : 
该方法返回一个与本应用相联系的保存媒体文件的路径,一般是/mnt/sdcard/Android/data/应用包名/files/Pictures。如果你的应用被删除了,该路径保存的所有文件也会被删除。4.4以后其他应用无法获取或修改该路径的文件。 
以下示例代码展示了如何创建一个File或Uri用来作为相机intent的参数或者作为自定义相机的一部分。

public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;

/** Create a file Uri for saving an image or video */
private static Uri getOutputMediaFileUri(int type){
      return Uri.fromFile(getOutputMediaFile(type));
}

/** Create a File for saving an image or video */
private static File getOutputMediaFile(int type){
    // To be safe, you should check that the SDCard is mounted
    // using Environment.getExternalStorageState() before doing this.

    File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
              Environment.DIRECTORY_PICTURES), "MyCameraApp");
    // This location works best if you want the created images to be shared
    // between applications and persist after your app has been uninstalled.

    // Create the storage directory if it does not exist
    if (! mediaStorageDir.exists()){
        if (! mediaStorageDir.mkdirs()){
            Log.d("MyCameraApp", "failed to create directory");
            return null;
        }
    }

    // Create a media file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    File mediaFile;
    if (type == MEDIA_TYPE_IMAGE){
        mediaFile = new File(mediaStorageDir.getPath() + File.separator +
        "IMG_"+ timeStamp + ".jpg");
    } else if(type == MEDIA_TYPE_VIDEO) {
        mediaFile = new File(mediaStorageDir.getPath() + File.separator +
        "VID_"+ timeStamp + ".mp4");
    } else {
        return null;
    }

    return mediaFile;
}

相机特性

大部分相机设定可以通过Camera.parameters获取并修改。Android支持一系列相机特性设置,具体可到原文查看。 
有三个特性的处理比较复杂: 
Metering and focus areas 测量和对焦 
Face detection 脸部识别 
Time lapse video 延时录像

检测相机特性是否可用

因为设备限制,不是所有的设备都支持所有的相机特性。即使支持,也可能只是有限度支持,所以需要检测特性是否可用。检测可以通过以下代码实现。同时parameters也支持getSupported…(), is…Supported()或者getMax…()判断是否支持以及支持程度。

// get Camera parameters
Camera.Parameters params = mCamera.getParameters();

List<String> focusModes = params.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
  // Autofocus mode is supported
}

可以在清单文件中声明需要的相机特性,Google Play会确保应用只安装在支持设备上。

使用相机特性

可以通过获取camera的parameters,然后设置属性,再设置回camera。

// get Camera parameters
Camera.Parameters params = mCamera.getParameters();
// set the focus mode
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// set Camera parameters
mCamera.setParameters(params);

几乎所有参数都支持这样的设置方式,设置效果也几乎实时展现,但是修改预览尺寸和相机方向需要先停止预览,然后修改再重启预览。Android 4.0(API 14) 开始修改相机方向不需要停止预览了。

测量和对焦

Android 4.0(API 14)开始可以指定局部进行对焦或者轻微设置,并应用到摄像头上。以下是设置两个测光区域的示例代码:

// Create an instance of Camera
mCamera = getCameraInstance();

// set Camera parameters
Camera.Parameters params = mCamera.getParameters();

if (params.getMaxNumMeteringAreas() > 0){ // check that metering areas are supported
    List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();

    Rect areaRect1 = new Rect(-100, -100, 100, 100);    // specify an area in center of image
    meteringAreas.add(new Camera.Area(areaRect1, 600)); // set weight to 60%
    Rect areaRect2 = new Rect(800, -1000, 1000, -800);  // specify an area in upper right of image
    meteringAreas.add(new Camera.Area(areaRect2, 400)); // set weight to 40%
    params.setMeteringAreas(meteringAreas);
}

mCamera.setParameters(params);

The Camera.Area包含两个数据字段,一个Rect对象是用来指定相机视野内的一个区域,还有一个权重值用来确定该区域在测光或者对焦计算的重要程度。 
上述代码确定了一个2000*2000的矩形区域,-1000,-1000分别是上坐标和左坐标,1000,1000分别是下坐标和右坐标。 
坐标系统 
图片红线表示用来确定Camera.area矩形区域的坐标系统。

坐标系统的边界总是对应预览界面的图像外边界,并不会因为图片缩放或者图片旋转而改变。

猜你喜欢

转载自blog.csdn.net/striver_jt/article/details/83017808