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

ydqp9937 9年前
   <h3><strong>开发之前</strong></h3>    <p>这几天接触了一下和视频相关的控件, 所以, 继之前的微信摇一摇, 我想到了来实现一下微信小视频录制的功能, 它的功能点比较多, 我每天都抽出点时间来写写, 说实话, 有些东西还是比较费劲, 希望大家认真看看, 说得不对的地方还请大家在评论中指正. 废话不多说, 进入正题.</p>    <h3><strong>开发环境</strong></h3>    <p>最近刚更新的, 没更新的小伙伴们抓紧了</p>    <ul>     <li>Android Studio 2.2.2</li>     <li>JDK1.7</li>     <li>API 24</li>     <li>Gradle 2.2.2</li>    </ul>    <p>相关知识点</p>    <ul>     <li> <p>视频录制界面 SurfaceView 的使用</p> </li>     <li> <p>Camera的使用</p> </li>     <li> <p>相机的对焦, 变焦</p> </li>     <li> <p>视频录制控件MediaRecorder的使用</p> </li>     <li> <p>简单自定义View</p> </li>     <li> <p>GestureDetector(手势检测)的使用</p> </li>    </ul>    <p>用到的东西真不少, 不过别着急, 咱们一个一个来.</p>    <h3><strong>开始开发</strong></h3>    <p><strong>案例预览</strong></p>    <p>请原谅Gif图的粗糙</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/11da4d8176c8c5432255e746116ff273.gif"></p>    <p style="text-align:center">微信小视频</p>    <p><strong>案例分析</strong></p>    <p>大家可以打开自己微信里面的小视频, 一块简单的分析一下它的功能点有哪些 ?</p>    <ul>     <li> <p>基本的视频预览功能</p> </li>     <li> <p>长按 "按住拍" 实现视频的录制</p> </li>     <li> <p>录制过程中的进度条从两侧向中间变短</p> </li>     <li> <p>当松手或者进度条走到尽头视频停止录制 并保存</p> </li>     <li> <p>从 "按住拍" 上滑取消视频的录制</p> </li>     <li> <p>双击屏幕 变焦 放大</p> </li>    </ul>    <p>根据上述的分析, 我们一步一步的完成</p>    <p><strong>搭建布局</strong></p>    <p>布局界面的实现还可以, 难度不大</p>    <pre>  <code class="language-java"><?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></code></pre>    <p><strong>视频预览的实现</strong></p>    <p>step1: 得到SufaceView控件, 设置基本属性和相应监听(该控件的创建是异步的, 只有在真正"准备"好之后才能调用)</p>    <pre>  <code class="language-java">mSurfaceView = (SurfaceView) findViewById(R.id.main_surface_view);   //设置屏幕分辨率  mSurfaceHolder.setFixedSize(videoWidth, videoHeight);  mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);  mSurfaceHolder.addCallback(this);</code></pre>    <p>step2: 实现接口的方法, surfaceCreated方法中开启视频的预览, 在surfaceDestroyed中销毁</p>    <pre>  <code class="language-java">//////////////////////////////////////////////  // 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;      }  }</code></pre>    <p>step3: 实现视频预览的方法</p>    <pre>  <code class="language-java">/**   * 开启预览   *   * @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();          }      }    }</code></pre>    <p>Note: 上面添加了自动对焦的代码, 但是部分手机可能不支持</p>    <p><strong>自定义双向缩减的进度条</strong></p>    <p>有些像我一样的初学者一看到自定义某某View, 就觉得比较牛X. 其实呢, Google已经替我们写好了很多代码, 所以我们用就行了.而且咱们的这个进度条也没啥, 不就是一根线, 今天咱就来说说.</p>    <p>step1: 继承View, 完成初始化</p>    <pre>  <code class="language-java">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);  }</code></pre>    <p>Note: OnProgressEndListener, 主要用于当进度条走到中间了, 好通知相机停止录制, 接口如下:</p>    <pre>  <code class="language-java">public interface OnProgressEndListener{      void onProgressEndListener();  }  /**   * 当进度条结束后的 监听   * @param onProgressEndListener   */  public void setOnProgressEndListener(OnProgressEndListener onProgressEndListener) {      mOnProgressEndListener = onProgressEndListener;  }</code></pre>    <p>step2 :设置Setter方法用于通知我们的Progress改变状态</p>    <pre>  <code class="language-java">/**   * 设置进度   * @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();  }</code></pre>    <p>step3 :最重要的一步, 画出我们的进度条,使用的就是View中的onDraw(Canvas canvas)方法</p>    <pre>  <code class="language-java">@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));      }  }</code></pre>    <p><strong>录制事件的处理</strong></p>    <p>录制中触发的事件包括四个:</p>    <ol>     <li>长按录制</li>     <li>抬起保存</li>     <li>上滑取消</li>     <li>双击放大(变焦) <p>现在对这4个事件逐个分析:</p> <p>前三这个事件, 我都放在了一个onTouch()回调方法中了</p> <p>对于第4个, 我们待会谈</p> <p>我们先把onTouch()中局部变量列举一下:</p> </li>    </ol>    <pre>  <code class="language-java">@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;     // ...  }</code></pre>    <p><strong>长按录制</strong></p>    <p>长按录制我们需要监听ACTION_DOWN事件, 使用线程延迟发送Handler来实现进度条的更新</p>    <pre>  <code class="language-java">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;  }</code></pre>    <p>Note: startRecord()这个方法先不说, 我们只需要知道执行了它就可以录制了, 但是Handler事件还是要说的, 它只负责更新进度条的进度</p>    <pre>  <code class="language-java">////////////////////////////////////////////////////  // 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;          }      }  }</code></pre>    <p><strong>抬起保存</strong></p>    <p>同样我们这儿需要监听ACTION_UP事件, 但是要考虑当用户抬起过快时(录制的时间过短), 不需要保存. 而且, 在这个事件中包含了取消状态的抬起, 解释一下: 就是当上滑取消时抬起的一瞬间取消录制, 大家看代码</p>    <pre>  <code class="language-java">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;</code></pre>    <p>Note: 同样的, 内部的stopRecordUnSave()和stopRecordSave();大家先不要考虑, 我们会在后面介绍, 他俩从名字就能看出 前者用来停止录制但不保存, 后者停止录制并保存</p>    <p><strong>上滑取消</strong></p>    <p>配合上一部分说得抬起取消事件, 实现上滑取消</p>    <pre>  <code class="language-java">case MotionEvent.ACTION_MOVE:    if (ex > left && ex < right) {        float currentY = event.getY();        if (downY - currentY > 10) {            isCancel = true;            mProgressBar.setCancel(true);        }    }    break;</code></pre>    <p>Note: 主要原理不难, 只要按下并且向上移动一定距离 就会触发,当手抬起时视频录制取消</p>    <p><strong>双击放大(变焦)</strong></p>    <p>这个事件比较特殊, 使用了Google提供的GestureDetector手势检测 来判断双击事件</p>    <p>step1: 对SurfaceView进行单独的Touch事件监听, why? 因为GestureDetector需要Touch事件的完全托管, 如果只给它传部分事件会造成某些事件失效</p>    <pre>  <code class="language-java">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;      }  });</code></pre>    <p>step2: 重写GestureDetector.SimpleOnGestureListener, 实现双击事件</p>    <pre>  <code class="language-java">///////////////////////////////////////////////////////////////////////////  // 变焦手势处理类  ///////////////////////////////////////////////////////////////////////////  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;      }  }</code></pre>    <p>step3: 实现相机的变焦的方法</p>    <pre>  <code class="language-java">/**   * 相机变焦   *   * @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);          }      }    }</code></pre>    <p>Note: 至此我们已经完成了对所有事件的监听, 看到这里大家也许有些疲惫了, 不过不要灰心, 现在完成我们的核心部分, 实现视频的录制</p>    <p><strong>实现视频的录制</strong></p>    <p>说是核心功能, 也只不过是我们不知道某些API方法罢了, 下面代码中我已经加了详细的注释, 部分不能理解的记住就好^v^</p>    <pre>  <code class="language-java">/**   * 开始录制   */  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();          }        }  }</code></pre>    <p><strong>实现视频的停止</strong></p>    <p>大家可能会问, 视频的停止为什么单独抽出来说呢? 仔细的同学看上面代码会看到这两个方法: stopRecordSave和stopRecordUnSave, 一个停止保存, 一个是停止不保存, 接下来我们就补上这个坑</p>    <p>停止并保存</p>    <pre>  <code class="language-java">private void stopRecordSave() {      if (isRecording) {          isRunning = false;          mMediaRecorder.stop();          isRecording = false;          Toast.makeText(this, "视频已经放至" + mTargetFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();      }  }</code></pre>    <p>停止不保存</p>    <pre>  <code class="language-java">private void stopRecordUnSave() {      if (isRecording) {          isRunning = false;          mMediaRecorder.stop();          isRecording = false;          if (mTargetFile.exists()) {              //不保存直接删掉              mTargetFile.delete();          }      }  }</code></pre>    <p>Note: 这个停止不保存是我自己的一种想法, 如果大家有更好的想法, 欢迎大家到评论中指出, 不胜感激</p>    <h3><strong>总结</strong></h3>    <p>终于写完了!!! 这是我最想说得话, 从案例一开始到现在已经过去很长时间. 这是我写得最长的一篇博客, 发现能表达清楚自己的想法还是很困难的, 这是我最大的感受!!!</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/6f84739ab85f</p>    <p> </p>