概述:

有些开发者可能会需要一个自定义的相机用户接口, 以实现自己独特样式的相机和特殊的功能. 创建一个自定义相机activity比调用系统相机需要更多的代码, 但是它可以为用户提供更加丰富的体验.

注意: 这里介绍的是使用老版本的Camera类, 该类已经不推荐使用. 推荐使用的类是android.hardware.camera2.该类在API21中引入. 

为一个APP创造一个自定义相机接口的普通步骤是这样的:

1.      检测和访问相机– 创建代码来检查是否存在相机并申请访问.

2.      创建一个Preview类– 创建一个相机预览类, 该类继承SurfaceView并实现SurfaceHolder接口. 这个类负责预览相机拍摄到的图像.

3.      创建一个Preview用的Layout – 一旦有了相机预览类, 就得创建一个layout来合并预览和给用户提供的操作界面.

4.      为拍照功能设置监听– 为我们的启动拍照或者视频的接口控制连接监听器来响应用户的操作, 比如点击按钮拍照.

5.      抓取和保存文件– 写代码实现抓取图片或者视频, 并保存输出.

6.      释放camera – 用完之后, APP必须适时释放以供其它APP使用.

相机硬件是一种共享资源, 它必须被小心的使用这样就不会让我们的APP与其它需要使用它的APP发生冲突. 下面的小节将会讨论如何检测相机硬件, 如何请求访问相机, 如何抓取图片或者视频和用完之后如何释放相机资源.

注意, 请一定记得在使用完毕后及时释放相机资源. 否则所有后续尝试访问相机的APP包括我们自己的APP都无法再访问相机资源, 并很可能引起APP闪退.

检测相机硬件:

如果我们的APP没有在manifest中声明需要使用相机资源, 我们应该在运行时检测相机资源是否可用. 想要实现这个操作, 需要使用PackageManager.hasSystemFeature()方法, 栗子:


/** 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;
    }
}


Android设备可以拥有多个相机, 比如一个用于拍照的主摄像头和一个用于视频通话或者自拍的前置摄像头. Android 2.3及更高版本允许我们使用Camera.getNumberOfCameras()方法检测可用相机的数量.

访问相机:

如果在APP运行的设备上确定有一个相机, 那么可以使用Camera的实例来请求访问它(或者使用intent访问). 想要在底层访问相机, 需要使用Camera.open()方法并确认捕获任何异常, 栗子:


/** 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
}


要注意在使用Camera.open()方法访问相机的时候必须检查异常. 如果相机不存在或者检查失败的话会导致APP挂掉.

在Android 2.3及更高版本中, 我们可以使用Camera.open(int)访问指定的相机. 上面的栗子将会访问第一个, 就是后置摄像机.

检查相机功能:

一旦取得了对相机的访问, 我们就可以通过Camera.getParameters()方法获得更多的相机功能的信息, 该方法会返回一个Camera.Parameters对象, 可以通过它查看相机功能. 当使用API 9 及更高版本时, 使用Camera.getCameraInfo(0方法来决定相机是在设备的前面还是后面, 以及画面的方向.

创建一个预览类:

对于用户来说, 如果他们想拍出靠谱的图片或者视频, 他们必须能看到设备相机所看到的内容. 这时候就需要一个预览的窗口让用户可以看到相机拍到的东西, 相机预览类是SurfaceView, 它可以显示从相机直播的图像数据.

下面的栗子展示了如何创建一个基本的相机预览类, 并可以放在View layout中. 这个类实现了SurfaceHolder.Callback接口, 它可以用来捕捉view创建和回收的事件.


/** A basic Camera preview class */
public classCameraPreview extendsSurfaceView implementsSurfaceHolder.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 thepreview.
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch(IOException e){
            Log.d(TAG,"Errorsetting 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,"Errorstarting camera preview: " + e.getMessage());
        }
    }
}


如果想要为相机预览设置一个指定的大小, 则需要在surfaceChanged()中设置. 当设置预览窗口的大小的时候, 必须使用从getSupportedPreviewSizes()提供的值. 不要在setPreviewSize()方法中设置随意的值.

在layout中放置preview:

相机预览类(比如前一节的栗子)必须被放置在layout中并跟它的用户控制接口(比如拍照或者摄像键)放在一起. 本的小节将会展示如何创建一个基础的layout和activity用来预览. 下面的layout代码提供了一个非常基础的view, 它可以用来显示一个相机的预览. 在这个栗子中, FrameLayout标签用于作为相机预览类的容器. 使用这种layout是为了让更多的图片信息或者控件可以覆盖在相机的预览图像上.


<?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>


在大多数设备上, 默认的相机方向是横向的. 这个栗子中的layout指定了一个横向的layout, 并且使用下面的代码修正APP的屏幕方向为横向. 为简单起见, 可以在manifest中通过这段代码修改APP预览activity的方向到横屏:


<activity android:name=".CameraActivity"
          android:label="@string/app_name"

          android:screenOrientation="landscape">
          <!-- configure this activity to use landscapeorientation -->

          <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>


一个相机预览窗口并不一定必须是横屏模式, 从Android2.2开始, 我们可以使用setDisplayOrientation()方法来设置预览图像的方向. 为了在用户旋转屏幕的时候改变预览方向, 在surfaceChanged()方法中, 首先通过Camera.stopPreview()停止预览, 然后改变方向, 之后再用Camera.startPreview()方法重新启动预览.

在相机预览的activity中, 向FrameLayout中添加预览类. 相机activity必须确保相机暂停或者关闭的时候释放相机资源. 下面的代码展示了如何修改相机activity来关联预览类:


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);
    }
}


getCameraInstance()方法是在”访问相机”小节实现的.

捕捉图片(拍照..):

一旦预览类创建完毕, 并且已经可以在一个layout中显示, 就表示已经为捕捉图片做好了准备. 在APP代码中我们必须为用户接口设置监听器以相应用户的操作. 想要获取一个图片, 要使用Camera.takePicture()方法. 该方法接收三个参数用来获取相机传回的数据. 为了接收接收JPEG格式的数据, 我们必须实现一个Camera.PictureCallback接口来接收图片数据, 并将它写入一个文件中. 下面的代码展示了一个基础的Camera.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,"Errorcreating 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 notfound: " + e.getMessage());
        } catch(IOException e){
            Log.d(TAG,"Erroraccessing file: " + e.getMessage());
        }
    }
};


通过Camera.takePicture()方法来触发拍照操作. 下面的栗子展示了如何从一个button的View.OnClickListener中调用该方法:


// 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);
        }
    }
);


mPicture变量在之前的栗子中定义.

注意: 一定要记得在APP用完相机之后释放它. 释放方法是Camera.release().

捕捉视频(摄像…):

使用Android framework捕捉视频需要对Camera对象很仔细的使用和管理, 并且需要跟MediaRecorder类协作使用. 当使用Camera类录制视频的时候, 我们必须管理Camera.lock()和Camera.unlock()方法来允许MediaRecorder访问相机硬件, 除此之外还有Cemra.open()和Camera.release()方法.

从Android4.0开始, Camera.lock()和Camera.unlock()方法已经可以自动管理了.

不同于使用设备拍照, 录像需要一个非常特殊的顺序. 详情如下:

1.      打开相机– 使用Camera.open()方法来获取一个相机对象的实例.

2.      连接预览窗口– 通过Camera.setPreviewDisplay()方法连接一个SurfaceView到相机, 这样就可以预览相机的图像了.

3.      开始预览– 调用Camera.startPreview()方法来启动显示直播的相机图像.

4.      开始录制视频– 想要成功录制视频, 必须完成下面的步骤:

a)        解锁相机– 通过Camera.unlock()方法为MediaRecorder解锁相机.

b)        配置 MediaRecorder – 按照下面指定的顺序调用MediaRecorder中的下列方法.

                        i.             setCamera – 设置相机用于录像, 使用APP当前的Camera实例.

                      ii.             setAudioSource() – 设置音频源, 使用MediaRecorder.AudioSource.CAMCORDER.

                     iii.             setVideoSource() – 设置视频源, 使用MediaRecorder.VideoSource.CAMCORDER.

                     iv.             设置视频输出的编码格式. 对于Android2.2及更高版本, 使用MediaRecorder.setProfile方法, 并使用CamcorderProfile.get()获取一个profile实例. 对于Android2.2以前的版本, 必须设置视频输出格式和编码参数: setOutputFormat()方法用于设置输出格式, 默认设置是MediaRecorder.OutputFormat.MPEG_4. setAudioEncoder()方法用于设置声音编码类型,默认值是MediaRecorder.AudioEncoder.AMR_NB. setVideoEncoder()方法用于设置视频编码类型,默认值是MediaRecorder.VideoEncoder.MPEG_4_SP.

                      v.             setOutputFile() – 设置输出文件, 使用”保存媒体文件”小节中的getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()方法.

                     vi.             setPreviewDisplay() – 为APP指定SurfaceView预览layout. 使用”连接预览窗口”中相同的对象.

注意, 我们必须按照上面的顺序调用这些MediaRecorder配置方法, 否则APP将会出错, 录制也将会失败.

c)        准备MediaRecorder – 通过MediaRecorder.prepare()方法及提供的配置设置项准备MediaRecorder.

d)        启动MediaRecorder – 使用MediaRecorder.start()方法启动录制视频.

5.      停止录制视频– 按顺序调用下面的方法来完成视频录制:

a)        停止MediaRecorder – 使用MediaRecorder.stop()方法停止视频录制.

b)        重置MediaRecorder – 可选, 从recorder中移除配置设置项. 方法是MediaRecorder.reset().

c)        释放MediaRecorder – 使用MediaRecorder.release()方法释放MediaRecorder.

d)        锁定相机– 使用Camera.lock()锁定相机, 这样未来的MediaRecorder会话就可以使用它了. 从Android4.0开始, 该方法不用调用了, 除非MediaRecorder.prepare()方法调用失败.

6.      停止预览– 当activity已经完成使用相机, 使用Camera.stopPreview()来停止预览.

7.      释放相机– 使用Camera.release()方法释放相机, 这样其它的APP才可以再次使用它.

注意: 不创建相机预览而使用MediaRecorder是可行的. 但是用户通常更希望在拍摄之前可以看到预览.

提示: 如果我们的APP是用来录像的, 设置setRecordingHint(Boolean)为true来提前启动预览. 这个设置可以帮助减少开始录制话费的时间.

配置MediaRecorder:

当使用MediaRecorder类来录像的时候, 我们必须按顺序执行配置步骤, 然后调用MediaRecorder.prepare()方法来检查和实现配置. 下面的代码展示了如何合适的配置和准备MediaRecorder类:


private boolean prepareVideoRecorder(){

    mCamera = getCameraInstance();
    mMediaRecorder = new MediaRecorder();

    // Step 1:Unlock and set camera to MediaRecorder
    mCamera.unlock();
    mMediaRecorder.setCamera(mCamera);

    // Step 2: Setsources
    mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

    // Step 3: Set aCamcorderProfile (requires API Level 8 or higher)
    mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));

    // Step 4: Setoutput file
    mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());

    // Step 5: Setthe preview output
    mMediaRecorder.setPreviewDisplay(mPreview.getHolder().getSurface());

    // Step 6:Prepare configured MediaRecorder
    try {
        mMediaRecorder.prepare();
    } catch(IllegalStateException e){
        Log.d(TAG,"IllegalStateException preparing MediaRecorder:"+ e.getMessage());
        releaseMediaRecorder();
        return false;
    } catch(IOException e){
        Log.d(TAG,"IOException preparing MediaRecorder: " + e.getMessage());
        releaseMediaRecorder();
        return false;
    }
    return true;
}


Android2.2版本之前, 我们必须直接设置输出格式和编码格式参数, 而不是使用CamcorderProfile. 这种方法是这样的:


// Step3: Set output format and encoding (for versions prior to API Level 8)
    mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
    mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);


上面的MediaRecorder的录像参数提供了默认的设置项, 但是如果想要自定义的话, 我们可以使用这些方法:

l  setVideoEncodingBitRate()

l  setVideoSize()

l  setVideoFrameRate()

l  setAudioEncodingBitRate()

l  setAudioChannels()

l  setAudioSamplingRate()

启动和停止MediaRecorder:

当使用MediaRecorder类启动和停止视频录制的时候, 我们必须按顺序执行下面的步骤: 

1.      使用Camera.unlock()解锁相机.

2.      像上面栗子中那样配置MediaRecorder.

3.      使用MediaRecorder.start()开始录制.

4.      录制视频.

5.      调用MediaRecorder.stop()方法停止录制.

6.      使用MediaRecorder.release()方法释放媒体recorder.

7.      使用Camera.lock()锁定相机.

下面的栗子展示了如何使用一个button来合适的启动和停止视频录制:

注意, 当完成录制之后不要直接释放相机, 否则预览就会被停止.


private boolean isRecording = false;

// 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) {
            if (isRecording) {
                // stop recording and release camera
                mMediaRecorder.stop();  // stop the recording
                releaseMediaRecorder(); // release the MediaRecorder object
                mCamera.lock();         // take camera access back from MediaRecorder

                // inform the user that recording has stopped
                setCaptureButtonText("Capture");
                isRecording = false;
            } else {
                // initialize video camera
                if (prepareVideoRecorder()) {
                    // Camera is available and unlocked, MediaRecorder is prepared,
                    // now you can start recording
                    mMediaRecorder.start();

                    // inform the user that recording has started
                    setCaptureButtonText("Stop");
                    isRecording = true;
                } else {
                    // prepare didn't work, release the camera
                    releaseMediaRecorder();
                    // inform user
                }
            }
        }
    }
);


上面的代码中, prepareVideoRecorder()方法在”配置MediaRecorder”小节中实现. 该方法负责锁定相机, 配置和准备MediaRecorder实例.

释放相机:

相机是一个可以被设备上的APP共享的资源. 我们的APP可以在获取到一个Camera实例之后开始使用相机, 当使用结束之后, 必须记得释放相机对象, 还有在APP暂停的时候(Activity.onPause)也要记得释放. 如果APP没有合适的释放相机, 所有的接下来的访问相机的请求包括我们自己的APP都将会失败, 可能会导致我们的APP闪退.

想要释放一个Camera对象的实例, 需要使用Camera.release()方法. 栗子:


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;
        }
    }
}


保存媒体文件:

用户通过拍照或者录像创建的媒体文件应该被保存在设备的外部存储目录中(SD卡), 这样可以节省系统空间让用户可以在设备之外访问这些文件. 有很多种可以存放媒体文件的目录, 然而作为一个开发者, 我们应该只考虑两个标准的路径:

1.      Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)– 该方法返回标准的共享的和官方推荐的路径用来保存图片和视频. 该目录是共享的(public), 所以其他的APP也可以很容易的发现读取修改和删除保存在这里的文件. 如果我们的APP被用户卸载了, 保存在这里的媒体文件不会被移除. 想要避免干涉用户已经存在的图片和视频, 我们应该为APP创建一个子目录来存放自己的媒体文件. 该方法只能在Android2.2及之后的版本中使用. 更早的API版本可以参考这里.

2.      Context.getExternalFilesDir(Envireonment.DIRECTORY_PICTURES)– 该方法返回一个标准的路径用于保存图片和视频, 该路径是与APP关联的. 如果APP被卸载了, 那么任何保存在该路径下的文件都将被卸载. 该路径中的文件并不是加密的, 其它的APP可以读取修改和删除它们.

下面的栗子展示了如何为一个媒体文件创建一个File或者Uri路径:


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;
}


注意: Environment.getExternalStoragePublicDirectory()在Android2.2及更高版本中可用, 如果目标设备使用更早的Android版本, 则要使用Environment.getExternalStorageDirectory()方法代替.

 

总结:

获取相机对象(PackageManager, Camera); 创建预览窗口(SurfaceView,SurfaceHolder); 关联预览窗口和相机对象; 设置拍摄操作的事件监听器; 拍摄保存; 释放;

 

参考: https://developer.android.com/guide/topics/media/camera.html