最近因爲須要作一個錄音功能(/噓 悄悄透露一下,千萬別告訴紅薯,就是新版本的OSC客戶端噢),起初打算採用仿微信的錄音方式,最後又改爲了QQ的錄音方式,以前的微信錄音控件也就白寫了[大哭]。以前有不少朋友在問我自定義控件應該怎麼學習,遂正好拿出來說講嘍,沒來得及截效果圖,你們就本身腦補一下微信發語音時的樣子吧。java
所謂自定義控件其實就是因爲系統SDK沒法完成須要的功能時,經過本身擴展系統組件達到完成所需功能作出的控件。
微信
Android自定義控件有兩種實現方式,一種是經過繼承View類,其中的所有界面經過畫布和畫筆本身建立,這種控件通常多用於遊戲開發中;另外一種則是經過繼承已有控件,或採用包含關係包含一個系統控件達到目的,這也是接下來本文所要講到的方法。
ide
先看代碼(篇幅有限,僅保留重要方法)
工具
/** * 錄音專用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<RecordButton> mOuterInstance; public ShowVolumeHandler(RecordButton outer) { mOuterInstance = new WeakReference<RecordButton>(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,我採用從外部獲取的方式,方便之後修改這個彈窗,也方便代碼閱讀的時候更加清晰。再好比根據話筒音量改變錄音圖標這樣的方法,設置成外部之後,就算之後更換其餘圖片,更換其餘顯示方式,對自定義控件自己來講,不須要改任何代碼。this
對於錄音和放音的功能實現,採用包含關係單獨寫在一個新類裏面,這樣方便之後作更多擴展,好比將來採用私有的錄音編碼加密,好比播放錄音以前先放一段音樂(誰特麼這麼無聊)等等。。。編碼
再來看一下Thread與Handle的交互,這裏我設計的並非很好,其實不該該將兩種消息放在同一個msg中發出的,這裏主要是考慮到消息簡單,使用一個空msg僅僅經過一個int值區分信息就好了。加密
Handle中採用了一個軟引用包含外部類,這種方式在網上有不少講解,以後我也會單獨再寫一篇博客講解,這裏你們知道目的是爲了防止對象間的互相引用形成內存泄露就能夠了。spa
以上即是對仿微信錄音界面的一個講解,其實微信的錄音效果實現起來比起QQ的效果仍是比較簡單的,之後我也會再講QQ錄音控件的實現方法。設計