Android 微信小视频录制功能实现

来源:互联网 发布:音乐 知乎 编辑:程序博客网 时间:2024/04/29 09:23

目录

  • 开发之前
    • 开发环境
    • 相关知识点
  • 开始开发
    • 案例预览
    • 案例分析
    • 搭建布局
    • 视频预览的实现
    • 自定义双向缩减的进度条
    • 录制事件的处理
      • 长按录制
      • 抬起保存
      • 上滑取消
      • 双击放大(变焦)
    • 实现视频的录制
    • 实现视频的停止
  • 完整代码
  • 总结

开发之前

这几天接触了一下和视频相关的控件, 所以, 继之前的微信摇一摇, 我想到了来实现一下微信小视频录制的功能, 它的功能点比较多, 我每天都抽出点时间来写写, 说实话, 有些东西还是比较费劲, 希望大家认真看看, 说得不对的地方还请大家在评论中指正. 废话不多说, 进入正题.

开发环境

最近刚更新的, 没更新的小伙伴们抓紧了

  • Android Studio 2.2.2
  • JDK1.7
  • API 24
  • Gradle 2.2.2

相关知识点

  • 视频录制界面 SurfaceView 的使用

  • Camera的使用

  • 相机的对焦, 变焦

  • 视频录制控件MediaRecorder的使用

  • 简单自定义View

  • GestureDetector(手势检测)的使用

用到的东西真不少, 不过别着急, 咱们一个一个来.

开始开发

案例预览

请原谅Gif图的粗糙

微信小视频

案例分析

大家可以打开自己微信里面的小视频, 一块简单的分析一下它的功能点有哪些 ?

  • 基本的视频预览功能

  • 长按 “按住拍” 实现视频的录制

  • 录制过程中的进度条从两侧向中间变短

  • 当松手或者进度条走到尽头视频停止录制 并保存

  • 从 “按住拍” 上滑取消视频的录制

  • 双击屏幕 变焦 放大

根据上述的分析, 我们一步一步的完成

搭建布局

布局界面的实现还可以, 难度不大

<?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回调/////////////////////////////////////////////@Overridepublic void surfaceCreated(SurfaceHolder holder) {    mSurfaceHolder = holder;    startPreView(holder);}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic 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, 反之为绿色barprivate 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 */@Overridepublic void setVisibility(int visibility) {    mVisibility = visibility;    //重新绘制    invalidate();}

step3 :最重要的一步, 画出我们的进度条,使用的就是View中的onDraw(Canvas canvas)方法

@Overrideprotected 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));    }}

录制事件的处理

录制中触发的事件包括四个:


  1. 长按录制
  2. 抬起保存
  3. 上滑取消
  4. 双击放大(变焦)

现在对这4个事件逐个分析:
前三这个事件, 我都放在了一个onTouch()回调方法中了
对于第4个, 我们待会谈
我们先把onTouch()中局部变量列举一下:
@Overridepublic 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上了, 写博客真是不易! 写篇像样的博客更是不易, 希望大家多多支持

总结

终于写完了!!! 这是我最想说得话, 从案例一开始到现在已经过去很长时间. 这是我写得最长的一篇博客, 发现能表达清楚自己的想法还是很困难的, 这是我最大的感受!!!
实话说这个案例不是很困难, 但是像我这样的初学者拿来练练手还是非常好的, 在这里还要感谢再见杰克的博客, 也给我提供了很多帮助
最后, 希望大家共同进步!

3 0
原创粉丝点击