现在的位置: 首页 > Android开发经验 > 正文

Android Camera开发(一)之基础知识

2016年11月10日 Android开发经验 ⁄ 共 15444字 ⁄ 字号 评论关闭

概述

Android手机关于Camera的使用,一是拍照,二是摄像,由于Android提供了强大的组件功能,为此对于在Android手机系统上进行Camera的开发,我们可以使用两类方法:一是借助Intent和MediaStore调用系统Camera App程序来实现拍照和摄像功能,二是根据Camera API自写Camera程序。

基础知识

Android系统提供API来支持自定义相机拍照和系统拍照,以下是有关的类:

  • Camera
    该类提供基础API来使用设备上的相机,且该类可以为你的应用提供拍照和录像相关的API。
  • SurfaceView
    该类用于显示相机的预览数据。
  • MediaRecorder
    该类提供相机录像相关的API。

注意事项

在你的应用程序能够在Android设备上使用相机之前,你应该考虑几个问题,那就是你的App打算如何使用相机拍照或者录像?

  • Camera需求的声明:
    使用相机功能对于你的应用程序来说是否很重要并且你不希望你的应用程序被安装在没有相机的机器上?如果是这样,那么你需要把相机需求声明在配置文件里。
  • 调用系统拍照还是自定义相机:
    你的应用程序该如何使用相机?你是否仅仅需要拍摄一张照片或者一个视频,或者你的应用程序希望提供一种使用相机的新的方式?
  • 存储:
    是否你的应用生成的图片和视频仅对你的应用可见,还是其他应用程序例如相册或者其他的多媒体和社交App也可以使用它们?你是否希望你的应用程序被卸载后,这些照片和视频仍然可用,还是一起被删除?

权限申明

  • Camera Permission - 你的应用必须申请相机权限才可以使用设备相机。

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

注意:如果你使用Intent调用系统相机,你的应用无需申请该权限。

  • Storage Permission - 如果你的应用需要保存照片或者视频到设备存储中,你必须在Manifest指定文件的写权限

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

  • Audio Recording Permission - 你必须申请录音权限才能使用相机来录像.

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

  • Location Permission - 当然如果你需要拍摄的照片记录地理位置,你同样需要申请如下权限

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

调用系统相机

你的应用可以通过发送一个Intent 到系统相机应用来实现抓取一张照片或者一段视频剪辑,然后将它们返回给你的应用。
使用camera intent调用系统相机流程如下:
(1)Compose a Camera Intent - 创建一个Intent请求用来拍照或者录像,有关的Intent类型如下:

  • MediaStore.ACTION_IMAGE_CAPTURE - 该Intent action 类型用于请求系统相机拍照。
  • MediaStore.ACTION_VIDEO_CAPTURE - 该Intent action 类型用于请求系统相机录像。

(2)Start the Camera Intent - 调用activity的startActivityForResult()方法来发送camera intent请求拍照或者录像,当发送camera intent 以后,当前应用会跳转到系统相机应用app界面,让用户可以拍照或者录像。
(3)Receive the Intent Result - 在你的应用中实现onActivityResult()回调方法去接收来自系统相机的拍摄结果。该方法在用户完成拍照或者录像以后由系统调用。

系统拍照

代码如下,按上面的三步走:

button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
}
});

...

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
/**
* 通过data取得数据
*/

if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
Bundle extras = data.getExtras();
Bitmap bitmap = (Bitmap) extras.get("data");
image.setImageBitmap(bitmap);
}
}

但是,现在手机像素这么高,万一图片特别大呢,会不会data过大而FC呢?放心,Android早就考虑到了,所以,data里面压根就不是完整的图片,它只是一张缩略图。所以,我们需要获取到拍摄的原图,就不能使用这种方法。但是我们可以这样做,我们可以指定MediaStore类的一个EXTRA_OUTPUT来指定拍摄图像保存的位置,相当于建立一个临时文件。在onActivityResult中,我们不使用data来获取图像,而是直接去读这个临时文件即可。如果自己代码指定了保存图片的uri,data里面就不会保存数据。

button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
}
});
...

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
/**
* 通过存储Uri取得数据
*/

if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
image.setImageURI(getOutputMediaFileUri(MEDIA_TYPE_IMAGE));
}
}
}

这样我们就可以获取到完整的拍摄图片了。后面你可以让图像显示出来。

下面来看看保存多媒体文件:
拍照或者录像生成的多媒体文件需要保存到手机存储目录中(SD Card),所以在应用中必须有往手机中写文件的权限。一般可以有多种本地路径来保存多媒体文件,但是主要有如下两种常用的路径:

  • Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
    该方法返回一个标准的外部存储路径去保存照片和视频。这个路径是公共的,所以其他应用也可以访问,修改,删除该路径下的照片和视频,如果你的应用被卸载了,媒体文件依然存在本地储存中。为了避免和其他多媒体文件混淆,你应该在公共目录下创建一个子目录来保存你自己应用中的多媒体数据。
  • Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    该方法返回一个标准的,唯独当前应用自己可见的路径去保存照片和视频。如果该应用被卸载,在该目录下的所有多媒体数据将会被移除。但是有一个好处就是其他应用无法去访问,修改,删除该路径下的文件。

如下示例代码演示如何创建一个路径用来保存照片和视频:

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){
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "WatsonCamera");

if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()){
return null;
}
}

File mediaFile;
if (type == MEDIA_TYPE_IMAGE){
mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_watson.jpg");
} else if(type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator + "VID_watson.mp4");
} else {
return null;
}

return mediaFile;
}

系统录像

发送Intent录像携带的外部数据extra的信息如下:

  • MediaStore.EXTRA_OUTPUT
    该关键字和拍照使用的关键字一样,意思就是制定一个路径和文件名来构建一个Uri对象来保存录像结果。
  • MediaStore.EXTRA_VIDEO_QUALITY
    该关键字用于指定拍摄的录像质量,参数0表示低质量,参数1表示高质量。
  • MediaStore.EXTRA_DURATION_LIMIT
    该关键之用于指定拍摄的录像的时间限制,单位是秒。
  • MediaStore.EXTRA_SIZE_LIMIT
    该关键字用于指定拍摄的录像文件大小限制,单位值byte。

代码如下,按上面的三步走:

button2.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
Uri fileUri = getOutputMediaFileUri(MEDIA_TYPE_VIDEO);
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
startActivityForResult(intent, CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE);
}
});

...

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
image.setVisibility(View.VISIBLE);
video.setVisibility(View.GONE);
image.setImageURI(getOutputMediaFileUri(MEDIA_TYPE_IMAGE));
}
} else if (requestCode == CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
video.setVisibility(View.VISIBLE);
image.setVisibility(View.GONE);
video.setVideoURI(getOutputMediaFileUri(MEDIA_TYPE_VIDEO));
video.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
video.start();
}
});
video.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (null != video) {
video.stopPlayback();
}
}
});
}
}
}

自定义相机

创建一个自定义的相机app基本遵循如下步骤:

  • 检测和访问相机:
    首先代码检测该设备相机是否存在,如果存在才能请求访问设备相机。
  • 创建一个预览来显示相机图像:
    在你的布局中使用SurfaceView控件,然后在代码中继承SurfaceHolder.Callback接口并且实现接口中的方法来显示来自相机的图像信息。
  • 设置相机基本参数:
    根据需求设置相机预览尺寸,图片大小,预览方向,图片方向等。
  • 设置拍照录像监听:
    当用户按下按钮时调用Camera.takePicture()或者MediaRecorder.start()来进行拍照或录像。
  • 文件保存:
    当拍照结束或者录像视频结束时,需要开启一个后台线程去保存图片或者视频文件。
  • 释放相机资源:
    Camera硬件是一个共享资源,所以你必须小心的编写你的应用代码来管理相机资源。一般在Activity的生命周期的onResume中开启相机,在onPause中释放相机。

注意: 当你不在使用相机资源时,记得调用Camera.release()方法来释放相机资源,否则其他应用甚至你自己的应用再次请求访问设备相机时会失败,并且crash。

检测相机硬件是否存在

一般情况,我们会在运行代码时检测该设备是否有相机硬件,如果有相机硬件,才进一步去访问相机,如下是检测相机硬件是否存在是代码示例:

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

Android 设备可以有多个相机硬件,现在一般手机都是前后两个camera,因此我们在Android2.3以后也可以使用Camera.getNumberOfCameras()方法来获得当前设备camera个数来判断相机硬件是否存在。

创建Camera预览

Camera预览布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<RelativeLayout
android:id="@+id/record_navigation_bar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#F8F8F8" >

<ImageView
android:id="@+id/record_act_back"
android:layout_width="25dp"
android:layout_height="31dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:src="@drawable/icon_ll_back" />

</RelativeLayout>

<SurfaceView
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_above="@+id/record_bottom_bar"
android:layout_below="@+id/record_navigation_bar" />

<RelativeLayout
android:id="@+id/record_bottom_bar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:background="@drawable/recording_bottom_bar_bg_interview" >

<Button
android:id="@+id/btn_start_recording"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_centerInParent="true"
android:background="@drawable/recording_act_vedio_start" />

<Button
android:id="@+id/btn_change_module"
android:layout_width="45dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginLeft="30dp"
android:background="@drawable/change_module_photo" />

</RelativeLayout>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:layout_below="@+id/record_navigation_bar"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical" >

<View
android:id="@+id/record_video_tip"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginRight="10dp"
android:background="@drawable/record_video_tip" />

<TextView
android:id="@+id/record_video_time"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="17sp" />

</LinearLayout>

</RelativeLayout>

这里写图片描述

然后,我们创建一个Activity,用来展示Camera的预览,那么在这个Activity里面,我们需要做什么呢?两件事情:

  • 初始化相机
  • 将内容显示到SurfaceView

Android的Camera是独享的,如果多处调用,就会抛出异常,所以,我们需要将Camera的生命周期与SurfaceView的生命周期绑定:

  • surfaceCreated方法中初始化相机
  • surfaceDestroyed方法中释放相机

初始化相机非常简单:

private Camera getCamera() {
Camera camera;
try {
camera = Camera.open();
} catch (Exception e) {
camera = null;
}
return camera;
}

注意: 在调用Camera.open()方法时总是要去捕获一个异常,以免打开相机设备失败导致整个应用crash。在Android2.3以及更高api上,你可以使用Camera.open(int)来打开指定的相机。以上代码示例总是默认打开后置camera,一般情况参数为0表示打开后置camera,参数为1表示打开前置camera。
释放相机也非常简单:

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (camera != null) {
try {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
camera = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}

那么下面我们再来看如何把相机图像设置到SurfaceView中进行预览:

@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
camera = getCamera();
camera.setPreviewDisplay(holder); //camera关联到SurfaceView
camera.setDisplayOrientation(90); //旋转90度
camera.startPreview(); //开始预览
} catch (Exception e) {
finish();
}
}

是不是也非常简单,camera的一个方法已经帮我们自动关联了SurfaceView。
这里需要注意下这个方法camera.setDisplayOrientation(90),通过这个方法,我们可以调整摄像头的角度,不然默认是横屏,图像会显示的比较奇怪。当然,即使你设置了90,图像也有可能比较奇怪,这是因为你没有对图像进行正确的缩放,比例不对。

通过上面的设置,我们已经可以正常预览摄像头的图像内容了。
这里写图片描述

拍照

一旦你创建了camera preview并且加载到布局中可以实时显示预览画面了,此时就可以进行拍照了。为了配合拍照,我们需要做一些设置,设置拍照参数,当然你也可以不设置而使用默认参数,默认参数基本上就能满足我们的要求。

Camera.Parameters params = mCamera.getParameters();
params.setPictureFormat(ImageFormat.JPEG);
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
mCamera.setParameters(params);
//自动聚焦
camera.autoFocus(new AutoFocusCallback() {
public void onAutoFocus(boolean success, Camera camera) {
if (success)
System.out.println("聚焦成功 !");
else
System.out.println("聚焦失败 !");
}
});

在代码中你应该实现一个监听回调来捕获用户拍照的行为。可以调用camera.takePciture()方法来进行拍照。

public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg);

该方法接受三个参数,第一个参数ShutterCallback响应快门的接口,第二个参数PictureCallback接收raw格式的图片数据,第三个参数PictureCallback接收jpeg格式的图片数据。为了保存图片数据,你可以根据需要实现以上三个接口。此处我们暂且实现第三个PictureCallback接口回调。示例代码如下:

//拍照
if (camera != null) {
camera.takePicture(null, null, mPictureCallback);
}

//第三个PictureCallback接口回调,通过data[]保持图片数据信息
private Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
File pictureFile = MainActivity.getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (pictureFile == null){
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(RecordVedioAct.this, "图像已保存", Toast.LENGTH_SHORT).show();
camera.startPreview(); //拍完继续预览
}
};

录像

Camera视频录像不仅涉及到Camera类还用到了MediaRecorder类。当你使用Camera录像时,你应该调用Camera.lock()和Camera.unlock()来管理camera硬件,允许MediaRecorder访问camera硬件。你应该在camera和MediaRecorder关联之前调用Camera.unlock()来解锁camera,允许MediaRecorder访问Camera,在释放MediaRecorder资源以后调用Camera.lock()来锁定camera以保证camera硬件资源的共享性。
注:在Android4.0以后,系统会自动管理camera.unlock()以及camera.lock(),无需用户自己管理。

启动录像流程需要一个指定调用顺序,如下是详细的步骤流程:
(1)Open Camera – 使用Camera.open()静态方法来获得camera对象实例。
(2)Connect Preview – 使用camera.setPreviewDiaplay(holder)方法将相机的预览画面显示在SurfaceView控件上。
(3)Start Preview – 使用camera.startPreview()方法开始启动预览画面。
(4)Start Recording Video – 必须完成以下步骤才能正常开始正常录音:

  • Unlock the Camera - 调用camera.unlock()方法解锁camera,使得MediaRecorder进程能访问Camera硬件。
  • Configure MediaRecorder - 在这一步,分别调用MediaRecorder类中如下方法来配置MediaRecorder:

/**配置MediaRecorder*/
recorder.setCamera(camera); //设置camera用于录像
recorder.setOutputFile(filePath); //设置输出文件路径
recorder.setAudioSource(MediaRecorder.AudioSource.MIC); //设置录像音频来源
recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //设置录像视频来源
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //设置视频的输出格式
recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); //设置视频的编码格式
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //设置音频的编码格式
/**输出格式和编码格式,对于Android2.2或者更高版本使用MediaRecorder.setProfile方法即可,使用方法CamcorderProfile.get()来获得一个配置信息*/
recorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
setPreviewDisplay(holder.getSurface()) //为MediaRecorder指定预览显示

注意:在这一步,你必须调用MediaRecorder类中的以上全部方法来配置MediaRecorder,否则你的应用将无法正常录像并且报错。

由于录像默认设置了很多参数,无需用户太关心更细节的参数设置,但是如果需要在你的应用中修改这些默认参数设置,你可以使用如下方法来修改默认参数:

recorder.setAudioEncodingBitRate(); //设置音频编码的字节率
recorder.setVideoEncodingBitRate(); //设置视频编码的字节率
recorder.setOrientationHint(tureAngle); //设置MediaRecorder旋转角度
recorder.setAudioSamplingRate(); //设置音频采样率
recorder.setMaxDuration(5 * 60 * 1000); //设置最大录制时间
recorder.setVideoSize(640, 480); //设置视频尺寸大小,在setVideoSource()和setOutFormat()之后
recorder.setVideoFrameRate() //设置视频帧率,在setVideoSource()和setOutFormat()之后
recorder.setAudioChannels(); //设置音频的频道数目,参数一般1/2

  • Prepare MediaRecorder - 在配置完MediaRecorder参数之后调用mediaRecorder.prepare()方法来准备MediaRecorder。
  • Start MediaRecorder - 调用mediaRecorder.start()方法启动录像。

(5)Stop Recording Video – 当你结束录像时调用如下方法:

  • Stop MediaRecorder - 首先调用 mediaRecorder.stop()方法停止多媒体录像。
  • Reset MediaRecorder - 调用mediaRecorder.reset()方法重置多媒体状态,调用该方法之后之前的所有MediaRecorder configuration将被移除,你如果还想再次录像,需要再次配置多媒体参数。
  • Release MediaRecorder - 调用 mediaRecorder.release()方法释放多媒体资源。
  • Lock the Camera - 调用camera.lock()方法来给Camera硬件加锁。在Android4.0及以后无需调用该方法,除非在调用mediaRecorder.prepare()失败时,才需要再次调用该方法。

(6)Stop the Preview - 当你的Activity已经不再使用camera时,调用camera.stopPreview()方法来停止预览。
(7)Release Camera - 当不再使用Camera时,调用camera.release()方法来释放camera,以便其他应用可以使用camera资源。

注意: 当完成一段视频录像时,不要马上去释放camera资源或者停止当前预览,因为有可能用户会再次启动录像操作。本文中将camera释放操作放在surfaceDestroyed里面。

如下代码演示在button的点击事件中去启动和停止视频录像操作:

/**录像*/
if (isRecording) {
stopRecord();
} else {
startRecord();
}

// 开始录像
private void startRecord() {
if (prepareVideoRecorder()) {
mediaRecorder.start();
//修改状态
isRecording = true;
Toast.makeText(RecordVedioAct.this, "开始录像", Toast.LENGTH_SHORT).show();
btn_start_recording.setBackgroundResource(R.drawable.recording_act_vedio_stop);
start_time = 0;
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
start_time++;
handler.sendEmptyMessage(0);
}
}, 0, 1000);
} else {
mediaRecorder.release();
camera.lock();
}
}

// 停止录像
private void stopRecord() {
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
camera.lock();
//修改状态
isRecording = false;
btn_start_recording.setBackgroundResource(R.drawable.recording_act_vedio_start);
timer.cancel();
record_video_tip.setVisibility(View.VISIBLE);
Toast.makeText(RecordVedioAct.this, "录像已保存", Toast.LENGTH_SHORT).show();
}

private boolean prepareVideoRecorder(){
mediaRecorder = new MediaRecorder();
camera.unlock();
mediaRecorder.setCamera(camera);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
mediaRecorder.setOutputFile(MainActivity.getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());
mediaRecorder.setPreviewDisplay(myHolder.getSurface());
try {
mediaRecorder.prepare();
} catch (Exception e) {
mediaRecorder.release();
camera.lock();
return false;
}
return true;
}

Demo下载地址

抱歉!评论已关闭.

×