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>
錄制事件的處理
錄制中觸發的事件包括四個:
- 長按錄制
- 抬起保存
- 上滑取消
- 雙擊放大(變焦)
現在對這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