Android 微信小视频录制功能实现详细介绍
android 微信小视频录制功能
开发之前
这几天接触了一下和视频相关的控件, 所以, 继之前的微信摇一摇, 我想到了来实现一下微信小视频录制的功能, 它的功能点比较多, 我每天都抽出点时间来写写, 说实话, 有些东西还是比较费劲, 希望大家认真看看, 说得不对的地方还请大家在评论中指正. 废话不多说, 进入正题.
开发环境
最近刚更新的, 没更新的小伙伴们抓紧了
- android studio 2.2.2
- jdk1.7
- api 24
- gradle 2.2.2
相关知识点
- 视频录制界面 surfaceview 的使用
- camera的使用
- 相机的对焦, 变焦
- 视频录制控件mediarecorder的使用
- 简单自定义view
- gesturedetector(手势检测)的使用
用到的东西真不少, 不过别着急, 咱们一个一个来.
开始开发
案例分析
大家可以打开自己微信里面的小视频, 一块简单的分析一下它的功能点有哪些 ?
- 基本的视频预览功能
- 长按 “按住拍” 实现视频的录制
- 录制过程中的进度条从两侧向中间变短
- 当松手或者进度条走到尽头视频停止录制 并保存
- 从 “按住拍” 上滑取消视频的录制
- 双击屏幕 变焦 放大
根据上述的分析, 我们一步一步的完成
搭建布局
布局界面的实现还可以, 难度不大
<?xml version="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 public void surfacecreated(surfaceholder holder) { msurfaceholder = holder; startpreview(holder); } @override public void surfacechanged(surfaceholder holder, int format, int width, int height) { } @override public void surfacedestroyed(surfaceholder holder) { if (mcamera != null) { log.d(tag, "surfacedestroyed: "); //停止预览并释放摄像头资源 mcamera.stoppreview(); mcamera.release(); mcamera = null; } if (mmediarecorder != null) { mmediarecorder.release(); mmediarecorder = null; } }
step3: 实现视频预览的方法
/** * 开启预览 * * @param holder */ private void startpreview(surfaceholder holder) { log.d(tag, "startpreview: "); if (mcamera == null) { mcamera = camera.open(camera.camerainfo.camera_facing_back); } if (mmediarecorder == null) { mmediarecorder = new mediarecorder(); } if (mcamera != null) { mcamera.setdisplayorientation(90); try { mcamera.setpreviewdisplay(holder); camera.parameters parameters = mcamera.getparameters(); //实现camera自动对焦 list<string> focusmodes = parameters.getsupportedfocusmodes(); if (focusmodes != null) { for (string mode : focusmodes) { mode.contains("continuous-video"); parameters.setfocusmode("continuous-video"); } } mcamera.setparameters(parameters); mcamera.startpreview(); } catch (ioexception e) { e.printstacktrace(); } } }
note: 上面添加了自动对焦的代码, 但是部分手机可能不支持
自定义双向缩减的进度条
有些像我一样的初学者一看到自定义某某view, 就觉得比较牛x. 其实呢, google已经替我们写好了很多代码, 所以我们用就行了.而且咱们的这个进度条也没啥, 不就是一根线, 今天咱就来说说.
step1: 继承view, 完成初始化
private static final string tag = "bothwayprogressbar"; //取消状态为红色bar, 反之为绿色bar private boolean iscancel = false; private context mcontext; //正在录制的画笔 private paint mrecordpaint; //上滑取消时的画笔 private paint mcancelpaint; //是否显示 private int mvisibility; // 当前进度 private int progress; //进度条结束的监听 private onprogressendlistener monprogressendlistener; public bothwayprogressbar(context context) { super(context, null); } public bothwayprogressbar(context context, attributeset attrs) { super(context, attrs); mcontext = context; init(); } private void init() { mvisibility = invisible; mrecordpaint = new paint(); mrecordpaint.setcolor(color.green); mcancelpaint = new paint(); mcancelpaint.setcolor(color.red); }
note: onprogressendlistener, 主要用于当进度条走到中间了, 好通知相机停止录制, 接口如下:
public interface onprogressendlistener{ void onprogressendlistener(); } /** * 当进度条结束后的 监听 * @param onprogressendlistener */ public void setonprogressendlistener(onprogressendlistener onprogressendlistener) { monprogressendlistener = onprogressendlistener; }
step2 :设置setter方法用于通知我们的progress改变状态
/** * 设置进度 * @param progress */ public void setprogress(int progress) { this.progress = progress; invalidate(); } /** * 设置录制状态 是否为取消状态 * @param iscancel */ public void setcancel(boolean iscancel) { this.iscancel = iscancel; invalidate(); } /** * 重写是否可见方法 * @param visibility */ @override public void setvisibility(int visibility) { mvisibility = visibility; //重新绘制 invalidate(); }
step3 :最重要的一步, 画出我们的进度条,使用的就是view中的ondraw(canvas canvas)方法
@override protected void ondraw(canvas canvas) { super.ondraw(canvas); if (mvisibility == view.visible) { int height = getheight(); int width = getwidth(); int mid = 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 public boolean ontouch(view v, motionevent event) { boolean ret = false; int action = event.getaction(); float ey = event.gety(); float ex = event.getx(); //只监听中间的按钮处 int vw = v.getwidth(); int left = listener_start; int right = vw - listener_start; float downy = 0; // ... }
长按录制
长按录制我们需要监听action_down事件, 使用线程延迟发送handler来实现进度条的更新
switch (action) { case motionevent.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 = new thread() { @override public void run() { super.run(); try { mprogress = 0; isrunning = true; while (isrunning) { mprogress++; mhandler.obtainmessage(0).sendtotarget(); thread.sleep(20); } } catch (interruptedexception e) { e.printstacktrace(); } } }; mprogressthread.start(); ret = true; } break; // ... return true; }
note: startrecord()这个方法先不说, 我们只需要知道执行了它就可以录制了, 但是handler事件还是要说的, 它只负责更新进度条的进度
//////////////////////////////////////////////////// // handler处理 ///////////////////////////////////////////////////// private static class myhandler extends handler { private weakreference<mainactivity> mreference; private mainactivity mactivity; public myhandler(mainactivity activity) { mreference = new weakreference<mainactivity>(activity); mactivity = mreference.get(); } @override public void handlemessage(message msg) { switch (msg.what) { case 0: mactivity.mprogressbar.setprogress(mactivity.mprogress); break; } } }
抬起保存
同样我们这儿需要监听action_up事件, 但是要考虑当用户抬起过快时(录制的时间过短), 不需要保存. 而且, 在这个事件中包含了取消状态的抬起, 解释一下: 就是当上滑取消时抬起的一瞬间取消录制, 大家看代码
case motionevent.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();大家先不要考虑, 我们会在后面介绍, 他俩从名字就能看出 前者用来停止录制但不保存, 后者停止录制并保存
上滑取消
配合上一部分说得抬起取消事件, 实现上滑取消
case motionevent.action_move: if (ex > left && ex < right) { float currenty = event.gety(); if (downy - currenty > 10) { iscancel = true; mprogressbar.setcancel(true); } } break;
note: 主要原理不难, 只要按下并且向上移动一定距离 就会触发,当手抬起时视频录制取消
双击放大(变焦)
这个事件比较特殊, 使用了google提供的gesturedetector手势检测 来判断双击事件
step1: 对surfaceview进行单独的touch事件监听, why? 因为gesturedetector需要touch事件的完全托管, 如果只给它传部分事件会造成某些事件失效
mdetector = new gesturedetector(this, new zoomgesturelistener()); /** * 单独处理msurfaceview的双击事件 */ msurfaceview.setontouchlistener(new view.ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { mdetector.ontouchevent(event); return true; } });
step2: 重写gesturedetector.simpleongesturelistener, 实现双击事件
/////////////////////////////////////////////////////////////////////////// // 变焦手势处理类 /////////////////////////////////////////////////////////////////////////// class zoomgesturelistener extends gesturedetector.simpleongesturelistener { //双击手势事件 @override public boolean ondoubletap(motionevent e) { super.ondoubletap(e); log.d(tag, "ondoubletap: 双击事件"); if (mmediarecorder != null) { if (!iszoomin) { setzoom(20); iszoomin = true; } else { setzoom(0); iszoomin = false; } } return true; } }
step3: 实现相机的变焦的方法
/** * 相机变焦 * * @param zoomvalue */ public void setzoom(int zoomvalue) { if (mcamera != null) { camera.parameters parameters = mcamera.getparameters(); if (parameters.iszoomsupported()) {//判断是否支持 int maxzoom = parameters.getmaxzoom(); if (maxzoom == 0) { return; } if (zoomvalue > maxzoom) { zoomvalue = maxzoom; } parameters.setzoom(zoomvalue); mcamera.setparameters(parameters); } } }
note: 至此我们已经完成了对所有事件的监听, 看到这里大家也许有些疲惫了, 不过不要灰心, 现在完成我们的核心部分, 实现视频的录制
实现视频的录制
说是核心功能, 也只不过是我们不知道某些api方法罢了, 下面代码中我已经加了详细的注释, 部分不能理解的记住就好^v^
/** * 开始录制 */ private void startrecord() { 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 临时写个文件地址, 稍候该!!! file targetdir = environment. getexternalstoragepublicdirectory(environment.directory_movies); mtargetfile = new file(targetdir, systemclock.currentthreadtimemillis() + ".mp4"); mmediarecorder.setoutputfile(mtargetfile.getabsolutepath()); mmediarecorder.setpreviewdisplay(msurfaceholder.getsurface()); mmediarecorder.prepare(); //正式录制 mmediarecorder.start(); isrecording = true; } catch (exception e) { e.printstacktrace(); } } }
实现视频的停止
大家可能会问, 视频的停止为什么单独抽出来说呢? 仔细的同学看上面代码会看到这两个方法: stoprecordsave和stoprecordunsave, 一个停止保存, 一个是停止不保存, 接下来我们就补上这个坑
停止并保存
private void stoprecordsave() { if (isrecording) { isrunning = false; mmediarecorder.stop(); isrecording = false; toast.maketext(this, "视频已经放至" + mtargetfile.getabsolutepath(), toast.length_short).show(); } }
停止不保存
private void stoprecordunsave() { if (isrecording) { isrunning = false; mmediarecorder.stop(); isrecording = false; if (mtargetfile.exists()) { //不保存直接删掉 mtargetfile.delete(); } } }
note: 这个停止不保存是我自己的一种想法, 如果大家有更好的想法, 欢迎大家到评论中指出, 不胜感激
完整代码
源码我已经放在了github上了, 写博客真是不易! 写篇像样的博客更是不易, 希望大家多多支持
总结
终于写完了!!! 这是我最想说得话, 从案例一开始到现在已经过去很长时间. 这是我写得最长的一篇博客, 发现能表达清楚自己的想法还是很困难的, 这是我最大的感受!!!
实话说这个案例不是很困难, 但是像我这样的初学者拿来练练手还是非常好的, 在这里还要感谢再见杰克的博客, 也给我提供了很多帮助
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!