仿微信语音消息的录制和播放

来源:互联网 发布:移动宽带网络加速器 编辑:程序博客网 时间:2024/05/17 22:29

仿微信语音消息的录制和播放

一、简述

效果:

实现功能:

  1. 长按Button时改变Button显示文字,弹出Dialog(动态更新音量),动态生成录音文件,开始录音;
  2. 监听手指动作,规定区域。录音状态下手指划出规定区域取消录音,删除生成的录音文件;
  3. 监听手指动作。当手指抬起时,判断是否开始录音,录音时长是否过短,符合条件则提示录音时长过短;正常结束时通过回调返回该次录音的文件路径和时长。
    4.点击录音列表的item时,播放动画,播放对应的音频文件。

主要用到4个核心类:

  1. 自定义录音按钮(AudioRecordButton);
  2. 弹框管理类(DialogManager);
  3. 录音管理类(AudioManager)。

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封装

简述:使用MediaPlayer播放录制好的音频文件,要注意MediaPlayer资源的释放。更多关于MediaPlayer的知识参考【MediaPlayer】的使用。点击此处跳转

代码:

import android.media.*;import java.io.IOException;/** * 播放管理类 */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();            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(android.media.AudioManager.STREAM_MUSIC);            mMediaPlayer.setOnCompletionListener(onCompletionListener);            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;        }    }}

三、DialogManager封装

封装了6个方法:

1. showRecordingDialog():用来设置Diaog布局,拿到控件的引用,显示Dialog。2. recording():更改Dialog状态为录音中状态。3. wantToCancel():更改Dialog状态为想要取消状态。4. tooShort():更改Dialog状态为录音时长过短状态。5. dismissDialog():移除Dialog。6. updateVoiceLevel():用来更新音量图片。

代码:

import android.app.Dialog;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.widget.ImageView;import android.widget.TextView;import com.tiddlerliu.wxrecorder.R;/** * Dialog管理类 */public class DialogManager {    private Dialog mDialog;    private ImageView mIcon;    private ImageView mVoice;    private TextView mLabel;    private Context mContext;    public DialogManager(Context context) {        mContext = context;    }    /**     * 显示Dialog     */    public void showRecordingDialog(){        //将布局应用于Dialog        mDialog = new Dialog(mContext, R.style.Theme_AudioDialog);        LayoutInflater inflater = LayoutInflater.from(mContext);        View view = inflater.inflate(R.layout.dialog_recorder,null);        mDialog.setContentView(view);        //成员控件赋值        mIcon = (ImageView) mDialog.findViewById(R.id.recorder_dialog_icon);        mVoice = (ImageView) mDialog.findViewById(R.id.recorder_dialog_voice);        mLabel = (TextView) mDialog.findViewById(R.id.recorder_dialog_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.mipmap.recorder);            mLabel.setText("手指上滑,取消发送");        }    }    public void wantToCancel(){        if(mDialog != null && mDialog.isShowing()){            mIcon.setVisibility(View.VISIBLE);            mVoice.setVisibility(View.GONE);            mLabel.setVisibility(View.VISIBLE);            mIcon.setImageResource(R.mipmap.cancel);            mLabel.setText("松开手指,取消发送");        }    }    public void tooShort(){        if(mDialog != null && mDialog.isShowing()){            mIcon.setVisibility(View.VISIBLE);            mVoice.setVisibility(View.GONE);            mLabel.setVisibility(View.VISIBLE);            mIcon.setImageResource(R.mipmap.voice_to_short);            mLabel.setText("录音时间过短");        }    }    public void dismissDialog(){        if(mDialog != null && mDialog.isShowing()){            mDialog.dismiss();            mDialog = null;        }    }    /**     * 通过level更新音量资源图片     * @param level     */    public void updateVoiceLevel(int level){        if(mDialog != null && mDialog.isShowing()){            int resId = mContext.getResources().getIdentifier("v"+level,"mipmap",mContext.getPackageName());            mVoice.setImageResource(resId);        }    }}

四、AudioManager封装

4.1 添加必要权限

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

4.2 代码

import android.media.MediaRecorder;import java.io.File;import java.io.IOException;import java.util.UUID;/** * 录音管理类 */public class AudioManager {    private String mDir;//文件夹名称    private MediaRecorder mMediaRecorder;    private String mCurrentFilePath;//文件储存路径    private static AudioManager mInstance;    //表明MediaRecorder是否进入prepare状态(状态为true才能调用stop和release方法)    private boolean isPrepared;    public AudioManager(String dir) {        mDir = dir;    }    public String getCurrentFilePath() {        return mCurrentFilePath;    }    /**     * 准备完毕接口     */    public interface  AudioStateListener{        void wellPrepared();    }    public AudioStateListener mListener;    public void setOnAudioStateListener(AudioStateListener listener){        mListener = listener;    }    /**     * 单例     * @return AudioManager     */    public static AudioManager getInstance(String dir){        if (mInstance == null){            synchronized (AudioManager.class){                if(mInstance == null){                    mInstance = new AudioManager(dir);                }            }        }        return mInstance;    }    /**     * 准备     */    public void prepareAudio() {        try {            isPrepared = false;            File dir = new File(mDir);//创建文件夹            if (!dir.exists()) {                dir.mkdirs();            }            String fileName = generateFileName();//随机生成文件名            File file = new File(dir, fileName);//创建文件            mCurrentFilePath = file.getAbsolutePath();            mMediaRecorder = new MediaRecorder();            mMediaRecorder.setOutputFile(file.getAbsolutePath());//设置输出文件            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//设置麦克风为音频源            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);//设置音频格式            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);//设置音频编码            mMediaRecorder.prepare();            mMediaRecorder.start();            //准备结束            isPrepared = true;            if (mListener != null){                mListener.wellPrepared();            }        } catch (IOException e) {            e.printStackTrace();        }    }    /**     * 随机生成文件的名称     * @return     */    private String generateFileName() {        return UUID.randomUUID().toString()+".amr";    }    /**     * 获取音量等级     */    public int getVoiceLevel(int maxLevel) {        if (isPrepared) {            try {                //mMediaRecorder.getMaxAmplitude()  范围:1-32767                return maxLevel * mMediaRecorder.getMaxAmplitude() / 32768 + 1;//最大值 * [0,1)+ 1            } catch (Exception e) {            }        }        return 1;    }    /**     * 重置     */    public void release(){        if(mMediaRecorder != null){            mMediaRecorder.stop();            mMediaRecorder.release();            mMediaRecorder = null;        }    }    /**     * 取消     */    public void cancel(){        release();        //删除产生的文件        if(mCurrentFilePath != null){            File file = new File(mCurrentFilePath);            file.delete();            mCurrentFilePath = null;        }    }}

五、AudioRecordButton封装

import android.annotation.SuppressLint;import android.content.Context;import android.os.Environment;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.Button;import com.tiddlerliu.wxrecorder.R;/** * 自定义Button */@SuppressLint("AppCompatCustomView")public class AudioRecordButton extends Button implements AudioManager.AudioStateListener{    private static final int STATE_NORMAL = 1;//默认状态    private static final int STATE_RECORDING = 2;//录音状态    private static final int STATE_WANT_CANCEL = 3;//想取消状态    private static final int DISTANCE_Y_CANCEL = 50;//定义上滑取消距离    private int mCurState = STATE_NORMAL;//记录当前状态    private boolean isRecording = false;//是否在录音状态    private DialogManager mDialogManager;    private AudioManager mAudioManager;    private float mTime;//记录录音时长    private boolean mReady;//是否触发OnLongClick事件    private boolean isComplete = true;//是否已经完成    public AudioRecordButton(Context context) {        this(context,null);    }    public AudioRecordButton(Context context, AttributeSet attrs) {        super(context, attrs);        mDialogManager = new DialogManager(getContext());        String dir = Environment.getExternalStorageDirectory()+"/TiddlerLiu/recorder/audios";//最好判断SD卡是否存在可读        mAudioManager = AudioManager.getInstance(dir);        mAudioManager.setOnAudioStateListener(this);        setOnLongClickListener(new OnLongClickListener() {            @Override            public boolean onLongClick(View v) {                mReady = true;                mAudioManager.prepareAudio();                return false;            }        });    }    /**     * 录音完成后的回调     */    public interface AudioFinishRecorderListener {        void onFinish(float seconds,String filePath);    }    private AudioFinishRecorderListener mAudioFinishRecorderListener;    public void setAudioFinishRecorderListener(AudioFinishRecorderListener listener){        mAudioFinishRecorderListener = listener;    }    private static final int MSG_AUDIO_PREPARED = 0x110;    private static final int MSG_VOICE_CHANGED = 0x111;    private static final int MSG_DIALOG_DISMISS = 0x112;    private static final int MSG_AUDIO_COMPLETE = 0x113;//达到最大时长,自动完成    /**     * 获取音量大小     */    private Runnable mGetVoiceLevelRunnable = new Runnable() {        @Override        public void run() {            while (isRecording){                try {                    Thread.sleep(100);                    mTime += 0.1f;                    if(mTime >= 60f){//60s自动触发完成录制                        mHandler.sendEmptyMessage(MSG_AUDIO_COMPLETE);                    }                    mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    };    private Handler mHandler = new Handler(){        @Override        public void handleMessage(Message msg) {            switch (msg.what){                case MSG_AUDIO_PREPARED:                    //显示应该在audio end prepared以后                    mDialogManager.showRecordingDialog();                    isRecording = true;                    isComplete = false;                    new Thread(mGetVoiceLevelRunnable).start();                    break;                case MSG_VOICE_CHANGED:                    mDialogManager.updateVoiceLevel(mAudioManager.getVoiceLevel(7));                    break;                case MSG_DIALOG_DISMISS:                    mDialogManager.dismissDialog();                    break;                case MSG_AUDIO_COMPLETE:                    complete();                    reset();                    break;                default:                    break;            }        }    };    @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_WANT_CANCEL);                    }else{                        changeState(STATE_RECORDING);                    }                }                break;            case MotionEvent.ACTION_UP:                if(!isComplete){//没有执行超时自动完成逻辑                    if (!mReady) {//还未触发OnLongClick事件                        reset();                        return super.onTouchEvent(event);                    }                    if (!isRecording || mTime < 0.6f) {//还未开始录音  或者  录制时长过短                        mDialogManager.tooShort();                        mAudioManager.cancel();                        mHandler.sendEmptyMessageDelayed(MSG_DIALOG_DISMISS, 1300);//1.3秒后关闭对话框                    } else if (mCurState == STATE_RECORDING) {//正常录制结束                        complete();                    } else if (mCurState == STATE_WANT_CANCEL) {//想要取消状态                        mDialogManager.dismissDialog();                        mAudioManager.cancel();                    }                    reset();                }                break;        }        return super.onTouchEvent(event);    }    /**     * 正常录制结束     */    private void complete() {        mDialogManager.dismissDialog();        mAudioManager.release();        if(mAudioFinishRecorderListener != null  && !isComplete){            mAudioFinishRecorderListener.onFinish(mTime,mAudioManager.getCurrentFilePath());        }    }    /**     * 恢复状态和标志位     */    private void reset() {        isRecording = false;        mReady = false;        mTime = 0;        isComplete = true;        changeState(STATE_NORMAL);    }    /**     * 根据(x,y)坐标,判断是否想要取消     * @param x     * @param y     * @return     */    private boolean wantToCancel(int x, int y) {        if(x < 0 || x > getWidth()){//手指移出button范围            return true;        }        if(y < - DISTANCE_Y_CANCEL || y > getHeight() + DISTANCE_Y_CANCEL){//手指移出Y轴设定范围            return true;        }        return false;    }    /**     * 改变状态     * @param state     */    private void changeState(int state) {        if(mCurState != state){            mCurState = state;            switch (state){                case STATE_NORMAL:                    setBackgroundResource(R.drawable.btn_recorder_normal);                    setText(R.string.str_recorder_normal);                    break;                case STATE_RECORDING:                    setBackgroundResource(R.drawable.btn_recorder_recording);                    setText(R.string.str_recorder_recording);                    if(isRecording){                        mDialogManager.recording();                    }                    break;                case STATE_WANT_CANCEL:                    setBackgroundResource(R.drawable.btn_recorder_recording);                    setText(R.string.str_recorder_want_cancel);                    mDialogManager.wantToCancel();                    break;                default:                    break;            }        }    }}

六、 主界面实现

6.1 adapter

import android.content.Context;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.util.DisplayMetrics;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.view.WindowManager;import android.widget.ArrayAdapter;import android.widget.TextView;import com.tiddlerliu.wxrecorder.R;import com.tiddlerliu.wxrecorder.model.Recorder;import java.util.List;public class RecorderAdapter extends ArrayAdapter<Recorder>{    private int mMinItemWidth;    private int mMaxItemWidth;    private LayoutInflater mInflater;    public RecorderAdapter(@NonNull Context context, List<Recorder> datas) {        super(context, -1 ,datas);        mInflater = LayoutInflater.from(context);        //获取屏幕参数        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics outMetrics = new DisplayMetrics();        wm.getDefaultDisplay().getMetrics(outMetrics);        //设置最小宽度和最大宽度        mMinItemWidth = (int) (outMetrics.widthPixels * 0.16f);        mMaxItemWidth = (int) (outMetrics.widthPixels * 0.64f);    }    @NonNull    @Override    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {        ViewHolder holder = null;        if(convertView == null){            convertView = mInflater.inflate(R.layout.item_recorder,parent,false);            holder = new ViewHolder();            holder.seconds = (TextView) convertView.findViewById(R.id.item_recorder_time);            holder.length = convertView.findViewById(R.id.item_recorder_length);            convertView.setTag(holder);        }else {            holder = (ViewHolder) convertView.getTag();        }        //设置时长        holder.seconds.setText(Math.round(getItem(position).getTime())+ "\"");        //根据时长按比例设置时长        ViewGroup.LayoutParams lp = holder.length.getLayoutParams();        lp.width = (int) (mMinItemWidth + (mMaxItemWidth/60f * getItem(position).getTime()));        return convertView;    }    private class ViewHolder{        TextView seconds;        View length;    }}

6.2 activity

import android.graphics.drawable.AnimationDrawable;import android.media.MediaPlayer;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.view.View;import android.widget.AdapterView;import android.widget.ArrayAdapter;import android.widget.ListView;import com.tiddlerliu.wxrecorder.CustomView.AudioRecordButton;import com.tiddlerliu.wxrecorder.CustomView.MediaManager;import com.tiddlerliu.wxrecorder.adapter.RecorderAdapter;import com.tiddlerliu.wxrecorder.model.Recorder;import java.util.ArrayList;import java.util.List;public class MainActivity extends AppCompatActivity {    private ListView mListView;    private AudioRecordButton mAudioRecordButton;    private ArrayAdapter<Recorder> mAdapter ;    private List<Recorder> mDatas = new ArrayList<>();    private View mAnimView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mListView = (ListView) findViewById(R.id.recorder_list);        mAudioRecordButton = (AudioRecordButton) findViewById(R.id.recorder_button);        mAudioRecordButton.setAudioFinishRecorderListener(new AudioRecordButton.AudioFinishRecorderListener() {            @Override            public void onFinish(float seconds, String filePath) {                Recorder recorder = new Recorder(seconds,filePath);                mDatas.add(recorder);                mAdapter.notifyDataSetChanged();                mListView.setSelection(mDatas.size()-1);            }        });        mAdapter = new RecorderAdapter(this,mDatas);        mListView.setAdapter(mAdapter);        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {            @Override            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {                if(mAnimView != null){                    mAnimView.setBackgroundResource(R.mipmap.adj);                    mAnimView = null;                }                //播放动画                mAnimView = view.findViewById(R.id.item_recorder_anim);                mAnimView.setBackgroundResource(R.drawable.play_ainm);                AnimationDrawable anim = (AnimationDrawable) mAnimView.getBackground();                anim.start();                //播放音频                MediaManager.playSound(mDatas.get(position).getFilePath(), new MediaPlayer.OnCompletionListener() {                    @Override                    public void onCompletion(MediaPlayer mp) {                        mAnimView.setBackgroundResource(R.mipmap.adj);                    }                });            }        });    }    @Override    protected void onPause() {        super.onPause();        MediaManager.pause();    }    @Override    protected void onResume() {        super.onResume();        MediaManager.resume();    }    @Override    protected void onDestroy() {        super.onDestroy();        MediaManager.release();    }}
原创粉丝点击