Android仿錄音功能,自訂控制項的設計技巧
歡迎各位加入我的Android開發群[257053751]
最近由於需要做一個錄音功能(/噓 悄悄透露一下,千萬別告訴紅薯,就是新版本的OSC用戶端噢),起初打算採用仿的錄音方式,最後又改成了QQ的錄音方式,之前的錄音控制項也就白寫了[大哭]。之前有很多朋友在問我自訂控制項應該怎麼學習,遂正好拿出來講講嘍,沒來得及截,大家就自己腦補一下發語音時的樣子吧。
所謂自訂控制項其實就是由於系統SDK無法完成需要的功能時,通過自己擴充系統組件達到完成所需功能做出的控制項。
Android自訂控制項有兩種實現方式,一種是通過繼承View類,其中的全部介面通過畫布和畫筆自己建立,這種控制項一般多用於遊戲開發中;另一種則是通過繼承已有控制項,或採用內含項目關聯性包含一個系統控制項達到目的,這也是接下來本文所要講到的方法。
先看代碼(篇幅有限,僅保留重要方法)
/** * 錄音專用Button,可彈出自訂的錄音dialog。需要配合{@link #RecordButtonUtil}使用 * @author kymjs(kymjs123@gmail.com) */public class RecordButton extends Button { private static final int MIN_INTERVAL_TIME = 700; // 錄音最短時間 private static final int MAX_INTERVAL_TIME = 60000; // 錄音最長時間 private RecordButtonUtil mAudioUtil; private Handler mVolumeHandler; // 用於更新錄音音量大小的圖片 public RecordButton(Context context) { super(context); mVolumeHandler = new ShowVolumeHandler(this); mAudioUtil = new RecordButtonUtil(); initSavePath(); } @Override public boolean onTouchEvent(MotionEvent event) { if (mAudioFile == null) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: initlization(); break; case MotionEvent.ACTION_UP: if (event.getY() < -50) { cancelRecord(); } else { finishRecord(); } break; case MotionEvent.ACTION_MOVE: //做一些UI提示 break; } return true; } /** 初始化 dialog和錄音器 */ private void initlization() { mStartTime = System.currentTimeMillis(); if (mRecordDialog == null) { mRecordDialog = new Dialog(getContext()); mRecordDialog.setOnDismissListener(onDismiss); } mRecordDialog.show(); startRecording(); } /** 錄音完成(達到最長時間或使用者決定錄音完成) */ private void finishRecord() { stopRecording(); mRecordDialog.dismiss(); long intervalTime = System.currentTimeMillis() - mStartTime; if (intervalTime < MIN_INTERVAL_TIME) { AppContext.showToastShort(R.string.record_sound_short); File file = new File(mAudioFile); file.delete(); return; } if (mFinishedListerer != null) { mFinishedListerer.onFinishedRecord(mAudioFile, (int) ((System.currentTimeMillis() - mStartTime) / 1000)); } } // 使用者手動取消錄音 private void cancelRecord() { stopRecording(); mRecordDialog.dismiss(); File file = new File(mAudioFile); file.delete(); if (mFinishedListerer != null) { mFinishedListerer.onCancleRecord(); } } // 開始錄音 private void startRecording() { mAudioUtil.setAudioPath(mAudioFile); mAudioUtil.recordAudio(); mThread = new ObtainDecibelThread(); mThread.start(); } // 停止錄音 private void stopRecording() { if (mThread != null) { mThread.exit(); mThread = null; } if (mAudioUtil != null) { mAudioUtil.stopRecord(); } } /******************************* inner class ****************************************/ private class ObtainDecibelThread extends Thread { private volatile boolean running = true; public void exit() { running = false; } @Override public void run() { while (running) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } if (System.currentTimeMillis() - mStartTime >= MAX_INTERVAL_TIME) { // 如果超過最長錄音時間 mVolumeHandler.sendEmptyMessage(-1); } if (mAudioUtil != null && running) { // 如果使用者仍在錄音 int volumn = mAudioUtil.getVolumn(); if (volumn != 0) mVolumeHandler.sendEmptyMessage(volumn); } else { exit(); } } } } private final OnDismissListener onDismiss = new OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { stopRecording(); } }; static class ShowVolumeHandler extends Handler { private final WeakReference mOuterInstance; public ShowVolumeHandler(RecordButton outer) { mOuterInstance = new WeakReference(outer); } @Override public void handleMessage(Message msg) { RecordButton outerButton = mOuterInstance.get(); if (msg.what != -1) { // 大於0時 表示當前錄音的音量 if (outerButton.mVolumeListener != null) { outerButton.mVolumeListener.onVolumeChange(mRecordDialog, msg.what); } } else { // -1 時表示錄音逾時 outerButton.finishRecord(); } } } /** 音量改變的監聽器 */ public interface OnVolumeChangeListener { void onVolumeChange(Dialog dialog, int volume); } public interface OnFinishedRecordListener { /** 使用者手動取消 */ public void onCancleRecord(); /** 錄音完成 */ public void onFinishedRecord(String audioPath, int recordTime); }}
/** * {@link #RecordButton}需要的工具類 * * @author kymjs(kymjs123@gmail.com) */public class RecordButtonUtil { public static final String AUDOI_DIR = Environment .getExternalStorageDirectory().getAbsolutePath() + "/oschina/audio"; // 錄音音頻儲存根路徑 private String mAudioPath; // 要播放的聲音的路徑 private boolean mIsRecording;// 是否正在錄音 private boolean mIsPlaying;// 是否現正播放 private OnPlayListener listener; // 初始化 錄音器 private void initRecorder() { mRecorder = new MediaRecorder(); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); mRecorder.setOutputFile(mAudioPath); mIsRecording = true; } /** 開始錄音,並儲存到檔案中 */ public void recordAudio() { initRecorder(); try { mRecorder.prepare(); } catch (IOException e) { e.printStackTrace(); } mRecorder.start(); } /** 擷取音量值,只是針對錄音音量 */ public int getVolumn() { int volumn = 0; // 錄音 if (mRecorder != null && mIsRecording) { volumn = mRecorder.getMaxAmplitude(); if (volumn != 0) volumn = (int) (10 * Math.log(volumn) / Math.log(10)) / 7; } return volumn; } /** 停止錄音 */ public void stopRecord() { if (mRecorder != null) { mRecorder.stop(); mRecorder.release(); mRecorder = null; mIsRecording = false; } } public void startPlay(String audioPath) { if (!mIsPlaying) { if (!StringUtils.isEmpty(audioPath)) { mPlayer = new MediaPlayer(); try { mPlayer.setDataSource(audioPath); mPlayer.prepare(); mPlayer.start(); if (listener != null) { listener.starPlay(); } mIsPlaying = true; mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { if (listener != null) { listener.stopPlay(); } mp.release(); mPlayer = null; mIsPlaying = false; } }); } catch (Exception e) { e.printStackTrace(); } } else { AppContext.showToastShort(R.string.record_sound_notfound); } } // end playing } public interface OnPlayListener { /** 播放聲音結束時調用 */ void stopPlay(); /** 播放聲音開始時調用 */ void starPlay(); }}
作為控制項介面控制邏輯,我們主要看一下onTouchEvent方法:當手指按下的時候,初始化錄音器。手指在螢幕上移動的時候如果滑到按鈕之上的時候,event.getY會返回一個負值(因為滑出控制項了嘛)。這裡我寫的是-50主要是為了多一點緩衝,防止誤操作。
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: initlization(); break; case MotionEvent.ACTION_UP: if (mIsCancel && event.getY() < -50) { cancelRecord(); } else { finishRecord(); } mIsCancel = false; break; case MotionEvent.ACTION_MOVE: // 當手指移動到view外面,會cancel //做一些UI提示 break; } return true; }
一些設計技巧:比如通過回調解耦,使控制項變得通用。雖說自訂控制項一般不需要多麼的通用,但是像錄音控制項這種很多應用都會用到的功能,還是做得通用一點要好。像錄音時彈出的dialog,我採用從外部擷取的方式,方便以後修改這個彈窗,也方便代碼閱讀的時候更加清晰。再比如根據話筒音量改變錄音表徵圖這樣的方法,設定成外部以後,就算以後更換其他圖片,更換其他顯示方式,對自訂控制項本身來說,不需要改任何代碼。
對於錄音和放音的功能實現,採用內含項目關聯性單獨寫在一個新類裡面,這樣方便以後做更多擴充,比如未來採用私人的錄音編碼加密,比如播放錄音之前先放一段音樂(誰特麼這麼無聊)等等。。。
再來看一下Thread與Handle的互動,這裡我設計的並不是很好,其實不應該將兩種訊息放在同一個msg中發出的,這裡主要是考慮到訊息簡單,使用一個空msg僅僅通過一個int值區分資訊就行了。
Handle中採用了一個軟引用包含外部類,這種方式在網上有很多講解,之後我也會單獨再寫一篇部落格講解,這裡大家知道目的是為了防止對象間的互相引用造成記憶體泄露就可以了。
以上便是對仿錄音介面的一個講解,其實的錄音效果實現起來比起QQ的效果還是比較簡單的,以後我也會再講QQ錄音控制項的實現方法。