前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >仿微信语音聊天

仿微信语音聊天

作者头像
xiangzhihong
发布于 2018-02-06 11:49:22
发布于 2018-02-06 11:49:22
9.4K00
代码可运行
举报
文章被收录于专栏:向治洪向治洪
运行总次数:0
代码可运行

如上图,是常见的仿微信的聊天程序,实现的效果如上图所示,由于项目太大,本文只讲录音部分。本项目示例代码: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()方法,用于执行长按录音操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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());
        }
    }
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

RecoderButton

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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加载不同的视图,例如我的项目代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-12-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
android学习笔记----关于音频焦点Audio Focus
为了便于理解,我们以android的8.0以前的版本为例,8.0以后有一定改动,但是基本思路一样。
砖业洋__
2023/05/06
1.9K0
Android 录音功能直接拿去用
这个类可以说是这个包的核心了,如果理解了这个 Service,录音这一块基本就没什么问题了。
developerHaoz
2018/08/20
3.2K1
Android开发之声网即时通讯与讯飞语音识别相结合
声网是一家提供语音、视频即时通讯服务的公司,他的服务大多基于WebRTC开源项目并进行一些优化和修改。而讯飞语音识别应该不用多说了,老罗在发布会上介绍得已经够详细了。 那么下面进入今天的主题,就是让声网和讯飞识别同时使用,之前可能有朋友没遇到过这样的需求,那先说一下让两者同时使用会出现啥问题,为什么要做修改呢?其实原因很简单,即时通讯过程中毫无疑问肯定会用到麦克风和扬声器的,而语音识别呢,麦克风当然也是必须的了,好,那问题来了,同时有两个地方需要调用麦克风,Android系统到底要分配给谁呢?经测试,这问题
forrestlin
2018/05/24
1.3K0
实习杂记(22):仿照VideoView+MediaPlayerController做视频
主要是抽取出来,有些方法是hide,有些类是  internal层的,无法使用,所以需要自己去想办法弄,
wust小吴
2019/07/08
9230
Android简易“吹一吹实现”以及录音和播放示例
最近在做一些跟传感器相关的东西,有注意到以前腾讯微博以前出过一个吹一吹交互,虽然和传感器无关,但是感觉也比较有兴趣,就写了一个拙劣的demo,因为接触媒体文件操作比较少,顺带写了一个录音和播放的例子,总结了一下一些小坑的地方,一并在此分享给大家。 主要思路和坑的地方 主要的思路是通过MediaRecorder提供的getMaxAmplitude()函数,获取一段时间内输入的音频最大幅值来进行检测,所以除了吹的动作,其他声音也会被录进来。 “吹”这个动作如果想和其他动作进行区分,其实本质在于吹的时候靠近听筒,
NaOH
2018/05/29
1.3K0
Android多媒体之视频播放器(基于MediaPlayer)
获取帧.png 基本上也就这么多了,最后讲一下视频封面帧图片的获取:数了一下这帧大概在15秒 测试了一下秒数越大,获取图片的速度越慢,也就是越卡,所以还是给0吧 如果在Adapter里实时加载会很卡,最好查询的时候就把bitmap放到实体类里,由于封面图不要很大 别把原图给放进去了,小心直接OOM。Bitmap的操作本文就不赘述了。
张风捷特烈
2019/03/15
5.6K0
Android多媒体之视频播放器(基于MediaPlayer)
仿IOS弹出框及提示框(含Demo)
做项目经常需要用到各种各样的提示框,今天先把通用的提示框和仿IOS的弹出框写到demo里面后面会持续更新其他的。
再见孙悟空_
2023/02/10
2.1K0
仿IOS弹出框及提示框(含Demo)
Android开发笔记(一百二十六)自定义音乐播放器
在Android手机上面,音频的处理比视频还要复杂,这真是出人意料。在前面的博文《Android开发笔记(五十七)录像录音与播放》中,介绍了视频/音频的录制与播放,其中录像用的是MediaRecorder类,播放用的是MediaPlayer类。虽然Android还提供了专门的视频视图VideoView,但是该控件并非新的东西,而是继承了MediaRecorder和MediaPlayer,所以严格来说,Android上面只有一种视频的录制和播放方式。可是音频就大不一样了,Android提供了两种录音方式,以及至少三种常用的播音方式。两种录音方式分别是MediaRecorder类和AudioRecord类,而播音方式包括MediaPlayer类、AudioTrack类和SoundPool类,它们的使用场合各有千秋,且待笔者下面细细道来。 首先是MediaRecorder与MediaPlayer,这对组合即可用于录像,也可单独录制音频。它们处理的音频文件是压缩过的编码文件,通常用于录制和播放音乐,是最经常用到的。MediaRecorder与MediaPlayer在处理音频和视频时,整体流程是一样的,只有在部分方法的调用上有所差异,下面分别把录音/播音有关的方法列出来。 MediaRecorder的录音相关方法: reset : 重置录制资源 prepare : 准备录制 start : 开始录制 stop : 结束录制 release : 释放录制资源 setOnErrorListener : 设置错误监听器。可监听服务器异常以及未知错误的事件。 setOnInfoListener : 设置信息监听器。可监听录制结束事件,包括达到录制时长或者达到录制大小。 setAudioSource : 设置音频来源。一般使用麦克风AudioSource.MIC。 setOutputFormat : 设置媒体输出格式。OutputFormat.AMR_NB表示窄带格式,OutputFormat.AMR_WB表示宽带格式,AAC_ADTS表示高级的音频传输流格式。该方法要在setVideoEncoder之前调用,不然调用setAudioEncoder时会报错“java.lang.IllegalStateException”。 setAudioEncoder : 设置音频编码器。AudioEncoder.AMR_NB表示窄带编码,AudioEncoder.AMR_WB表示宽带编码,AudioEncoder.AAC表示低复杂度的高级编码,AudioEncoder.HE_AAC表示高效率的高级编码,AudioEncoder.AAC_ELD表示增强型低延迟的高级编码。 注意:setAudioEncoder应在setOutputFormat之后执行,否则会出现“setAudioEncoder called in an invalid state(2)”的异常。 setAudioSamplingRate : 设置音频的采样率,单位赫兹(Hz)。该方法为可选,AMRNB默认8khz,AMRWB默认16khz。 setAudioChannels : 设置音频的声道数。1表示单声道,2表示双声道。该方法为可选 setAudioEncodingBitRate : 设置音频每秒录制的字节数。越大则音频越清晰。该方法为可选 setMaxDuration : 设置录制时长。单位毫秒。 setMaxFileSize : 设置录制的媒体大小。单位字节。 setOutputFile : 设置输出文件的路径。 MediaPlayer的播音相关方法: reset : 重置播放器 prepare : 准备播放 start : 开始播放 pause : 暂停播放 stop : 停止播放 setOnPreparedListener : 设置准备播放监听器。 setOnCompletionListener : 设置结束播放监听器。 setOnSeekCompleteListener : 设置播放拖动监听器。 create : 创建指定Uri的播放器。 setDataSource : 设置播放数据来源。create与setDataSource只需设置其一。 setVolume : 设置音量。第一个参数是左声道,第二个参数是右声道,取值在0-1之间。 setAudioStreamType : 设置音频流的类型。AudioManager.STREAM_MUSIC表示音乐,AudioManager.STREAM_RING表示铃声,AudioManager.STREAM_ALARM表示闹钟,AudioManager.STREAM_NOTIFICATION表示通知。 setLooping : 设置是否循环播放。 i
aqi00
2019/01/18
3.1K0
android学习笔记----来看看MediaPlayer释放资源release()的使用
当查阅 MediaPlayer 文档时 你会发现这个方法setOnCompletionListener,这里的说明指出该方法允许你注册一个回调。当媒体资源或音频文件到达结束位置时会回调该方法,注意该方法的输入是OnCompletionListener
砖业洋__
2023/05/06
1.2K0
相关推荐
android学习笔记----关于音频焦点Audio Focus
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验