您的位置:首页 > 移动开发 > 微信开发

Android仿微信语音聊天功能

2015-11-13 00:16 741 查看

本文是仿照张鸿洋在慕课网的教学视频《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() / 32768 + 1;
} catch (Exception e) {}
}
return 1;
}


录音可能被用户取消,也可能是正常的录制结束,因此还需要提供这两个方法。它们的差别在于正常录制结束时需要保留下录音文件,而取消录音时不用。

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 = 50;

//最大声音级别
private static final int MAX_VOLUME_LEVEL = 7;
//最短录音时长
private static final float LEAST_REC_TIME = 1.0f;

private static final int STATE_NORMAL = 1;
private static final int STATE_RECORDING = 2;
private static final int STATE_WANT_TO_CANCEL = 3;

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 = 0x100;
private static final int MSG_VOICE_CHANGED = 0x101;
private static final int MSG_DIALOG_DISMISS = 0x102;

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(100);
mTime += 0.1f;
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, 1300);
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,1300);
}
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: