Android相机开发(三): 实现拍照录像和查看

文章正文
发布时间:2024-11-05 01:26
Android相机开发(三): 实现拍照录像和查看

转载自:Penguin

Android Camera Develop: capture photo and video

概述

上篇完成了相机的偏好设置,本篇就要实现相机的核心功能——拍照和录像了。直觉上拍照和录像应该差别不大,但在Android中两者是有很大差别的,录像需要更多的步骤,以及更严格的处理逻辑。本篇还加入了对拍摄到的照片和视频的预览查看功能,就像其他相机APP那样。

生成文件名

因为拍照录像都是对相机的操作,所以这部分绝大多数都是在CameraPreview中添加代码。首先我们添加生成文件名这一功能,我们采用通用的做法,生成的文件名首先标记是照片还是视频,然后是拍摄的时间,比如IMG_20160503_153218.jpg和VID_20160505_080204.mp4。

在CameraPreview中添加

Java

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中添加

Java

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中添加

Java

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,指定相机、音频源、视频源、录制视频参数、输出文件路径以及预览等,关于细节请查看官方文档。

其中的

Java

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中添加

XML

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> 添加按钮

上述只是完成了方法的设计,现在就要在UI上添加按钮,并实际调用啦。

添加按钮

修改activity_main.xml为

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大概像这样

Screenshot Add Button

绑定事件

在MainActivity的onCreate()最后添加

Java

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("停止"); } } } });

并在

Java

CameraPreview mPreview = new CameraPreview(this);

前加final修饰符,即

Java

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()中的

Java

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

替换为

Java

initCamera();

在MainActivity中添加

Java

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中加入

Java

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之下加入

XML

<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" />

就会在窗口的右下角出现一个黑框,效果如下:

Screenshot Add ImageView

加入预览

之前提到拍照时异步操作,因为这个原因,我们将ImageView的操作交给CameraPreview处理。

修改CameraPreview

我们需要修改takePicture()和stopRecording()这两个方法,将其参数加上ImageView,并在其内部进行处理。
修改后的takePicture()如下

Java

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,以及在文件写入完成后加入了

Java

view.setImageURI(outputMediaFileUri);

即指定ImageView的URI为刚才生成的照片文件。

修改后的stopRecording()如下:

Java

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,以及在录像完成后加入了

Java

Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(outputMediaFileUri.getPath(), MediaStore.Video.Thumbnails.MINI_KIND); view.setImageBitmap(thumbnail);

第一句是根据指定的视频路径,生成了一张视频预览图;第二句则将这个图片交给ImageView显示。

修改MainActivity

我们还需要在MainActivity中找到ImageView,并在调用上述两个方法时添加参数。

在onCreate()中加入

Java

final ImageView mediaPreview = (ImageView) findViewById(R.id.media_preview);

修改

Java

mPreview.takePicture();

Java

mPreview.takePicture(mediaPreview);

修改

Java

mPreview.stopRecording();

Java

mPreview.stopRecording(mediaPreview); 运行一下试试

现在每拍到新照片或视频,预览框都会同步更新了。如下

Screenshot Enable ImageView

点击预览框全屏显示

只是在预览框中显示拍到的照片或视频预览还是不够的,我们还想要点击这个预览框时能够全屏显示照片,或播放视频,下面我们就来实现这一功能。

先说思路,监听ImageView的点击事件,当点击时,通过Intent创建并显示一个新的Activity,同时MainActivity将需要显示的照片或视频的URI和MIME交给新的Activity,而这个新的Activity则负责显示照片和播放视频。在需要时,用户点击后退,回到MainActivity。

修改MainActivity

在onCreate()的最后加入

Java

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类

File List ShowPhotoVideo

文件内容如下:

Java

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,如下

XML

<activity android:name=".ShowPhotoVideo" /> 运行一下试试

现在运行APP在拍照或录像后,点击预览框就能够全屏显示照片或播放视频了。比如点击视频预览图后

Screenshot Show VideoView.png

一点唠叨

直到本文,我们就实现了一个基本能用的相机APP了,为了代码的简洁和逻辑的清晰考虑,没有做太多特殊情况的处理,所以还会有各种各样的小bug,你可以自己进行优化。另外还会有一些功能上的增强和细节上的优化,会在随后的文章中介绍。

DEMO

本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-EnableCapture。