Android仿微信小视频录制功能
Android仿微信小视频录制功能
作为开博的第一篇文章,正好最近在学习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了,现在要把它用起来。
UI
先上xml:
<?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" > <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就这样。
更多相关文章
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用
- python list.sort()根据多个关键字排序的方法实现
- Android(安卓)源码解析-AsyncTask
- android:绘图
- Android(安卓)Layout布局文件里的android:layout_height等属性为
- Android(安卓)App开发基础篇—四大组件之Activity简介
- android的消息处理机制(图+源码分析)——Looper,Handler,Message
- Android输入法原理和疑云