Android相机开发(三): 实现拍照录像和查看
Android Camera Develop: capture photo and video
概述
上篇完成了相机的偏好设置,本篇就要实现相机的核心功能——拍照和录像了。直觉上拍照和录像应该差别不大,但在Android中两者是有很大差别的,录像需要更多的步骤,以及更严格的处理逻辑。本篇还加入了对拍摄到的照片和视频的预览查看功能,就像其他相机APP那样。
生成文件名
因为拍照录像都是对相机的操作,所以这部分绝大多数都是在CameraPreview
中添加代码。首先我们添加生成文件名这一功能,我们采用通用的做法,生成的文件名首先标记是照片还是视频,然后是拍摄的时间,比如IMG_20160503_153218.jpg
和VID_20160505_080204.mp4
。
在CameraPreview
中添加
public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;
private Uri outputMediaFileUri;
private String outputMediaFileType;
private File getOutputMediaFile(int type) {
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), TAG);
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Log.d(TAG, "failed to create directory");
return null;
}
}
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");
outputMediaFileType = "image/*";
} else if (type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"VID_" + timeStamp + ".mp4");
outputMediaFileType = "video/*";
} else {
return null;
}
outputMediaFileUri = Uri.fromFile(mediaFile);
return mediaFile;
}
public Uri getOutputMediaFileUri() {
return outputMediaFileUri;
}
public String getOutputMediaFileType() {
return outputMediaFileType;
}
其中静态成员变量MEDIA_TYPE_IMAGE
和MEDIA_TYPE_VIDEO
标记文件为照片还是视频;outputMediaFileUri
和outputMediaFileType
则用来记录生成文件的URI和MIME类型。getOutputMediaFile()
则根据参数中指定的文件类型,生成File
类型的实例,供调用者写入文件;getOutputMediaFile()
会在Android外部存储的图片路径下生成TAG
文件夹,并在此生成文件,比如外部存储的Pictures/CameraDemo/
;因为Android拍摄到的照片一般是jpg
格式,视频一般是mp4
格式,所以直接写死;最后生成的文件还会与outputMediaFileUri
和outputMediaFileType
同步,而getOutputMediaFileUri()
和getOutputMediaFileType()
则是对外的接口,后面再解释。
拍照
拍照主要用到的是Camera
的takePicture()
方法,通过指定回调函数,将照片数据写入到文件中。
在CameraPreview
中添加
public void takePicture() {
mCamera.takePicture(null, null, new Camera.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");
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
camera.startPreview();
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found: " + e.getMessage());
} catch (IOException e) {
Log.d(TAG, "Error accessing file: " + e.getMessage());
}
}
});
}
调用takePicture()
时实际会调用mCamera.takePicture()
,其第三个参数即指定回调函数;当相机拍照完成后,就会触发onPictureTaken()
,其中data
参数就是jpeg格式的照片数据。我们只需要调用getOutputMediaFile()
获取输出文件,并向此文件写入照片数据就好了。
onPictureTaken()
触发后相机会停止预览,此时我们手动添加camera.startPreview()
让相机持续预览。另外takePicture()
是一个异步过程,需要注意。
录像
录像部分的代码很多,但其中绝大部分都是来自Android官方文档的,基本就是一个不变的套路。录像是交给MediaRecorder
类在做,大体上来说就是实例化一个MediaRecorder
,向其指定一系列参数,然后start()
开始录像,stop()
结束录像。
在CameraPreview
中添加
private MediaRecorder mMediaRecorder;
public boolean startRecording() {
if (prepareVideoRecorder()) {
mMediaRecorder.start();
return true;
} else {
releaseMediaRecorder();
}
return false;
}
public void stopRecording() {
if (mMediaRecorder != null) {
mMediaRecorder.stop();
}
releaseMediaRecorder();
}
public boolean isRecording() {
return mMediaRecorder != null;
}
private boolean prepareVideoRecorder() {
mCamera = getCameraInstance();
mMediaRecorder = new MediaRecorder();
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
String prefVideoSize = prefs.getString("video_size", "");
String[] split = prefVideoSize.split("x");
mMediaRecorder.setVideoSize(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());
mMediaRecorder.setPreviewDisplay(mHolder.getSurface());
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;
}
private void releaseMediaRecorder() {
if (mMediaRecorder != null) {
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null;
mCamera.lock();
}
}
prepareVideoRecorder()
即实例化MediaRecorder
为成员变量mMediaRecorder
,指定相机、音频源、视频源、录制视频参数、输出文件路径以及预览等,关于细节请查看官方文档。
其中的
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
String prefVideoSize = prefs.getString("video_size", "");
String[] split = prefVideoSize.split("x");
mMediaRecorder.setVideoSize(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
是从Preference
中读取视频分辨率偏好设置,并将其应用到mMediaRecorder
;实际上这一步可以省略,录制的视频分辨率会和相机预览分辨率保持相同。
startRecording()
首先调用prepareVideoRecorder()
准备录像(如果不成功则放弃),start()
开始录像。stopRecording()
则调用stop()
结束录像,并通过releaseMediaRecorder()
完成收尾工作。isRecording()
则用来判断当前是否正在录像。
申请权限
录像需要用到音频,而生成文件需要写入外部存储
在AndroidManifest.xml
中添加
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
添加按钮
上述只是完成了方法的设计,现在就要在UI上添加按钮,并实际调用啦。
添加按钮
修改activity_main.xml
为
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="设置" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<Button
android:id="@+id/button_capture_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照" />
<Button
android:id="@+id/button_capture_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="录像" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
UI大概像这样
绑定事件
在MainActivity
的onCreate()
最后添加
final Button buttonCapturePhoto = (Button) findViewById(R.id.button_capture_photo);
buttonCapturePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPreview.takePicture();
}
});
final Button buttonCaptureVideo = (Button) findViewById(R.id.button_capture_video);
buttonCaptureVideo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mPreview.isRecording()) {
mPreview.stopRecording();
buttonCaptureVideo.setText("录像");
} else {
if (mPreview.startRecording()) {
buttonCaptureVideo.setText("停止");
}
}
}
});
并在
CameraPreview mPreview = new CameraPreview(this);
前加final修饰符,即
final CameraPreview mPreview = new CameraPreview(this);
绑定拍照很容易不解释。录像会有开始和结束两种状态,这里我们仅用一个按钮实现,处理逻辑也很简单,主要就是用isRecording()
判断当前状态,根据当前状态选择处理方法,并修改button的text
属性。
运行一下试试
这样就完成了拍照和录像的基本功能,你可以在真机上运行APP,点击“拍照”和“录像”了,生成的文件你可以在外部存储Pictures
文件夹下的文件夹中找到。
解决后台返回时黑屏的问题
上一篇文章说到过这一篇会解决这个问题,现在就来着手解决啦,也是为下一节作铺垫。
原因分析
这里就不长篇大论从Activity
的生命周期和View
的关系细节讲了。黑屏的原因很简单,MainActivity
在被切换到后台时,其本身的状态是保留的,但在其中的CameraPreview
却被销毁了;当MainActivity
从后台返回后,MainActivity
状态恢复,而CameraPreview
只在其onCreate()
中实例化和加入窗口中,但MainActivity
不会再触发onCreate()
,造成黑屏。
解决方法
分析出上述原因后,解决方法也容易得出了。MainActivity
在从后台返回时,会触发onResume()
,我们只需要在其中像在onCreate()
中一样完成整个CameraPreview
的初始化,就解决这个问题了。
分离CameraPreview
初始化语句
为了之后的方便,将onCreate()
中涉及到CameraPreview
初始化的语句独立为方法,将onCreate()
中的
final CameraPreview mPreview = new CameraPreview(this);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
SettingsFragment.passCamera(mPreview.getCameraInstance());
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
SettingsFragment.setDefault(PreferenceManager.getDefaultSharedPreferences(this));
SettingsFragment.init(PreferenceManager.getDefaultSharedPreferences(this));
替换为
initCamera();
在MainActivity
中添加
private CameraPreview mPreview;
private void initCamera() {
mPreview = new CameraPreview(this);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
SettingsFragment.passCamera(mPreview.getCameraInstance());
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
SettingsFragment.setDefault(PreferenceManager.getDefaultSharedPreferences(this));
SettingsFragment.init(PreferenceManager.getDefaultSharedPreferences(this));
}
注意由于mPreview
的处理分离,将mPreview
提升为成员变量;原先添加的final CameraPreview
删去,变更为对成员变量的赋值。
重载onResume
为了安全起见,也对onPause()
重载,在MainActivity
中加入
public void onPause() {
super.onPause();
mPreview = null;
}
public void onResume() {
super.onResume();
if (mPreview == null) {
initCamera();
}
}
当APP切换到后台时触发onPause()
,此时mPreview
被销毁,将其赋值null
;当从后台切换回来时,重新对mPreview初始化。
运行一下试试
现在把APP切换到后台再切换回来就不会出现黑屏的问题了。
添加预览
这里的预览不是相机预览了,是拍到的照片和视频的预览。目前常见的相机APP在拍照后,左下角或某个角落的小框就会马上显示新拍到的照片,点击这个小框就会全屏显示这个照片,现在我们就来实现这个功能。
预览框实际是ImageView
,通过向ImageView
指定图片或图片的URI,就可以在UI上显示这个图片了。那么对于拍到的视频怎么显示呢?这里我们可以获取到视频的预览图,将这个预览图指定给ImageView
就好了。
添加预览框
修改activity_main.xml
,在RelativeLayout
内的LinearLayout
之下加入
<ImageView
android:id="@+id/media_preview"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="#000" />
就会在窗口的右下角出现一个黑框,效果如下:
加入预览
之前提到拍照时异步操作,因为这个原因,我们将ImageView
的操作交给CameraPreview
处理。
修改CameraPreview
我们需要修改takePicture()
和stopRecording()
这两个方法,将其参数加上ImageView
,并在其内部进行处理。
修改后的takePicture()
如下
public void takePicture(final ImageView view) {
mCamera.takePicture(null, null, new Camera.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");
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
view.setImageURI(outputMediaFileUri);
camera.startPreview();
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found: " + e.getMessage());
} catch (IOException e) {
Log.d(TAG, "Error accessing file: " + e.getMessage());
}
}
});
}
相比于之前,加入了参数final ImageView view
,以及在文件写入完成后加入了
view.setImageURI(outputMediaFileUri);
即指定ImageView
的URI为刚才生成的照片文件。
修改后的stopRecording()
如下:
public void stopRecording(final ImageView view) {
if (mMediaRecorder != null) {
mMediaRecorder.stop();
Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(outputMediaFileUri.getPath(), MediaStore.Video.Thumbnails.MINI_KIND);
view.setImageBitmap(thumbnail);
}
releaseMediaRecorder();
}
相比于之前,加入了参数final ImageView view
,以及在录像完成后加入了
Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(outputMediaFileUri.getPath(), MediaStore.Video.Thumbnails.MINI_KIND);
view.setImageBitmap(thumbnail);
第一句是根据指定的视频路径,生成了一张视频预览图;第二句则将这个图片交给ImageView
显示。
修改MainActivity
我们还需要在MainActivity中找到ImageView
,并在调用上述两个方法时添加参数。
在onCreate()
中加入
final ImageView mediaPreview = (ImageView) findViewById(R.id.media_preview);
修改
mPreview.takePicture();
为
mPreview.takePicture(mediaPreview);
修改
mPreview.stopRecording();
为
mPreview.stopRecording(mediaPreview);
运行一下试试
现在每拍到新照片或视频,预览框都会同步更新了。如下
点击预览框全屏显示
只是在预览框中显示拍到的照片或视频预览还是不够的,我们还想要点击这个预览框时能够全屏显示照片,或播放视频,下面我们就来实现这一功能。
先说思路,监听ImageView
的点击事件,当点击时,通过Intent
创建并显示一个新的Activity
,同时MainActivity
将需要显示的照片或视频的URI和MIME交给新的Activity
,而这个新的Activity
则负责显示照片和播放视频。在需要时,用户点击后退,回到MainActivity
。
修改MainActivity
在onCreate()
的最后加入
mediaPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, ShowPhotoVideo.class);
intent.setDataAndType(mPreview.getOutputMediaFileUri(), mPreview.getOutputMediaFileType());
startActivityForResult(intent, 0);
}
});
即对ImageView
的点击事件监听,点击时创建一个新的Intent
,新的Activity
是ShowPhotoVideo
(稍后创建);然后通过Data
和Type
向这个Intent
传递拍到的照片或视频的URI和MIME(也可以用setExtra()
实现,但这个方法更直观准确);最后启动这个Activity
,并加入回退栈,使得点击后退时能够返回到MainActivity
(上一步解决的后台返回黑屏问题也在这个得到应用)。
创建ShowPhotoVideo
这个类继承自Activity
,功能很简单,根据Type
判断使用ImageView
显示照片,或使用VideoView
显示视频,然后向ImageView
或VideoView
传递Data
包含的URI信息。
创建ShowPhotoVideo类
文件内容如下:
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.MediaController;
import android.widget.RelativeLayout;
import android.widget.VideoView;
public class ShowPhotoVideo extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RelativeLayout relativeLayout = new RelativeLayout(this);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
Uri uri = getIntent().getData();
if (getIntent().getType().equals("image/*")) {
ImageView view = new ImageView(this);
view.setImageURI(uri);
view.setLayoutParams(layoutParams);
relativeLayout.addView(view);
} else {
MediaController mc = new MediaController(this);
VideoView view = new VideoView(this);
mc.setAnchorView(view);
mc.setMediaPlayer(view);
view.setMediaController(mc);
view.setVideoURI(uri);
view.start();
view.setLayoutParams(layoutParams);
relativeLayout.addView(view);
}
setContentView(relativeLayout, layoutParams);
}
}
与其他Activity
不同的是,ShowPhotoVideo
没有layout文件,其布局等在onCreate()
中用代码生成。首先新建一个RelativeLayout
即relativeLayout
;再创建一个layout参数layoutParams
,其参数设置长和宽均为MATCH_PARENT
,随后像参数加入新规则CENTER_IN_PARENT
,即居中显示。新建了ImageView
或VideoView
后,将view
指定layoutParams
参数,并将view
加入到relativeLayout
中。最后将本Activity
的布局指定为刚才创建的relativeLayout,并设置layout参数为layoutParams
。
对于ImageView
和VideoView
的选择和操作。根据Type
决定实例化ImageView
或VideoView
,若实例化ImageView
,则指定其URI为Data
参数,指定layout参数后加入到layout中;若实例化VideoView
,则同时还会实例化MediaController
用来对播放视频进行控制,在对MediaController
进行初始化操作后,将VideoView
指定layout参数后加入到layout中。
再说下工作过程。MainActivity
创建ShowPhotoVideo
的Intent
,并传递照片或视频的URI和MIME,然后切换到新的Activity
即ShowPhotoVideo
;ShowPhotoVideo
创建时触发onCreate()
,通过MIME实例化ImageView
或VideoView
,用代码生成布局并将ImageView
或VideoView
加入到布局,最后将布局应用到ShowPhotoVideo
,完成整个工作过程。
修改AndroidManifest
Intent
操作时需要在AndroidManifest.xml中提前声明Activity
,这里我们就是要将ShowPhotoVideo
加入到声明中。
在application
中添加新的Activity
,如下
<activity android:name=".ShowPhotoVideo" />
运行一下试试
现在运行APP在拍照或录像后,点击预览框就能够全屏显示照片或播放视频了。比如点击视频预览图后
一点唠叨
直到本文,我们就实现了一个基本能用的相机APP了,为了代码的简洁和逻辑的清晰考虑,没有做太多特殊情况的处理,所以还会有各种各样的小bug,你可以自己进行优化。另外还会有一些功能上的增强和细节上的优化,会在随后的文章中介绍。
DEMO
本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-EnableCapture。
参考
- Camera | Android Developers
- MediaRecorder | Android Developers
- SharedPreferences | Android Developers
- Managing the Activity Lifecycle | Android Developers
- ImageView | Android Developers
- VideoView | Android Developers
- ViewGroup.LayoutParams | Android Developers
- android - Camera preview screen black when activity resumes - Stack Overflow
- Android: Is it possible to display video thumbnails? - Stack Overflow
- preferences - How do I get the SharedPreferences from a PreferenceActivity in Android? - Stack Overflow
- VideoView shows black after video capture from Android camera - Stack Overflow
- Lesson 16. Creating layout programmatically. LayoutParams
- How to resume android camera preview after onPictureTaken function? - Stack Overflow
您好,请问可以录像的时候可以不显示录像界面嘛?隐藏录像的效果,最近有个服务类项目会用到
您好我是一个安卓初学者,您的相机教程实在是太好了。可是我现在点击录像按钮程序就闪退了,不知道有哪些可能的原因去排查,拍照按钮没有问题照片也被成功保存了,能否告诉我一些可能的闪退原因
哈哈我自己发现了问题。好像是新版本的安卓录像和录音等权限只在安装时申请还不够,打开的时候还要申请一次才行。于是可以手动去设置里为应用打开麦克风和相机权限就可以了!
博主你好,我现在跑你的demo按任何键都会闪退,可以给点建议吗
拍照功能按下后应该是拍了照片,但就停在那,然后按其他键就会闪退。
CameraPreview这个类第133行和61行 报错了java.lang.NumberFormatException: For input string: ""
这是因为没有设置录制视频的分辨率参数,建议确认以下APP拿到了相机权限~
新手小白 为什么在CameraPreview 里面加您附的代码的时候好多词Android Studio都不认识不管是给照片文件名还是拍照的代码
你可以直接去下载DEMO的源码哟,因为Android项目文件众多且相互依赖,只看单一的代码就会出现这种问题的哟~
拍照时会有一点点轻微的小抖动啊?
似乎会有一些的,有种解决方法是拍照的时候加点动画,像快门的动画那样~
仔细看了下,但是没发现出现抖动的具体原因。