作为开博的第一篇文章,正好最近在学习Android视频录制功能,所以决定趁热记录下来。
关于Android视频录制功能的实现流程以及相关API的介绍网上有很多,这里就不再赘述。在学习的过程中主要参考了 Vitamio公司的VCamera SDK 项目,因为该项目在Java层全开源,所以在调用Android系统的相机进行录制方面记录下一些学习体会。
具体关于VCamera项目,请移步VCamera
首先是定义视频录制接口,接口很简单只包含两个方法,因为是调用系统的API实现录制,所以在方法上只用考虑到start和stop方法,出于对功能的拓展性上(比如自定义相机功能),接口的方法还可以更丰富。
public interface IMediaRecorder {
/**
* 开始录制
* @return 录制失败返回null
*/
public MediaObject startRecord();
/**
* 停止录制
*/
public void stopRecord();
}
接着是bean:
/** 视频最大时长,默认10秒 */
private int mMaxDuration;
/** 视频目录 */
private String mOutputDirectory;
/** 对象文件 */
private String mOutputObjectPath;
/** 视频码率 */
private int mVideoBitrate;
/** 最终视频输出路径 */
private String mOutputVideoPath;
/** 最终视频截图输出路径 */
private String mOutputVideoThumbPath;
/** 文件夹及文件名 */
private String mKey;
/** 开始时间 */
private long mStartTime;
/** 结束时间 */
private long mEndTime;
/** 视频移除标志位 */
private boolean mRemove;
/** 两个构造方法 */
public MediaObject(String key, String path) {
this(key, path, DEFAULT_VIDEO_BITRATE);
}
public MediaObject(String key, String path, int videoBitrate) {
this.mKey = key;
this.mOutputDirectory = path;
this.mVideoBitrate = videoBitrate;
this.mOutputObjectPath = mOutputDirectory + File.separator + mKey + ".obj";
this.mOutputVideoPath = mOutputDirectory + File.separator + mKey +".mp4";
this.mOutputVideoThumbPath = mOutputDirectory + File.separator + mKey + ".jpg";
this.mMaxDuration = DEFAULT_MAX_DURATION;
}
在视频录制过程中分为两个部分,第一个就是我们在界面上看到的摄像头传来的预览画面Preview
,另一个则是视频录制时使用的”录像机“Recorder
,同样为了提高拓展性,在功能实现上我们先将预览画面提出来实现它。因为也许你有许多不同的Recorder
,但是预览的方式只有那么一个。
那么抽象出一个录像父类非常重要,它实现了一些都需要用到的方法,其中当然包括我们的Preview
。
public abstract class MediaRecorderBase implements Callback, PreviewCallback, IMediaRecorder {...}
非常直观哈,Callback
接口是SurfaceHolder
的回调接口,我们所看到的预览画面就是呈现在SurfaceView
上的,PreviewCallback
预览画面的回调接口,至于我们先前定义的IMediaRecorder
就留给后人去实现吧。
回到主线预览画面上,关于SurfaceView
这里不做介绍了,我们要做的是先把它的SurfaceHolder
拿过来
public void setSurfaceHolder(SurfaceHolder sh) {
if (sh != null) {
sh.addCallback(this);
if (!DeviceUtils.hasHoneycomb()) {
sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
}
}
然后就可以掌控这一切了。。
public void prepare() {
mPrepared = true;
if (mSurfaceCreated)
startPreview();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
this.mSurfaceHolder = holder;
this.mSurfaceCreated = true;
if (mPrepared && !mStartPreview)
startPreview();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
this.mSurfaceHolder = holder;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
release();
}
startPreview()
就是我们的预览方法:
/** 开始预览 */
public void startPreview() {
if (mStartPreview || mSurfaceHolder == null || !mPrepared)
return;
else
mStartPreview = true;
try {
//打开镜头
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK)
camera = Camera.open();
else
camera = Camera.open(mCameraId);
try {
//为preview设置SurfaceHolder
camera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
if (mOnErrorListener != null) {
mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_SET_PREVIEW_DISPLAY, 0);
}
Log.e(" ", "setPreviewDisplay fail " + e.getMessage());
}
//获取摄像头参数
Parameters parameters = camera.getParameters();
prepareCameraParaments(parameters);
setPreviewCallback(parameters);
camera.setParameters(parameters);
camera.startPreview();
if (mOnPreparedListener != null)
mOnPreparedListener.onPrepared();
} catch (Exception e) {
e.printStackTrace();
if (mOnErrorListener != null) {
mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_PREVIEW, 0);
}
Log.e(" ", "startPreview fail :" + e.getMessage());
}
}
这里有几个方法依次说一下,首先是protected void prepareCameraParaments(Parameters parameters)
调用它会对相机进行一些参数上的处理,里面基本包括:
List<int[]> supportedPreviewFpsRange = parameters.getSupportedPreviewFpsRange();
这里返回一个数组列表,里面包含了设备所支持预览画面的FPS值,每个数组含有两个值,第一个值是最小FPS,另一个是最大FPS值,这样就构成了一个区间,可以检测你所期望的FPS值在不在设备支持中。那么:
parameters.setPreviewFrameRate(mFrameRate);
接着,
List supportedPreviewSizes = parameters.getSupportedPreviewSizes();
这个返回了设备支持的预览画面的尺寸,Size有很多,但是重点在比例,16:9、4:3 、11:9等等,比例非常关键,它直接关系到后来我们的SurfaceView
的尺寸,选一个你想要的把它:
parameters.setPreviewSize(customSize.width, customSize.height);
我们知道小视频录制的时候是竖屏的,所以把它竖过来是必须的:
camera.setDisplayOrientation(90);
顺时针旋转90度,之所以这么做,API文档上有介绍,大意就是camera sensor没有转,但是我们可以把画面转过来。
这样就可以了,当然如果想设置更多,当然没问题,这里的Parameters
设置的参数实际上都是以Map键值对的形式传入的,具体的源码也有大神解读。那么,比如加一个防抖功能(前提是你的设备支持):
if ("true".equals(parameters.get("video-stabilization-supported")))
parameters.set("video-stabilization", "true");
再来看protected void setPreviewCallback(Parameters parameters)
方法,作用呢就是给Camera对象设置我们一开始实现的那个PreviewCallback
接口:
protected void setPreviewCallback(Parameters parameters) {
Camera.Size size = parameters.getPreviewSize();
if (size != null) {
PixelFormat pf = new PixelFormat();
PixelFormat.getPixelFormatInfo(parameters.getPreviewFormat(), pf);
int buffSize = size.width * size.height * pf.bitsPerPixel / 8;
try {
camera.addCallbackBuffer(new byte[buffSize]);
camera.addCallbackBuffer(new byte[buffSize]);
camera.addCallbackBuffer(new byte[buffSize]);
camera.setPreviewCallbackWithBuffer(this);
} catch (OutOfMemoryError e) {
Log.e(" ", "startPreview...setPreviewCallback...", e);
}
} else {
camera.setPreviewCallback(this);
}
}
之前我们为parameter对象设置过previewSize参数,所以明显的这里用到了Camera.setPreviewCallbackWithBuffer(this)
方法,实际上就是在视频预览的回调中加入缓冲区Buffer,怎么做文档上说的很清楚,调用Camera.addCallbackBuffer(new byte[buffSize]);
而且Applications can add one or more buffers to the queue.
可以加入多个到队列中,再看When a preview frame arrives and there is still at least one available buffer, the buffer will be used and removed from the queue.
好吧用过了还要抛弃掉。。另外buffer大小,文档中也告诉了怎么去计算,就是呈现出来的每帧画面中的每个像素所占的bits的和除以8。那么被无情抛弃的buffer怎么办?队空了没buffer用了,这一帧的画面也会被系统舍弃。实际上,我们有回调PreviewCallback
:
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
camera.addCallbackBuffer(data);
}
再把它加回去。(以上全是个人理解,有偏差请见谅)
当设置完参数就可以开启预览了camera.startPreview()
。
停止预览很简单:Camera.stopPreview()
,然后将相机回调解绑,资源释放掉,holder资源释放,定义的标志位重置就好。毕竟这些资源太重了。
回到录像部分,在这里需要做的首先是继承之前的MediaRecorderBase
类并且实现未实现的IMediaRecorder
接口方法。
public class MediaRecorderSystem extends MediaRecorderBase implements OnErrorListener {
@Override
public MediaObject startRecord() {...}
@Override
public void stopRecord() {...}
...
}
startRecord()
会返回一个MediaObject对象,所以我们暴露出一个方法可以得到它
/** 拍摄存储对象 */
private MediaObject mMediaObject;
public MediaObject setOutputDirectory(String key, String path) {
if (StringUtils.isNotEmpty(path)) {
File f = new File(path);
if (f != null) {
if (f.exists()) {
//已经存在,删除
FileUtils.deleteFile(f);
}
if (f.mkdirs()) {
mMediaObject = new MediaObject(key, path, mVideoBitrate);
}
}
}
return mMediaObject;
}
之后在startRecord()
中实现视频录制,直接上代码:
if (mMediaRecorder == null) {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setOnErrorListener(this);
} else {
mMediaRecorder.reset();
}
// Step 1: Unlock and set camera to MediaRecorder
camera.unlock();
mMediaRecorder.setCamera(camera);
mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
// Step 2: Set sources
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);//before setOutputFormat()
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//before setOutputFormat()
//设置视频输出的格式和编码
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
CamcorderProfile mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_TIME_LAPSE_CIF);
//after setVideoSource(),after setOutFormat()
mMediaRecorder.setVideoSize(mProfile.videoFrameWidth, mProfile.videoFrameHeight);
mMediaRecorder.setAudioEncodingBitRate(44100);
if (mProfile.videoBitRate > 2 * 1024 * 1024)
mMediaRecorder.setVideoEncodingBitRate(2 * 1024 * 1024);
else
mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate);
//after setVideoSource(),after setOutFormat();
mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate);
//after setOutputFormat()
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//after setOutputFormat()
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
// Step 3: Set output file
mMediaRecorder.setOutputFile(mMediaObject.getOutputVideoPath());
// Step 4: start and return
mMediaRecorder.prepare();
mMediaRecorder.start();
mMediaObject.setStartTime(System.currentTimeMillis());
mRecording = true;
return mMediaObject;
上面的一系列是调用系统API录制视频的基本流程,也可以去参考开发手册的讲解。
在stopRecord()
方法中,把录像停下来:
...
mMediaRecorder.setOnErrorListener(null);
mMediaRecorder.setPreviewDisplay(null);
...
mMediaRecorder.stop();
...
camera.lock();
重写父类的release()
方法,释放掉MediaRecorder资源:
...
super.release();
mMediaRecorder.release();
...
另外,在OnErrorListener
监听中捕获到异常并且mMediaRecorder.reset()
。
视频录制功能基本上就完成了,但是我们需求的功能却不止这些,那么就再加点料。回到MediaRecorderBase
类,实现需要添加的功能分别是:自动、手动对焦,变焦,摄像头切换以及闪光灯的开关。
这些功能需求很简单:
对焦:当我们一开启预览时会自动调节焦距,当获取到点击时则进入手动对焦;
变焦:双击预览区域zoom+,再次双击zoom-回到初始状态;
摄像头切换:默认开启后置摄像头,点击按钮切换至前置摄像头,再次点击切换到后置;
闪光灯:默认关闭,点击按钮打开闪光灯保持常亮,再次点击关闭;
首先是对焦,这里用到持续对焦这条参数
...
//获取到设备支持的对焦模式
List focusModes = parameters.getSupportedFocusModes();
if(!CollectionUtils.isEmpty(focusModes)){
if(focusModes.list.contains("continuous-video"))
parameters.setFocusMode("continuous-video");
}
这里以”continuous-video”参数为例,因为不同设备存在差异,实际上是需要判断focusModes
列表中支持的类型,类型有:“continuous-video”、“continuous-picture”和“auto”当然你也可以直接使用Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
。
手动对焦方法如下:
public boolean manualFocus(AutoFocusCallback cb, List focusAreas) {
//判断系统是否是4.0以上的版本
if (camera != null && focusAreas != null && DeviceUtils.hasICS()) {
try {
camera.cancelAutoFocus();
Parameters parameters = camera.getParameters();
if(parameters != null){
// getMaxNumFocusAreas检测设备是否支持
if (parameters.getMaxNumFocusAreas() > 0) {
parameters.setFocusAreas(focusAreas);
}
// getMaxNumMeteringAreas检测设备是否支持
if (parameters.getMaxNumMeteringAreas() > 0)
parameters.setMeteringAreas(focusAreas);
parameters.setFocusMode("macro");
camera.setParameters(parameters);
camera.autoFocus(cb);
return true;
}
} catch (Exception e) {
if (mOnErrorListener != null) {
mOnErrorListener.onVideoError(
MEDIA_ERROR_CAMERA_AUTO_FOCUS, 0);
}
if (e != null)
Log.e(" ", "autoFocus", e);
}
}
return false;
}
首先传入一个AutoFocusCallback
回调,它会告诉我们对焦是否OK和当前Camera的对象,List
是需要对焦的区域。流程很简单,先取消自动对焦,然后向相机参数中设置好对焦区域和对焦模式,再调用相机的Camera.autoFocus(...)
方法,这样就会对指定的区域进行对焦。
变焦:
public void setZoom(int zoomValue) {
Parameters parameters = camera.getParameters();
if (parameters.isZoomSupported()) {
final int MAX = parameters.getMaxZoom();
if (MAX == 0)
return;
if (zoomValue > MAX)
zoomValue = MAX;
parameters.setZoom(zoomValue); // value zoom value. The valid range
// is 0 to getMaxZoom.
camera.setParameters(parameters);
}
else
return;
}
传入一个焦距值,然后判断相机是否支持变焦,这里会出一个bug,对于有的设备,isZoomSupported()
返回了true,但是仍然无法变焦,则需要判断下MaxZoom
的值,如果为0,则仍然不支持变焦,因为是固定传入焦距,所以直接设置就好了。
摄像头切换我们在startPreview()
方法中有这么一段:
...
try {
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK)
camera = Camera.open();
else
camera = Camera.open(mCameraId);
try {
camera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
if (mOnErrorListener != null) {
mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_SET_PREVIEW_DISPLAY, 0);
}
Log.e(" ", "setPreviewDisplay fail " + e.getMessage());
}
...
通过对mCameraId
的改变,先调用stopPreview()
再调用startPreview()
就可以完成了。
最后,对于闪光的切换就不必多说了
public void toggleFlashMode() {
Parameters parameters = camera.getParameters();
if (parameters != null) {
try {
final String mode = parameters.getFlashMode();
if (TextUtils.isEmpty(mode) || Camera.Parameters.FLASH_MODE_OFF.equals(mode))
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
else
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(parameters);
} catch (Exception e) {
Log.e(" ", "toggleFlashMode", e);
}
}
}
那么,录像功能上基本上就OK了,现在要把它用起来。
先上xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<RelativeLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="49dip"
android:background="@color/black"
android:gravity="center_vertical" >
<ImageView
android:id="@+id/title_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dip"
android:padding="10dip"
android:src="@drawable/arrow_left" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="48dip"
android:layout_alignParentRight="true"
android:gravity="right|center_vertical"
android:orientation="horizontal" >
<CheckBox
android:id="@+id/record_camera_led"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/record_camera_flash_led_selector"
android:button="@null"
android:textColor="@color/white" />
<CheckBox
android:id="@+id/record_camera_switcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="10dp"
android:background="@drawable/record_camera_switch_selector"
android:button="@null" />
LinearLayout>
RelativeLayout>
<RelativeLayout
android:id="@+id/camera_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/title_layout" >
<SurfaceView
android:id="@+id/record_preview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/record_focusing"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:src="@drawable/video_focus"
android:visibility="gone" />
<TextView android:id="@+id/record_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="12sp"
android:background="@drawable/recorder_tips"/>
<com.example.activity.widget.movie.view.ProgressView
android:id="@+id/record_progress"
android:layout_width="match_parent"
android:layout_height="3dp"/>
RelativeLayout>
<RelativeLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/title_layout"
android:background="@color/black" >
<ImageView
android:id="@+id/record_controller"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_centerInParent="true"
android:src="@drawable/bg_movie_add_shoot" />
<TextView
android:layout_width="75dp"
android:layout_height="75dp"
android:layout_centerInParent="true"
android:text="按住拍"
android:gravity="center"
android:textColor="#FF45C01A"
android:textSize="20sp"/>
RelativeLayout>
RelativeLayout>
不出意外的不太像。。蛤蛤
Activity:
在onCreate
中处理View:
private void initViews() {
...
mWindowWidth = DeviceUtils.getScreenWidth(this);
int height = (int) (mWindowWidth * MediaRecorderBase.PREVIEW_RATIO);
((RelativeLayout.LayoutParams)mBottomLayout.getLayoutParams()).topMargin = mWindowWidth;
((RelativeLayout.LayoutParams)mRecordTipView.getLayoutParams()).topMargin = mWindowWidth - DisplayUtil.dip2px(this,(40));
((RelativeLayout.LayoutParams)mProgressView.getLayoutParams()).topMargin = mWindowWidth - DisplayUtil.dip2px(this, 3);
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mSurfaceView
.getLayoutParams();
lp.width = mWindowWidth;
lp.height = height;
mSurfaceView.setLayoutParams(lp);
mRecordTipView.setVisibility(View.GONE);
}
这里我们对SurfaceView的的宽度和高度做了处理,而不是在xml中,之前的PreView中设置过camera.setDisplayOrientation(90)
画面旋转的处理(下图),所以SurfaceView的尺寸也应做出相应的调整
在onResume()
中对MediaRecorder对象做处理:
...
private MediaRecorderBase mMediaRecorder;
private MediaObject mMediaObject;
@Override
protected void onResume() {
super.onResume();
if (mMediaRecorder == null) {
initMediaRecorder();
} else {
mRecordLed.setChecked(false);
mMediaRecorder.prepare();
}
}
private void initMediaRecorder() {
mMediaRecorder = new MediaRecorderSystem();
mMediaRecorder.setOnErrorListener(this);
File f = new File(CACHE_PATH);
if (!FileUtils.checkFile(f)) {
f.mkdirs();
}
String key = String.valueOf(System.currentTimeMillis());
mMediaObject = mMediaRecorder.setOutputDirectory(key,
CACHE_PATH + key);
mMediaObjList.add(mMediaObject);
mMediaRecorder.setSurfaceHolder(mSurfaceView.getHolder());
mMediaRecorder.prepare();
}
...
这么做确保Activity从后台切换回来显示预览依旧正常;
Activity销毁时,处理等待删除缓存目录以及释放资源:
@Override
protected void onDestroy() {
//activity 销毁时删除废弃的缓存目录
if (!CollectionUtils.isEmpty(mMediaObjList)) {
for (MediaObject obj : mMediaObjList) {
if(obj != null && obj.isRemove())
FileUtils.deleteDir(obj.getOutputDirectory());
}
}
mMediaRecorder.release();
super.onDestroy();
}
录制按钮的Touch事件:
private View.OnTouchListener mOnVideoControllerTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mMediaRecorder == null) {
return false;
}
if (mMediaObject == null){
String key = String.valueOf(System.currentTimeMillis());
mMediaObject = mMediaRecorder.setOutputDirectory(key,
CACHE_PATH + key);
mMediaObjList.add(mMediaObject);
}
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
// 判断是否已经超时
if (mMediaObject.getDuration() >= RECORD_TIME_MAX) {
return true;
}
mMediaObject.setStartTime(System.currentTimeMillis());
startRecord();
}
if(action == MotionEvent.ACTION_MOVE){
if(event.getY() < 0)
mAtRemove = true;
else
mAtRemove = false;
changeTip();
mProgressView.setRemove(mAtRemove);
mMediaObject.setRemove(mAtRemove);
mHandler.sendEmptyMessage(HANDLE_INVALIDATE_PROGRESS);
}
if(action == MotionEvent.ACTION_UP){
//停止录制
if (mPressedStatus){
mRecordTipView.setVisibility(View.GONE);
mCameraSwitch.setVisibility(View.VISIBLE);
stopRecord();
}
}
return true;
}
};
按下去就开始录制,向上滑提示放开取消,下滑回来继续录,直到放开停止录制。下面是两个控制录制的方法:
/**
* 开始录制
*/
private void startRecord() {
if (mMediaRecorder != null) {
mMediaRecorder.startRecord();
// 使用系统录制环境,不能在中途切换前后摄像头,否则有问题
if (mMediaRecorder instanceof MediaRecorderSystem) {
mCameraSwitch.setVisibility(View.GONE);
}
}
mPressedStatus = true;
mRecordController.setImageResource(R.drawable.bg_movie_add_shoot);
mCameraSwitch.setEnabled(false);
mRecordLed.setEnabled(false);
mTimeCount = 0;// 时间计数器重新赋值
mTimer = new Timer();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
mTimeCount++;
mProgressView.setProgress(mTimeCount * 100);// 设置进度条
mHandler.sendEmptyMessage(HANDLE_INVALIDATE_PROGRESS);
if (mTimeCount >= RECORD_TIME_MAX / 100) {// 达到指定时间
this.cancel();
mHandler.removeMessages(HANDLE_INVALIDATE_PROGRESS);
}
}
}, 0, 100);
}
/**
* 停止录制
*/
private void stopRecord() {
resetTimer();
mPressedStatus = false;
if (mMediaRecorder != null && mMediaObject != null) {
long endTime = System.currentTimeMillis();
mMediaObject.setEndTime(endTime);
int duration = (int) mMediaObject.getDuration();
//录制时间小于最小值取消录制并返回
if(duration < RECORD_TIME_MIN || mMediaObject.isRemove()){
mMediaObject.setRemove(true);
mMediaObjList.add(mMediaObject);
if(duration < RECORD_TIME_MIN && !mAtRemove)
Tools.showToast("视频时间太短");
mMediaRecorder.stopRecord();
mCameraSwitch.setEnabled(true);
mRecordLed.setEnabled(true);
mMediaObject = null;
return;
}
mMediaRecorder.stopRecord();
}
mCameraSwitch.setEnabled(true);
mRecordLed.setEnabled(true);
saveObj();
saveThumb();
//到下一个activity
// Intent intent = new Intent();
// intent.putExtra("MediaObj", mMediaObject);
// intent.setClass(this, MoviePreviewActivity.class);
// startActivity(intent);
// finish();
}
/**
* 重置计时器
*/
private void resetTimer(){
if(mTimer != null){
mTimer.cancel();
mTimer.purge();
mProgressView.setProgress(0);
mProgressView.setRemove(false);
mHandler.sendEmptyMessage(HANDLE_INVALIDATE_PROGRESS);
}
}
Timer计时器用于更新进度条。
再看SurfaceView的触摸事件:
private GestureDetector mDetector;
private View.OnTouchListener mOnSurfaveViewTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mMediaRecorder == null || !mCreated) {
return false;
}
return mDetector.onTouchEvent(event);
}
};
...
class ZoomGestureListener extends SimpleOnGestureListener{
@Override
public boolean onDoubleTap(MotionEvent e) {
if (mMediaRecorder == null || !mCreated) {
return false;
}
if(!mZoomIn){
mMediaRecorder.setZoom(8); //zoom in..
mZoomIn = true;
}else{
mMediaRecorder.setZoom(0); //zoom out..
mZoomIn = false;
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
checkCameraFocus(e);
return true;
}
}
用GestureDetector 对单击和双击事件进行捕获处理。checkCameraFocus(...)
方法就是用来手动对焦的:
private void checkCameraFocus(MotionEvent event) {
float x = event.getX();
float y = event.getY();
float touchMajor = event.getTouchMajor();
float touchMinor = event.getTouchMinor();
//触摸范围
Rect touchRect = new Rect((int) (x - touchMajor / 2),
(int) (y - touchMinor / 2), (int) (x + touchMajor / 2),
(int) (y + touchMinor / 2));
//坐标转换为focusArea范围
Rect focusRect = new Rect();
focusRect.set(touchRect.left * 2000 / mSurfaceView.getWidth() - 1000,
touchRect.top * 2000 / mSurfaceView.getHeight() - 1000,
touchRect.right * 2000 / mSurfaceView.getWidth() - 1000,
touchRect.bottom * 2000 / mSurfaceView.getHeight() - 1000);
if (focusRect.left >= focusRect.right
|| focusRect.top >= focusRect.bottom)
return;
ArrayList focusAreas = new ArrayList();
focusAreas.add(new Camera.Area(focusRect, 1000));
if (!mMediaRecorder.manualFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
// if (success) {
mFocusImage.setVisibility(View.GONE);
System.out.println("onAutoFocus previewsize..width = " + camera.getParameters().getPreviewSize().width
+ "\nheight = " + camera.getParameters().getPreviewSize().height);
// }
}
}, focusAreas)) {
mFocusImage.setVisibility(View.GONE);
}
int focusWidth = mFocusImage.getWidth();
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mFocusImage
.getLayoutParams();
int left = touchRect.left - (focusWidth / 2);
int top = touchRect.top - (focusWidth / 2);
if (left < 0)
left = 0;
else if (left >= mWindowWidth)
left = mWindowWidth - focusWidth;
if (top > mSurfaceView.getHeight())
top = mSurfaceView.getHeight() - focusWidth;
lp.leftMargin = left;
lp.topMargin = top;
mFocusImage.setLayoutParams(lp);
mFocusImage.setVisibility(View.VISIBLE);
mFocusImage.startAnimation(mFocusAnimation);
mHandler.sendEmptyMessageDelayed(HANDLE_HIDE_RECORD_FOCUS, 3500);// 最多3.5秒也要消失
}
这里的focusRect
是通过touchRect
计算映射过去的,之所以这么做,先看下图
简单来说Camera.Area对象的Rect字段是描述了一个矩形区域在一个2000 x 2000个单元格组成的区域中的映射位置。坐标-1000, -1000代表了top, left,并且坐标1000, 1000代表了bottom, right。并且即使使用Camera.setDisplayOrientation()旋转预览图像也不会改变该坐标系。
关于摄像头切换和闪光灯开关这里就简单说下,无非就是获取点击事件,然后调用之前在MediaRecorderBase
类中封装好的方法即可。
最后差点忘了我们的进度条。。这是一个粗糙的进度条:
public class ProgressView extends View {
/** 进度条 */
private Paint mProgressPaint;
/** 回删 */
private Paint mRemovePaint;
/** 最长时长 */
private int mMax;
/** 进度*/
private int mProgress;
private boolean isRemove;
public ProgressView(Context Context, AttributeSet Attr) {
super(Context, Attr);
init();
}
private void init() {
mProgressPaint = new Paint();
mRemovePaint = new Paint();
setBackgroundColor(getResources().getColor(R.color.transparent));
mProgressPaint.setColor(Color.GREEN);
mProgressPaint.setStyle(Paint.Style.FILL);
mRemovePaint.setColor(getResources().getColor(
R.color.title_back));
mRemovePaint.setStyle(Paint.Style.FILL);;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
final int width = getMeasuredWidth(), height = getMeasuredHeight();
int progressLength = (int) ((mProgress / (mMax * 1.0f)) * (width / 2));
canvas.drawRect(progressLength, 0, width - progressLength, height, isRemove ? mRemovePaint : mProgressPaint);
canvas.restore();
}
public void setMax(int max){
this.mMax = max;
}
public void setProgress(int progress){
this.mProgress = progress;
}
public void setRemove(boolean isRemove){
this.isRemove = isRemove;
}
}
作为第一篇博,难免会啰嗦抓不到重点,其实也是自己在总结的时候没有做很好得精炼,后续在总结UI的实现上,贴了代码没有介绍,是因为发现这边可总结的确实不多,OK就这样。