編輯:關於Android編程
Android 微信小視頻錄制功能
開發之前
這幾天接觸了一下和視頻相關的控件, 所以, 繼之前的微信搖一搖, 我想到了來實現一下微信小視頻錄制的功能, 它的功能點比較多, 我每天都抽出點時間來寫寫, 說實話, 有些東西還是比較費勁, 希望大家認真看看, 說得不對的地方還請大家在評論中指正. 廢話不多說, 進入正題.
開發環境
最近剛更新的, 沒更新的小伙伴們抓緊了
相關知識點
用到的東西真不少, 不過別著急, 咱們一個一個來.
開始開發
案例分析
大家可以打開自己微信裡面的小視頻, 一塊簡單的分析一下它的功能點有哪些 ?
根據上述的分析, 我們一步一步的完成
搭建布局
布局界面的實現還可以, 難度不大
<?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上了, 寫博客真是不易! 寫篇像樣的博客更是不易, 希望大家多多支持
總結
終於寫完了!!! 這是我最想說得話, 從案例一開始到現在已經過去很長時間. 這是我寫得最長的一篇博客, 發現能表達清楚自己的想法還是很困難的, 這是我最大的感受!!!
實話說這個案例不是很困難, 但是像我這樣的初學者拿來練練手還是非常好的, 在這裡還要感謝再見傑克的博客, 也給我提供了很多幫助
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
Gradle自定義插件在Gradle中創建自定義插件,Gradle提供了三種方式:在build.gradle腳本中直接使用 在buildSrc中使用 在獨立Module中
為了實現android activity之間的數據傳遞,主要利用意圖Intent 對象的方法來完成。 基本數據類型的傳遞是相當簡單了,主要通過如下方式完成如下: putE
iBeacon的工作原理是基於Bluetooth Low Energy(BLE)低功耗藍牙傳輸技術,iBeacon基站不斷向四周發送藍牙信號,當智
網上demo的效果:差不多應該是這樣的,但是容易出一些問題,比如你的圖片本身就是個圓角?又或者圖片太大,你想縮小顯示,但出現顯示內容不全?我想實現的效果是這樣的:http