講解一個比較通用的錄音控件實現方法與設計技巧
最近由於需要做一個錄音功能(/噓 悄悄透露一下,千萬別告訴紅薯,就是新版本的OSC客戶端噢),起初打算采用仿微信的錄音方式,最後又改成了QQ的錄音方式,之前的微信錄音控件也就白寫了[大哭]。之前有很多朋友在問我自定義控件應該怎麼學習,遂正好拿出來講講喽,沒來得及截效果圖,大家就自己腦補一下微信發語音時的樣子吧。
所謂自定義控件其實就是由於系統SDK無法完成需要的功能時,通過自己擴展系統組件達到完成所需功能做出的控件。
Android自定義控件有兩種實現方式,一種是通過繼承View類,其中的全部界面通過畫布和畫筆自己創建,這種控件一般多用於游戲開發中;另一種則是通過繼承已有控件,或采用包含關系包含一個系統控件達到目的,這也是接下來本文所要講到的方法。
先看代碼(篇幅有限,僅保留重要方法)
/**
* 錄音專用Button,可彈出自定義的錄音dialog。需要配合{@link #RecordButtonUtil}使用
* @author kymjs([email protected])
*/
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([email protected])
*/
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錄音控件的實現方法。
最簡單的Ubuntu Touch & Android 雙系統安裝方式 http://www.linuxidc.com/Linux/2014-01/94881.htm
在Nexus上實現Ubuntu和Android 4.4.2 雙啟動 http://www.linuxidc.com/Linux/2014-05/101849.htm
Ubuntu 14.04 配置 Android SDK 開發環境 http://www.linuxidc.com/Linux/2014-05/101039.htm
64位Ubuntu 11.10下Android開發環境的搭建(JDK+Eclipse+ADT+Android SDK詳細) http://www.linuxidc.com/Linux/2013-06/85303.htm
Ubuntu 14.04 x64配置Android 4.4 kitkat編譯環境的方法 http://www.linuxidc.com/Linux/2014-04/101148.htm
Ubuntu 12.10 x64 安裝 Android SDK http://www.linuxidc.com/Linux/2013-03/82005.htm
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11