Android 微信小视频录制功能实现详细介绍
Android微信小视频录制功能
开发之前
这几天接触了一下和视频相关的控件,所以,继之前的微信摇一摇,我想到了来实现一下微信小视频录制的功能,它的功能点比较多,我每天都抽出点时间来写写,说实话,有些东西还是比较费劲,希望大家认真看看,说得不对的地方还请大家在评论中指正.废话不多说,进入正题.
开发环境
最近刚更新的,没更新的小伙伴们抓紧了
- AndroidStudio2.2.2
- JDK1.7
- API24
- Gradle2.2.2
相关知识点
- 视频录制界面SurfaceView的使用
- Camera的使用
- 相机的对焦,变焦
- 视频录制控件MediaRecorder的使用
- 简单自定义View
- GestureDetector(手势检测)的使用
用到的东西真不少,不过别着急,咱们一个一个来.
开始开发
案例分析
大家可以打开自己微信里面的小视频,一块简单的分析一下它的功能点有哪些?
- 基本的视频预览功能
- 长按“按住拍”实现视频的录制
- 录制过程中的进度条从两侧向中间变短
- 当松手或者进度条走到尽头视频停止录制并保存
- 从“按住拍”上滑取消视频的录制
- 双击屏幕变焦放大
根据上述的分析,我们一步一步的完成
搭建布局
布局界面的实现还可以,难度不大
<?xmlversion="1.0"encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/main_tv_tip" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:layout_marginBottom="150dp" android:elevation="1dp" android:text="双击放大" android:textColor="#FFFFFF"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <SurfaceView android:id="@+id/main_surface_view" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3"/> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@color/colorApp" android:orientation="vertical"> <RelativeLayout android:id="@+id/main_press_control" android:layout_width="match_parent" android:layout_height="match_parent"> <com.lulu.weichatsamplevideo.BothWayProgressBar android:id="@+id/main_progress_bar" android:layout_width="match_parent" android:layout_height="2dp" android:background="#000"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="按住拍" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textColor="#00ff00"/> </RelativeLayout> </LinearLayout> </LinearLayout> </FrameLayout>
视频预览的实现
step1:得到SufaceView控件,设置基本属性和相应监听(该控件的创建是异步的,只有在真正”准备”好之后才能调用)
mSurfaceView=(SurfaceView)findViewById(R.id.main_surface_view); //设置屏幕分辨率 mSurfaceHolder.setFixedSize(videoWidth,videoHeight); mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mSurfaceHolder.addCallback(this);
step2:实现接口的方法,surfaceCreated方法中开启视频的预览,在surfaceDestroyed中销毁
//////////////////////////////////////////////
//SurfaceView回调
/////////////////////////////////////////////
@Override
publicvoidsurfaceCreated(SurfaceHolderholder){
mSurfaceHolder=holder;
startPreView(holder);
}
@Override
publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,intheight){
}
@Override
publicvoidsurfaceDestroyed(SurfaceHolderholder){
if(mCamera!=null){
Log.d(TAG,"surfaceDestroyed:");
//停止预览并释放摄像头资源
mCamera.stopPreview();
mCamera.release();
mCamera=null;
}
if(mMediaRecorder!=null){
mMediaRecorder.release();
mMediaRecorder=null;
}
}
step3:实现视频预览的方法
/**
*开启预览
*
*@paramholder
*/
privatevoidstartPreView(SurfaceHolderholder){
Log.d(TAG,"startPreView:");
if(mCamera==null){
mCamera=Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
}
if(mMediaRecorder==null){
mMediaRecorder=newMediaRecorder();
}
if(mCamera!=null){
mCamera.setDisplayOrientation(90);
try{
mCamera.setPreviewDisplay(holder);
Camera.Parametersparameters=mCamera.getParameters();
//实现Camera自动对焦
List<String>focusModes=parameters.getSupportedFocusModes();
if(focusModes!=null){
for(Stringmode:focusModes){
mode.contains("continuous-video");
parameters.setFocusMode("continuous-video");
}
}
mCamera.setParameters(parameters);
mCamera.startPreview();
}catch(IOExceptione){
e.printStackTrace();
}
}
}
Note:上面添加了自动对焦的代码,但是部分手机可能不支持
自定义双向缩减的进度条
有些像我一样的初学者一看到自定义某某View,就觉得比较牛X.其实呢,Google已经替我们写好了很多代码,所以我们用就行了.而且咱们的这个进度条也没啥,不就是一根线,今天咱就来说说.
step1:继承View,完成初始化
privatestaticfinalStringTAG="BothWayProgressBar";
//取消状态为红色bar,反之为绿色bar
privatebooleanisCancel=false;
privateContextmContext;
//正在录制的画笔
privatePaintmRecordPaint;
//上滑取消时的画笔
privatePaintmCancelPaint;
//是否显示
privateintmVisibility;
//当前进度
privateintprogress;
//进度条结束的监听
privateOnProgressEndListenermOnProgressEndListener;
publicBothWayProgressBar(Contextcontext){
super(context,null);
}
publicBothWayProgressBar(Contextcontext,AttributeSetattrs){
super(context,attrs);
mContext=context;
init();
}
privatevoidinit(){
mVisibility=INVISIBLE;
mRecordPaint=newPaint();
mRecordPaint.setColor(Color.GREEN);
mCancelPaint=newPaint();
mCancelPaint.setColor(Color.RED);
}
Note:OnProgressEndListener,主要用于当进度条走到中间了,好通知相机停止录制,接口如下:
publicinterfaceOnProgressEndListener{
voidonProgressEndListener();
}
/**
*当进度条结束后的监听
*@paramonProgressEndListener
*/
publicvoidsetOnProgressEndListener(OnProgressEndListeneronProgressEndListener){
mOnProgressEndListener=onProgressEndListener;
}
step2:设置Setter方法用于通知我们的Progress改变状态
/**
*设置进度
*@paramprogress
*/
publicvoidsetProgress(intprogress){
this.progress=progress;
invalidate();
}
/**
*设置录制状态是否为取消状态
*@paramisCancel
*/
publicvoidsetCancel(booleanisCancel){
this.isCancel=isCancel;
invalidate();
}
/**
*重写是否可见方法
*@paramvisibility
*/
@Override
publicvoidsetVisibility(intvisibility){
mVisibility=visibility;
//重新绘制
invalidate();
}
step3:最重要的一步,画出我们的进度条,使用的就是View中的onDraw(Canvascanvas)方法
@Override
protectedvoidonDraw(Canvascanvas){
super.onDraw(canvas);
if(mVisibility==View.VISIBLE){
intheight=getHeight();
intwidth=getWidth();
intmid=width/2;
//画出进度条
if(progress<mid){
canvas.drawRect(progress,0,width-progress,height,isCancel?mCancelPaint:mRecordPaint);
}else{
if(mOnProgressEndListener!=null){
mOnProgressEndListener.onProgressEndListener();
}
}
}else{
canvas.drawColor(Color.argb(0,0,0,0));
}
}
录制事件的处理
录制中触发的事件包括四个:
- 长按录制
- 抬起保存
- 上滑取消
- 双击放大(变焦)
现在对这4个事件逐个分析:
前三这个事件,我都放在了一个onTouch()回调方法中了
对于第4个,我们待会谈
我们先把onTouch()中局部变量列举一下:
@Override
publicbooleanonTouch(Viewv,MotionEventevent){
booleanret=false;
intaction=event.getAction();
floatey=event.getY();
floatex=event.getX();
//只监听中间的按钮处
intvW=v.getWidth();
intleft=LISTENER_START;
intright=vW-LISTENER_START;
floatdownY=0;
//...
}
长按录制
长按录制我们需要监听ACTION_DOWN事件,使用线程延迟发送Handler来实现进度条的更新
switch(action){
caseMotionEvent.ACTION_DOWN:
if(ex>left&&ex<right){
mProgressBar.setCancel(false);
//显示上滑取消
mTvTip.setVisibility(View.VISIBLE);
mTvTip.setText("↑上滑取消");
//记录按下的Y坐标
downY=ey;
//TODO:2016/10/20开始录制视频,进度条开始走
mProgressBar.setVisibility(View.VISIBLE);
//开始录制
Toast.makeText(this,"开始录制",Toast.LENGTH_SHORT).show();
startRecord();
mProgressThread=newThread(){
@Override
publicvoidrun(){
super.run();
try{
mProgress=0;
isRunning=true;
while(isRunning){
mProgress++;
mHandler.obtainMessage(0).sendToTarget();
Thread.sleep(20);
}
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
};
mProgressThread.start();
ret=true;
}
break;
//...
returntrue;
}
Note:startRecord()这个方法先不说,我们只需要知道执行了它就可以录制了,但是Handler事件还是要说的,它只负责更新进度条的进度
////////////////////////////////////////////////////
//Handler处理
/////////////////////////////////////////////////////
privatestaticclassMyHandlerextendsHandler{
privateWeakReference<MainActivity>mReference;
privateMainActivitymActivity;
publicMyHandler(MainActivityactivity){
mReference=newWeakReference<MainActivity>(activity);
mActivity=mReference.get();
}
@Override
publicvoidhandleMessage(Messagemsg){
switch(msg.what){
case0:
mActivity.mProgressBar.setProgress(mActivity.mProgress);
break;
}
}
}
抬起保存
同样我们这儿需要监听ACTION_UP事件,但是要考虑当用户抬起过快时(录制的时间过短),不需要保存.而且,在这个事件中包含了取消状态的抬起,解释一下:就是当上滑取消时抬起的一瞬间取消录制,大家看代码
caseMotionEvent.ACTION_UP:
if(ex>left&&ex<right){
mTvTip.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
//判断是否为录制结束,或者为成功录制(时间过短)
if(!isCancel){
if(mProgress<50){
//时间太短不保存
stopRecordUnSave();
Toast.makeText(this,"时间太短",Toast.LENGTH_SHORT).show();
break;
}
//停止录制
stopRecordSave();
}else{
//现在是取消状态,不保存
stopRecordUnSave();
isCancel=false;
Toast.makeText(this,"取消录制",Toast.LENGTH_SHORT).show();
mProgressBar.setCancel(false);
}
ret=false;
}
break;
Note:同样的,内部的stopRecordUnSave()和stopRecordSave();大家先不要考虑,我们会在后面介绍,他俩从名字就能看出前者用来停止录制但不保存,后者停止录制并保存
上滑取消
配合上一部分说得抬起取消事件,实现上滑取消
caseMotionEvent.ACTION_MOVE:
if(ex>left&&ex<right){
floatcurrentY=event.getY();
if(downY-currentY>10){
isCancel=true;
mProgressBar.setCancel(true);
}
}
break;
Note:主要原理不难,只要按下并且向上移动一定距离就会触发,当手抬起时视频录制取消
双击放大(变焦)
这个事件比较特殊,使用了Google提供的GestureDetector手势检测来判断双击事件
step1:对SurfaceView进行单独的Touch事件监听,why?因为GestureDetector需要Touch事件的完全托管,如果只给它传部分事件会造成某些事件失效
mDetector=newGestureDetector(this,newZoomGestureListener());
/**
*单独处理mSurfaceView的双击事件
*/
mSurfaceView.setOnTouchListener(newView.OnTouchListener(){
@Override
publicbooleanonTouch(Viewv,MotionEventevent){
mDetector.onTouchEvent(event);
returntrue;
}
});
step2:重写GestureDetector.SimpleOnGestureListener,实现双击事件
///////////////////////////////////////////////////////////////////////////
//变焦手势处理类
///////////////////////////////////////////////////////////////////////////
classZoomGestureListenerextendsGestureDetector.SimpleOnGestureListener{
//双击手势事件
@Override
publicbooleanonDoubleTap(MotionEvente){
super.onDoubleTap(e);
Log.d(TAG,"onDoubleTap:双击事件");
if(mMediaRecorder!=null){
if(!isZoomIn){
setZoom(20);
isZoomIn=true;
}else{
setZoom(0);
isZoomIn=false;
}
}
returntrue;
}
}
step3:实现相机的变焦的方法
/**
*相机变焦
*
*@paramzoomValue
*/
publicvoidsetZoom(intzoomValue){
if(mCamera!=null){
Camera.Parametersparameters=mCamera.getParameters();
if(parameters.isZoomSupported()){//判断是否支持
intmaxZoom=parameters.getMaxZoom();
if(maxZoom==0){
return;
}
if(zoomValue>maxZoom){
zoomValue=maxZoom;
}
parameters.setZoom(zoomValue);
mCamera.setParameters(parameters);
}
}
}
Note:至此我们已经完成了对所有事件的监听,看到这里大家也许有些疲惫了,不过不要灰心,现在完成我们的核心部分,实现视频的录制
实现视频的录制
说是核心功能,也只不过是我们不知道某些API方法罢了,下面代码中我已经加了详细的注释,部分不能理解的记住就好^v^
/**
*开始录制
*/
privatevoidstartRecord(){
if(mMediaRecorder!=null){
//没有外置存储,直接停止录制
if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
return;
}
try{
//mMediaRecorder.reset();
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
//从相机采集视频
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
//从麦克采集音频信息
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//TODO:2016/10/20设置视频格式
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setVideoSize(videoWidth,videoHeight);
//每秒的帧数
mMediaRecorder.setVideoFrameRate(24);
//编码格式
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
//设置帧频率,然后就清晰了
mMediaRecorder.setVideoEncodingBitRate(1*1024*1024*100);
//TODO:2016/10/20临时写个文件地址,稍候该!!!
FiletargetDir=Environment.
getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
mTargetFile=newFile(targetDir,
SystemClock.currentThreadTimeMillis()+".mp4");
mMediaRecorder.setOutputFile(mTargetFile.getAbsolutePath());
mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
mMediaRecorder.prepare();
//正式录制
mMediaRecorder.start();
isRecording=true;
}catch(Exceptione){
e.printStackTrace();
}
}
}
实现视频的停止
大家可能会问,视频的停止为什么单独抽出来说呢?仔细的同学看上面代码会看到这两个方法:stopRecordSave和stopRecordUnSave,一个停止保存,一个是停止不保存,接下来我们就补上这个坑
停止并保存
privatevoidstopRecordSave(){
if(isRecording){
isRunning=false;
mMediaRecorder.stop();
isRecording=false;
Toast.makeText(this,"视频已经放至"+mTargetFile.getAbsolutePath(),Toast.LENGTH_SHORT).show();
}
}
停止不保存
privatevoidstopRecordUnSave(){
if(isRecording){
isRunning=false;
mMediaRecorder.stop();
isRecording=false;
if(mTargetFile.exists()){
//不保存直接删掉
mTargetFile.delete();
}
}
}
Note:这个停止不保存是我自己的一种想法,如果大家有更好的想法,欢迎大家到评论中指出,不胜感激
完整代码
源码我已经放在了github上了,写博客真是不易!写篇像样的博客更是不易,希望大家多多支持
总结
终于写完了!!!这是我最想说得话,从案例一开始到现在已经过去很长时间.这是我写得最长的一篇博客,发现能表达清楚自己的想法还是很困难的,这是我最大的感受!!!
实话说这个案例不是很困难,但是像我这样的初学者拿来练练手还是非常好的,在这里还要感谢再见杰克的博客,也给我提供了很多帮助
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!