編輯:關於Android編程
這幾天接觸了一下和視頻相關的控件, 所以, 繼之前的微信搖一搖, 我想到了來實現一下微信小視頻錄制的功能, 它的功能點比較多, 我每天都抽出點時間來寫寫, 說實話, 有些東西還是比較費勁, 希望大家認真看看, 說得不對的地方還請大家在評論中指正. 廢話不多說, 進入正題.
Android Studio 2.2.2 JDK1.7 API 24 Gradle 2.2.2最近剛更新的, 沒更新的小伙伴們抓緊了
視頻錄制界面 SurfaceView 的使用
Camera的使用
相機的對焦, 變焦
視頻錄制控件MediaRecorder的使用
簡單自定義View
GestureDetector(手勢檢測)的使用
用到的東西真不少, 不過別著急, 咱們一個一個來.
大家可以打開自己微信裡面的小視頻, 一塊簡單的分析一下它的功能點有哪些 ?
基本的視頻預覽功能
長按 “按住拍” 實現視頻的錄制
錄制過程中的進度條從兩側向中間變短
當松手或者進度條走到盡頭視頻停止錄制 並保存
從 “按住拍” 上滑取消視頻的錄制
雙擊屏幕 變焦 放大
根據上述的分析, 我們一步一步的完成
布局界面的實現還可以, 難度不大
<framelayout android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"></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自動對焦 ListfocusModes = 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 WeakReferencemReference; private MainActivity mActivity; public MyHandler(MainActivity activity) { mReference = new WeakReference (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上了, 寫博客真是不易! 寫篇像樣的博客更是不易, 希望大家多多支持
有幾天沒有更新博客了,不過本篇卻准備了許久,希望能帶給每一位開發者最簡單高效的學習方式。廢話到此為止,下面開始正文。 NFC(Near Field Communicati
1.構建一張七彩圖:我們經常看到這種樣子的顏色選擇器吧..然後其實右邊的亮度選擇是:這樣我們的代碼就可以進行啦... // 創建七彩圖片 private voi
(1) (2) package com.example.listview1; import java.util.ArrayList; im
一、Android平台濾鏡濾鏡這個功能在目前的市場上應用很廣泛,發展也非常快,總結起來,基本上有以下三種應用會包含濾鏡功能,都各有所長。 二、相機濾鏡介紹1、相