本文是仿照張鴻洋在慕課網的教學視訊《Android-仿微信語音聊天》而作,從某種意義上來說并不能算作純粹的原創,在此首先向這位大神緻敬~
首先展示一下效果。1、當使用者按下“按住說話”按鈕時,彈出對話框,此時開始錄音,并且右邊的音量随聲音大小而波動。2、如果這時手指向上滑動,則顯示取消發送語音的提示。3、當錄音結束時,發送語音。4、如果錄音時間過短,則對話框給出提示,此次錄音失效。
實作此功能的關鍵在于三個部分:提示對話框,聲音錄制和錄音按鈕。
首先讨論錄音對話框,共分4種情況。
- 1、預設(不顯示對話框)
- 2、正在錄音(顯示麥克風和音量)
- 3、試圖取消(顯示箭頭)
- 4、時間過短(顯示歎号)
根據上面分析,先寫出對話框的布局。對話框上排為兩張圖檔,下面為一行提示文字
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:gravity="center"
android:background="@drawable/audiorec_dialog_loading_bg"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/img_recdlg_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/audiorec_recorder"
android:visibility="visible"/>
<ImageView
android:id="@+id/img_recdlg_voice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/audiorec_v1"
android:visibility="visible"/>
</LinearLayout>
<TextView
android:id="@+id/txt_recdlg_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/str_audiorecdlg_label_recording"
android:textColor="@color/white"/>
</LinearLayout>
并且在styles.xml檔案中,加上對話框的樣式
<style name="Theme_AudioDialog" parent="@android:style/Theme.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item> <!--半透明-->
<item name="android:backgroundDimEnabled">false</item> <!--背景變暗-->
</style>
接下來建立一個用于管理對話框的類——RecordDialogManager,并且在類中提供外部調用的方法,使其能夠轉換成上面說的4中情況。預設狀态下,直接把dialog給dismiss掉即可。對于正在錄音這種情況,首先我們要建立顯示對話框,然後将圖檔設為對應樣式。
public void showRecordingDialog()
{
mDialog = new Dialog(mContext, R.style.Theme_AudioDialog);
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.layout_dialog_rec,null);
mDialog.setContentView(view);
mIcon = (ImageView) mDialog.findViewById(R.id.img_recdlg_icon);
mVoice = (ImageView) mDialog.findViewById(R.id.img_recdlg_voice);
mLabel = (TextView) mDialog.findViewById(R.id.txt_recdlg_label);
mDialog.show();
}
public void recording()
{
if (mDialog != null && mDialog.isShowing())
{
mIcon.setVisibility(View.VISIBLE);
mVoice.setVisibility(View.VISIBLE);
mLabel.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.audiorec_recorder);
mLabel.setText(R.string.str_audiorecdlg_label_recording);
}
}
錄音過程中,需要動态改變顯示音量的大小,是以還需要提供一個調用方法,以改變音量值。這裡通過音量值,組成資源引用的名稱,然後加載對應的圖檔。
/**
* 更新聲音級别的圖檔
* @param level must be 1-7
*/
public void updateVoiceLevel(int level)
{
if (mDialog != null && mDialog.isShowing())
{
//通過level擷取resId
int resId = mContext.getResources().getIdentifier("audiorec_v"+level,
"drawable",mContext.getPackageName());
mVoice.setImageResource(resId);
}
}
試圖取消錄音時,需要換掉圖檔,并且隻顯示一張圖。錄音過短與之類似。
public void wangToCancel()
{
if (mDialog != null && mDialog.isShowing())
{
mIcon.setVisibility(View.VISIBLE);
mVoice.setVisibility(View.GONE);
mLabel.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.audiorec_cancel);
mLabel.setText(R.string.str_audiorecbtn_want_cancel);
}
}
public void tooShort()
{
if (mDialog != null && mDialog.isShowing())
{
mIcon.setVisibility(View.VISIBLE);
mVoice.setVisibility(View.GONE);
mLabel.setVisibility(View.VISIBLE);
mIcon.setImageResource(R.drawable.audiorec_voice_too_short);
mLabel.setText(R.string.str_audiorecdlg_label_too_short);
}
}
當然,文字也要換成對應的。
<string name="str_audiorecbtn_want_cancel">松開手指,取消發送</string>
<string name="str_audiorecdlg_label_recording">手指上滑,取消發送</string>
<string name="str_audiorecdlg_label_too_short">錄音時間過短</string>
接下來是聲音錄制子產品,使用MediaRecorder這個類實作錄音,并且向外部提供幾個方法,用于錄音過程的控制。由于我們不希望出現多個錄音的執行個體,是以這個類設為單例模式。
首先是準備錄音,這裡做一些初始化的操作,并且在完成之後要告知界面準備完畢,以便在界面上顯示正在錄音的對話框。是以,要提供一個接口,并在準備完成後調用。
public void prepareAudio() //準備
{
String strPath = MediaManager.getInstance().getStoragePath(MediaManager.MediaType.AUDIO_UPLOAD);
String fileName = "voice_"+System.currentTimeMillis()+".amr";
curFile = new File(strPath,fileName);
isPrepared = false;
recorder = new MediaRecorder();
recorder.setOutputFile(curFile.getAbsolutePath());
recorder.setAudioSource(MediaRecorder.AudioSource.MIC); //音頻源為麥克風
recorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); //輸出檔案格式
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //音頻編碼格式
try
{
recorder.prepare();
recorder.start(); //已經準備好
isPrepared = true;
if( null != listener )
{
listener.onPrepared();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
//接口
private AudioStateListener listener;
public interface AudioStateListener
{
void onPrepared(); //回調 準備完畢
}
public void setOnAudioStateListener( AudioStateListener listener)
{
this.listener = listener;
}
在錄音開始之後,需要不斷的擷取目前的音量,是以需要提供擷取音量的方法
public int getVoiceVolume( int maxLevel ) //音量等級
{
if( isPrepared )
{
try {
//振幅範圍是 1-32767
return maxLevel * recorder.getMaxAmplitude() / + ;
} catch (Exception e) {}
}
return ;
}
錄音可能被使用者取消,也可能是正常的錄制結束,是以還需要提供這兩個方法。它們的差别在于正常錄制結束時需要保留下錄音檔案,而取消錄音時不用。
public void release() //釋放
{
recorder.stop();
recorder.release();
recorder = null;
}
public void cancel() //取消
{
release();
if( null != curFile )
{
curFile.delete();
curFile = null;
}
}
最後讨論錄音按鈕,這個按鈕總共有三種狀态:未錄音時的狀态(STATE_NORMAL)、正在錄音時的狀态(STATE_RECORDING)和試圖取消錄音(STATE_WANT_TO_CANCEL)。
由于使用者的按下、移動和擡起是操作于這個Button的,是以我們需要記錄使用者的MotionEvent,并以此改變按鈕狀态。
此外,在一次錄制完成之後,需要給Button所在的Activity提供一個回調的方法,讓Activity執行後續的操作(比如上傳語音到伺服器)。
首先我們來定義按鈕的狀态和一些記錄狀态的變量
//Y方向按住移動此距離後更改狀态為試圖取消
private static final int DISTANCE_Y_CANCEL = ;
//最大聲音級别
private static final int MAX_VOLUME_LEVEL = ;
//最短錄音時長
private static final float LEAST_REC_TIME = f;
private static final int STATE_NORMAL = ;
private static final int STATE_RECORDING = ;
private static final int STATE_WANT_TO_CANCEL = ;
private int mCurState = STATE_NORMAL;
private boolean isRecording; //錄音準備是否已經完成
private boolean mReady; //是否已經進入錄音狀态
private float mTime; //計時
由于我們要接收錄音器準備完成的事件,是以我們需要實作對應的接口,并且在接口回調中顯示對話框(這裡隻寫了定義,還需要給VoiceRecorder設定上這個接口)。
對話框的顯示,這裡用了消息投遞的方法,是以還需要建立一個Handler并處理所有可能的資訊。除了對話框的顯示之外,更新目前音量和關閉對話框也是通過投遞消息來實作的。
VoiceRecorder.AudioStateListener asListener = new VoiceRecorder.AudioStateListener()
{
@Override
public void onPrepared()
{
mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
}
};
private static final int MSG_AUDIO_PREPARED = ;
private static final int MSG_VOICE_CHANGED = ;
private static final int MSG_DIALOG_DISMISS = ;
private Handler mHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MSG_AUDIO_PREPARED:
mDialogManager.showRecordingDialog();
isRecording = true;
new Thread(mGetVolume).start(); //開啟新線程,記錄錄音時間,并不斷擷取音量
break;
case MSG_VOICE_CHANGED:
mDialogManager.updateVoiceLevel(
VoiceRecorder.getInstance().getVoiceVolume(MAX_VOLUME_LEVEL));
break;
case MSG_DIALOG_DISMISS:
mDialogManager.dismissDialog();
break;
}
}
};
//擷取音量大小
private Runnable mGetVolume = new Runnable()
{
@Override
public void run()
{
while ( isRecording )
{
try
{
Thread.sleep();
mTime += f;
mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
};
接下來我們定義在不同狀态下,按鈕和對話框的更新。
private void changeState(int state)
{
if (mCurState != state)
{
mCurState = state;
switch (state)
{
case STATE_NORMAL:
setBackgroundResource(R.drawable.im_controlbar_inputbox_n);
setText(R.string.str_audiorecbtn_normal);
break;
case STATE_RECORDING:
if(mReady == false)
{
mReady = true;
VoiceRecorder.getInstance().prepareAudio();
}
setBackgroundResource(R.drawable.im_controlbar_inputbox_p);
setText(R.string.str_audiorecbtn_recording);
if (isRecording)
{
mDialogManager.recording();
}
break;
case STATE_WANT_TO_CANCEL:
setBackgroundResource(R.drawable.im_controlbar_inputbox_p);
setText(R.string.str_audiorecbtn_want_cancel);
mDialogManager.wangToCancel();
break;
default:
break;
}
}
}
然後是最關鍵的部分,通過MotionEvent來更改按鈕的目前狀态,是以要覆寫onTouchEvent方法。由于在按下之後,可能最終要取消錄音,是以需要在按下後,使用者移動手指時,獲得目前的坐标。
@Override
public boolean onTouchEvent(MotionEvent event)
{
int action = event.getAction();
int x = (int) event.getX();
int y = (int) event.getY();
switch (action)
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
當按下時,一次錄音開始,更改狀态為STATE_RECORDING
case MotionEvent.ACTION_DOWN:
reset();
changeState(STATE_RECORDING);
break;
當移動手指時,需要檢測是否已經進入或越出了試圖取消錄音的範圍,并以此來更新狀态
case MotionEvent.ACTION_MOVE:
if (isRecording)
{
//根據坐标判斷是否想要取消
if (wantToCancel(x, y))
{
changeState(STATE_WANT_TO_CANCEL);
}
else
{
changeState(STATE_RECORDING);
}
}
break;
接下來是難點,當松開手指後,需要分以下幾種情況讨論
-1、如果按下之後立刻擡起手指,狀态還沒有切換到STATE_RECORDING(雖然幾乎不可能)
-2、狀态切換到STATE_RECORDING,但是AudioRecorder還沒準備完成
-3、AudioRecorder準備完成,但是錄音時間太短
-4、正常錄音結束
-5、使用者取消錄音
據此寫出對于ACTION_UP的處理
case MotionEvent.ACTION_UP:
if(!mReady) //狀态還沒切換
{
reset();
mDialogManager.showRecordingDialog();
mDialogManager.tooShort();
mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS, );
return super.onTouchEvent(event);
}
if( !isRecording || mTime < LEAST_REC_TIME ) //prepare還沒完成 或 錄音時間太短
{
VoiceRecorder.getInstance().cancel();
if(STATE_RECORDING == mCurState)
{
mDialogManager.tooShort();
mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS,);
}
else
{
mDialogManager.dismissDialog();
}
}
else if (STATE_RECORDING == mCurState) //正常錄制結束
{
mDialogManager.dismissDialog();
VoiceRecorder.getInstance().release();
if( listener != null)
{
listener.onRecordFinish(mTime,VoiceRecorder.getInstance().getFilePath());
}
}
else if (STATE_WANT_TO_CANCEL == mCurState) //取消錄音
{
mDialogManager.dismissDialog();
VoiceRecorder.getInstance().cancel();
}
reset();
break;
前文說過,在一次錄制完成之後,需要給按鈕所在的Activity提供一個回調的方法,是以定義一個接口
//錄音完成回調接口
public interface OnRecordFinishListener
{
void onRecordFinish(float seconds, String fileName);
}
private OnRecordFinishListener listener;
public void setOnRecordFinishListener( OnRecordFinishListener listener )
{
this.listener = listener;
}
至此,錄音按鈕這個類基本上就完成了。
當然,聲音錄下來了最終是為了播放,是以我們還需要寫一個類用于播放聲音,這個用MediaPlayer實作就可以,沒什麼過多強調的,直接上代碼了。
public class MediaManager
{
private static MediaManager mInstance;
private static final String AUDIO_DIR = "/im/audio";
private static final String AUDIO_UPLOAD_DIR = "/im/audio/upload";
private MediaManager() {}
public static MediaManager getInstance()
{
if (null == mInstance)
{
synchronized (MediaManager.class)
{
if (null == mInstance)
{
mInstance = new MediaManager();
}
}
}
return mInstance;
}
private MediaPlayer mMediaPlayer;
private boolean isPause; //目前是否暫停
public String getStoragePath(MediaType type)
{
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
{
String sdcardPath = Environment.getExternalStorageDirectory().getAbsolutePath();
File dir = null;
switch (type)
{
case AUDIO:
dir = new File(sdcardPath + AUDIO_DIR);
break;
case AUDIO_UPLOAD:
dir = new File(sdcardPath + AUDIO_UPLOAD_DIR);
break;
}
if (!dir.exists())
{
dir.mkdirs();
}
return dir.getAbsolutePath();
}
else
{
return null;
}
}
public void playSound(String filePath, MediaPlayer.OnCompletionListener listener)
{
if (null == mMediaPlayer)
{
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener()
{
@Override
public boolean onError(MediaPlayer mp, int what, int extra)
{
mMediaPlayer.reset();
return false;
}
});
}
else
{
mMediaPlayer.reset();
}
try
{
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnCompletionListener(listener);
mMediaPlayer.setDataSource(filePath);
mMediaPlayer.prepare();
mMediaPlayer.start();
}
catch (IOException e)
{
e.printStackTrace();
}
}
public void pause()
{
if (null != mMediaPlayer && mMediaPlayer.isPlaying())
{
mMediaPlayer.pause();
isPause = true;
}
}
public void resume()
{
if (null != mMediaPlayer && isPause)
{
mMediaPlayer.start();
isPause = false;
}
}
public void release()
{
if (null != mMediaPlayer)
{
mMediaPlayer.release();
mMediaPlayer = null;
}
}
public enum MediaType
{
AUDIO,
AUDIO_UPLOAD,
}
}
我們在錄音按鈕那個類裡面給Activity提供了一個回調方法,Activity中隻需實作這個接口,并完成後續操作即可。
AudioRecorderButton.OnRecordFinishListener orfListener = new AudioRecorderButton.OnRecordFinishListener()
{
@Override
public void onRecordFinish(float seconds, String fileName)
{
uploadAudio(new File(fileName), Math.round(seconds), new Callback()
{
@Override
public void onFailure(Exception e)
{
}
@Override
public void onSuccess()
{
//其他操作,添加到listview等等
}
});
}
};
源碼下載下傳連結:http://download.csdn.net/detail/liusiqian0209/9265237