Android 微信小視頻錄制功能的實現

ydqp9937 8年前發布 | 34K 次閱讀 安卓開發 Android開發 移動開發

開發之前

這幾天接觸了一下和視頻相關的控件, 所以, 繼之前的微信搖一搖, 我想到了來實現一下微信小視頻錄制的功能, 它的功能點比較多, 我每天都抽出點時間來寫寫, 說實話, 有些東西還是比較費勁, 希望大家認真看看, 說得不對的地方還請大家在評論中指正. 廢話不多說, 進入正題.

開發環境

最近剛更新的, 沒更新的小伙伴們抓緊了

  • 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回調
/////////////////////////////////////////////
@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>

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();
     }
    

    }

}</code></pre>

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); }</code></pre>

Note: OnProgressEndListener, 主要用于當進度條走到中間了, 好通知相機停止錄制, 接口如下:

public interface OnProgressEndListener{
    void onProgressEndListener();
}
/**

  • 當進度條結束后的 監聽
  • @param onProgressEndListener */ public void setOnProgressEndListener(OnProgressEndListener onProgressEndListener) { mOnProgressEndListener = onProgressEndListener; }</code></pre>

    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(); }</code></pre>

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

}</code></pre>

錄制事件的處理

錄制中觸發的事件包括四個:

  1. 長按錄制
  2. 抬起保存
  3. 上滑取消
  4. 雙擊放大(變焦)

    現在對這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;
    }
}

}</code></pre>

抬起保存

同樣我們這兒需要監聽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;</code></pre>

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;
    
    } });</code></pre>

    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;
    
    } }</code></pre>

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

}</code></pre>

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();
     }
    
    

    } }</code></pre>

    實現視頻的停止

    大家可能會問, 視頻的停止為什么單獨抽出來說呢? 仔細的同學看上面代碼會看到這兩個方法: 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: 這個停止不保存是我自己的一種想法, 如果大家有更好的想法, 歡迎大家到評論中指出, 不勝感激

    總結

    終于寫完了!!! 這是我最想說得話, 從案例一開始到現在已經過去很長時間. 這是我寫得最長的一篇博客, 發現能表達清楚自己的想法還是很困難的, 這是我最大的感受!!!

     

     

    來自:http://www.jianshu.com/p/6f84739ab85f

     

 本文由用戶 ydqp9937 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!