仿微信语音聊天

2021-02-20 11:14发布

如上图,是常见的仿微信的聊天程序,实现的效果如上图所示,由于项目太大,本文只讲录音部分。本项目示例代码:https://github.com/xiangzhihong/weixinAudio

主要用到4个核心类:
自定义录音按钮(RecoderButton);
弹框管理类(RecorderDialog);
录音管理类(AudioManager);
录音播放类(MediaManager)。
其中
1.AudioRecordButton状态:
1.STATE_NORMAL:普通状态
2.STATE_RECORDING:录音中
3.STATE_CANCEL:取消录音
2.DialogManager状态:
1.RECORDING:录音中
2.WANT_TO_CANCEL:取消录音
3.TOO_SHORT:录音时间太短
3.AudioManager:
1.prepare():准备状态
2.cancel():取消录音
3.release():正常结束录音
4.getVoiceLevel():获取音量

代码实现

自定义Button,重写onTouchEvent()方法,用于执行长按录音操作。

class AudioRecorderButton{
 onTouchEvent(){
 DOWN:
  changeButtonState(STATE_RECORDING);
          | DialogManager.showDialog(RECORDING)
  触发LongClick事件(AudioManager.prepare() --> end prepared --> |       );
          | getVoiceLevel();//开启一个线程,更新Dialog上的音量等级 
 MOVE:
  if(wantCancel(x,y)){
  DialogManager.showDialog(WANT_TO_CANCEL);更新Dialog
  changeButtonState(STATE_WANT_TO_CANCEL);更新Button状态
  }else{
  DialogManager.showDialog(WANT_TO_CANCEL);
  changeButtonState(STATE_RECORDING);
  }
  UP:
  if(wantCancel == curState){//当前状态是想取消状态
  AudioManager.cancel();
  }
  if(STATE_RECORDING = curState){
  if(tooShort){//判断录制时长,如果录制时间过短
   DialogManager.showDialog(TOO_SHORT);
  }
  AudioManager.release();
  callbackActivity(url,time);//(当前录音文件路径,时长)
  }
 }
}

相关的逻辑请查看项目源码。

MediaManager

public class MediaManager {

    private static MediaPlayer mMediaPlayer;

    private static boolean isPause;

    public static void playSound(String filePath,
                                 MediaPlayer.OnCompletionListener onCompletionListener) {
        if(mMediaPlayer == null){
            mMediaPlayer = new MediaPlayer();
        }else {
            mMediaPlayer.reset();
        }
        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setOnCompletionListener(onCompletionListener);
        try {
            mMediaPlayer.setDataSource(filePath);
            mMediaPlayer.prepare();
            mMediaPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void pause(){
        if(mMediaPlayer != null && mMediaPlayer.isPlaying()){
            mMediaPlayer.pause();
            isPause = true;
        }
    }
    public static void resume(){
        if(mMediaPlayer != null && isPause){
            mMediaPlayer.start();
            isPause = false;
        }
    }
    public static void release(){
        if(mMediaPlayer != null){
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }

}

RecorderDialog

录音弹窗类,主要包含录音的各种状态及弹窗。源码如下:

public class RecorderDialog {

    private Dialog mDialog;
    private ImageView mIcon;
    private TextView mLable;
    private TextView mLeftTime;
    private Context mContext;

    public RecorderDialog(Context context) {
        this.mContext = context;
    }

    public void showRecordingDialog() {
        LayoutInflater inflater = LayoutInflater.from(mContext);
        View view = inflater.inflate(R.layout.dialog_recorder, null);
        mDialog = new Dialog(mContext, R.style.style_dialog);
        mDialog.setContentView(view);
        mIcon = (ImageView) mDialog.findViewById(R.id.recorder_dialog_icon);
        mLable = (TextView) mDialog.findViewById(R.id.recoder_dialog_label);
        mLeftTime=(TextView) mDialog.findViewById(R.id.recoder_leftTime);
        mDialog.show();
    }

    public void recording() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setImageResource(R.mipmap.recorder_icon);
            mLable.setText("手指上滑,取消发送");
        }
    }

    public void wantToCancel() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setImageResource(R.mipmap.cancel_recorder_icon);
            mLable.setText("松开手指,取消发送");
        }
    }

    public void tooShort() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setImageResource(R.mipmap.voice_to_short);
            mLable.setText("录音时间过短");
        }
    }

    //倒计时提示(10-->0)
    public void recoderConfirm(int time) {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.GONE);
            mLeftTime.setVisibility(View.VISIBLE);
            mLeftTime.setText(time+"");
            mLable.setText("松开手指,取消发送");
        }
    }

    public void dimissDialog() {
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss();
            mDialog = null;
        }
    }

    public void setVoiceLevel(int level) {
        if (mDialog != null && mDialog.isShowing()) {
            //用switch冗余
            int resId = mContext.getResources().getIdentifier("v"+level,"mipmap",mContext.getPackageName());
        }
    }
}

由于需要用到权限系统,所以需要在配置文件中添加相关的权限。

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

RecoderButton

自定义录音按钮,录音的一切判断都在这个文件。

public class RecoderButton extends TextView implements AudioManager.AudioStateListener {

    private static final int DISANCE_Y_CANCEL = 50;
    private static final int STATE_RECORDER_NORMAL = 1; //正常
    private static final int STATE_RECORDING = 2;  //正在录制
    private static final int STATE_CANCEL = 3;  //取消
    private static final int MSG_AUDIO_PREPARED = 0X10;
    private static final int MSG_AUDIO_CHANGED = 0X11;
    private static final int MSG_AUDIO_DIMISS = 0X12;
    private static final int MSG_AUDIO_TIME_OUT = 0X13;
    private boolean mReady;
    private int mCurState = STATE_RECORDER_NORMAL;
    private boolean isRecording = false;//正在录音
    private float maxTime=10;//最大录制时长
    private float mTime=0;//录制时长
    private Timer timer = new Timer();
    private int leftTime=10;//录音倒计时,10开始提示
    private RecorderDialog dialog=null;
    private AudioManager audioManager=null;
    private FinishRecorderListener mListener;

    public RecoderButton(Context context) {
        this(context, null);
    }

    public RecoderButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initDialog();
        initClick();
    }

    private void initClick() {
        setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                mReady = true;
                audioManager.prepareAudio();
                return false;
            }
        });
    }

    private void initDialog() {
        dialog = new RecorderDialog(getContext());
        String dir = Environment.getExternalStorageDirectory() + "/recorder";//创建文件夹
        audioManager = AudioManager.getInstance(dir);
        audioManager.setOnAudioStateListener(this);
    }

    /**
     * 获取音量大小
     */
    private Runnable mGetVoiceLeveelRunnable = new Runnable() {
        @Override
        public void run() {
            while (isRecording) {
                try {
                    Thread.sleep(100);
                    mTime += 0.1f;
                    mHandler.sendEmptyMessage(MSG_AUDIO_CHANGED);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_AUDIO_PREPARED:
                    dialog.showDialog();
                    isRecording = true;
                    new Thread(mGetVoiceLeveelRunnable).start();
                    break;
                case MSG_AUDIO_CHANGED:
                    dialog.voiceLevel(audioManager.getVoiceLevel(7));
                    System.out.println("录音时间:"+mTime);
                    if (mTime>maxTime){
                       confirmTimer();
                    }
                    break;
                case MSG_AUDIO_DIMISS:
                    dialog.dimissDialog();
                    break;
            }
        }
    };

    //录音倒计时
    private Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            leftTime--;
            if (leftTime<=0){
                dialog.dimissDialog();
                audioManager.release();
                if (mListener != null) {
                    mListener.onFinish(mTime, audioManager.getCurrentFilePath());
                }
                return;
            }
            dialog.recoderConfirm(leftTime);
        }
    };

    @Override
    public void wellPrepared() {
        mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
    }

    @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:
                changeState(STATE_RECORDING);
                break;
            case MotionEvent.ACTION_MOVE:
                if (isRecording) {
                    //根据x y的坐标判断是否想取消
                    if (wantToCancel(x, y)) {
                        changeState(STATE_CANCEL);
                    } else {
                        changeState(STATE_RECORDING);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!mReady) {
                    reset();
                    return super.onTouchEvent(event);
                }
                if (!isRecording || mTime < 1.0f) {
                    System.out.println("录音时间过短");
                    dialog.recoderShort();
                    audioManager.cancel();
                    mHandler.sendEmptyMessageDelayed(MSG_AUDIO_DIMISS, 1300);
                } else if (mCurState == STATE_RECORDING) {
                    System.out.println("正常录制");
                    dialog.dimissDialog();
                    audioManager.release();
                    if (mListener != null) {
                        mListener.onFinish(mTime, audioManager.getCurrentFilePath());
                    }
                } else if (mCurState == STATE_CANCEL) {
                    System.out.println("取消了");
                    dialog.dimissDialog();
                    audioManager.cancel();
                }
                reset();
                break;

        }
        return super.onTouchEvent(event);
    }

    //恢复状态及标志位
    private void reset() {
        isRecording = false;
        mReady = false;
        mTime = 0;
        changeState(STATE_RECORDER_NORMAL);
    }

    private boolean wantToCancel(int x, int y) {
        if (x < 0 || x > getWidth()) {//判断手指的横坐标是否超出按钮的范围
            return true;
        }
        //再判断Y
        if (y < -DISANCE_Y_CANCEL || y > getHeight() + DISANCE_Y_CANCEL) {//按钮上部或下部
            return true;
        }
        return false;
    }

    //随着状态的改变,文本颜色和背景改变
    private void changeState(int state) {
        if (mCurState != state) {
            mCurState = state;
            switch (state) {
                case STATE_RECORDER_NORMAL:
                    setBackgroundResource(R.drawable.recentgle_gray_border);
                    setText("按住 说话");
                    break;
                case STATE_RECORDING:
                    setBackgroundResource(R.drawable.recentgle_gray);
                    setText("松开结束");
                    if (isRecording) {
                        dialog.recording();
                    }
                    break;
                case STATE_CANCEL:
                    setBackgroundResource(R.drawable.recentgle_gray);
                    setText("松开手指,取消发送");
                    dialog.cancelRecorder();
                    break;
            }
        }
    }

    //倒计时定时器
    private void confirmTimer() {
        timer.schedule(new TimerTask() {
            @Override public void run() {
                try {
                    Thread.sleep(1000);
                    handler.sendEmptyMessage(MSG_AUDIO_TIME_OUT);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }, 0, 1000);
    }

    public interface FinishRecorderListener {
        void onFinish(float seconds, String filePath);
    }

    public void setRecorderListener(FinishRecorderListener listener) {
        mListener = listener;
    }
}

最后录制完成后,点击列表的语音会完成播放功能。

MediaManager

public class MediaManager {

    public static MediaManager instance=null;
    private  MediaPlayer mMediaPlayer=null;
    private static boolean isPause=false;

    public static synchronized MediaManager getInstance() {
        if (instance == null) {
            instance = new MediaManager();
        }
        return instance;
    }

    public  void playSound(String filePath){
        playSound(filePath,null);
    }

    public void playSound(String filePath,
                                 MediaPlayer.OnCompletionListener onCompletionListener) {
        if(mMediaPlayer == null){
            mMediaPlayer = new MediaPlayer();
        }else {
            mMediaPlayer.reset();
        }
        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setOnCompletionListener(onCompletionListener);
        try {
            mMediaPlayer.setDataSource(filePath);
            mMediaPlayer.prepare();
            mMediaPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public  void pause(){
        if(mMediaPlayer != null && mMediaPlayer.isPlaying()){
            mMediaPlayer.pause();
            isPause = true;
        }
    }

    public  void resume(){
        if(mMediaPlayer != null && isPause){
            mMediaPlayer.start();
            isPause = false;
        }
    }

    public  void release(){
        if(mMediaPlayer != null){
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }
}

对于聊天列表,是一个比较复杂的逻辑,开发的时候可以重写getItemViewType函数,然后不同的ViewType加载不同的视图,例如我的项目代码如下:

 ChatItem struct = getItem(position);
        switch (struct.chatType) {
            case ChatItem.CHAT_TYPE_TIME:
                return initTimeView(convertView, parent, (String) struct.data);
            case ChatItem.CHAT_TYPE_GROUP_TIP:
                return initTipView(convertView, parent, (String) struct.data);
            case ChatItem.CHAT_TYPE_OUT_TEXT:
                return initOutTextView(convertView, parent, (Message) struct.data);
            case ChatItem.CHAT_TYPE_IN_TEXT:
                return initInTextView(convertView, parent, (Message) struct.data);
            case ChatItem.CHAT_TYPE_OUT_IMAGE:
                return initOutImageView(convertView, parent, (Message) struct.data);
            case ChatItem.CHAT_TYPE_IN_IMAGE:
                return initInImageView(convertView, parent, (Message) struct.data);
            case ChatItem.CHAT_TYPE_OUT_AUDIO:
                return initOutAudioView(convertView, parent, (Message) struct.data);
            case ChatItem.CHAT_TYPE_BONUS_NOTICE:
                return initBonusNotice(convertView, parent, (Message) struct.data);
            case ChatItem.CHAT_TYPE_RECOMMEND:
                return initRecommendType(convertView, parent, (Message) struct.data);
            default:
                return new View(context);
        }

如果,你想了解mp3格式相关的内容,可以查看下面的链接:http://blog.csdn.net/omrapollo/article/details/50470659

本文同步分享在 博客“xiangzhihong8”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签: