Android实现语音发送&播放功能以及示例代码

本文链接:https://blog.csdn.net/qq_40785165/article/details/109658968

大家好,我是小黑,一个还没秃头的程序员~~~

这是我第一次写文章,也是希望将我以后的学习经历分享给大家,希望大家喜欢!

简单的事你重复做,你就是专家;重复的事你认真做,你就是赢家。

之前在一个聊天室项目中实现了发送图片之后,我又想着实现一个发送语音的功能,包括录音、计时、播放、耳机与外放切换,先看一下效果图

可以看到发送语音的功能是由点击语音功能模块后弹出的对话框来实现的,点击开始按钮会开始录音,点击完成释放资源并上传录音到服务器,最终刷新录音列表,列表行点击事件会进行录音播放并监听耳机的连接广播。

*注:以下代码中的颜色与尺寸均为项目中定义好的,可替换成自己想要的值。

本次功能开发需要添加的权限如下:

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

(一)先定义一个对话框的样式dialog_microphone,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:background="@drawable/frame_grey_white_edge"
    android:gravity="center"
    android:orientation="vertical"
    android:paddingLeft="@dimen/b20"
    android:paddingTop="@dimen/b50"
    android:paddingRight="@dimen/b20"
    android:paddingBottom="@dimen/b50">
​
    <ImageView
        android:layout_width="@dimen/b120"
        android:layout_height="@dimen/b120"
        android:src="@mipmap/icon_microphone" />
​
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/b50"
        android:text="点击外部区域,取消发送" />
​
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/b20">
​
        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
            android:id="@+id/btn_start"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:padding="@dimen/b10"
            android:text="开始"
            android:textColor="@color/color_white"
            android:textSize="@dimen/b28"
            app:qmui_backgroundColor="@color/color_orange_main"
            app:qmui_borderColor="@color/color_white"
            app:qmui_radius="@dimen/b10" />
​
        <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton
            android:id="@+id/btn_ok"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:padding="@dimen/b10"
            android:text="完成"
            android:textColor="@color/color_white"
            android:textSize="@dimen/b28"
            app:qmui_backgroundColor="@color/color_orange_main"
            app:qmui_borderColor="@color/color_white"
            app:qmui_radius="@dimen/b10" />
​
    </LinearLayout>
</LinearLayout>

代码中QMUIRoundButton是QMUI框架的按钮控件,感兴趣的小伙伴可以自行百度了解一下,或者换成普通的按钮控件即可,frame_grey_white_edge代码如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/color_gray_e8" />
    <corners android:radius="@dimen/b20" />
    <stroke android:color="@color/color_white" />
</shape>

(二)我们可以使用MediaRecorder的Api进行录音,我将所用到的Api都整理到类--MediaHelper中去,代码如下:

public class MediaHelper {
    private MediaRecorder mMediaRecorder;
    private String mPath;//文件夹
    private String mFilePath;//文件
​
    private static MediaHelper mInstance;
​
    private MediaHelper(String path) {
        mPath = path;
    }
​
    /**
     * 准备播放后的回调
     * 这个时候文件夹里已经有文件生成了
     * 如果不去释放资源将会一直进行录音
     */
    public interface MediaStateListener {
        void preparedDone();
    }
​
    public MediaStateListener mMediaStateListener;
​
    public void setMediaStateListener(MediaStateListener mediaStateListener) {
        mMediaStateListener = mediaStateListener;
    }
​
​
    /**
     * 单例模式获取 MediaHelper
     * 双检锁/双重校验锁
     *
     * @param path
     * @return
     */
    public static MediaHelper getInstance(String path) {
        if (mInstance == null) {
            synchronized (MediaHelper.class) {
                if (mInstance == null) {
                    mInstance = new MediaHelper(path);
                }
            }
        }
​
        return mInstance;
    }
​
    /**
     * 准备录音
     */
    public void prepare() {
​
        try {
            File fileDir = new File(mPath);
            boolean b = !fileDir.exists();
            if (b) {
                fileDir.mkdirs();
            }
​
            String fileName = System.currentTimeMillis() + ".amr"; // 文件名字
            File file = new File(fileDir, fileName);  // 文件路径
​
            mMediaRecorder = new MediaRecorder();
            mFilePath = file.getAbsolutePath();
            //设置保存文件的路径
            if (Build.VERSION.SDK_INT < 26) {
                //若api低于26,调用setOutputFile(String path)
                mMediaRecorder.setOutputFile(file.getAbsolutePath());
            } else {
                //若API高于26 使用setOutputFile(File path)
                mMediaRecorder.setOutputFile(new File(file.getAbsolutePath()));
            }
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);    // 设置MediaRecorder的音频源为麦克风
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);    // 设置音频的格式
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);    // 设置音频的编码为AMR_NB
​
            mMediaRecorder.prepare();
            mMediaRecorder.start();
​
            if (mMediaStateListener != null) {
                mMediaStateListener.preparedDone();
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
​
    }
​
    /**
     * 释放资源
     */
    public void release() {
        mMediaRecorder.stop();
        mMediaRecorder.release();
        mMediaRecorder = null;
    }
​
    /**
     * 取消
     */
    public void cancel() {
        release();
        //删除相应的录音
        if (mFilePath != null) {
            File file = new File(mFilePath);
            if (file.exists()) {
                file.delete();
            }
            mFilePath = null;
        }
    }
    //获取生成的文件路径
    public String getFilePath() {
        return mFilePath;
        }
}

  (三) 对话框中通过点击相应按钮调用上述类中的相应的方法,我这里有个对话框的类叫MicrophoneDialog,代码如下:

public class MicrophoneDialog extends BaseDialog implements MediaHelper.MediaStateListener {
    public static final int EXTRA_START = 1;//开始录制
    public static final int EXTRA_UPDATE_TIME = 2;//更新时长
    private AudioListener mAudioListener;
    private long mTime;//时长
    private String mPath = Constants.APK_PATH;
    private boolean authDismiss;//是否是自动关闭的,自动关闭的不触发关闭监听
​
    private MediaHelper mMediaHelper;
    private boolean isRecording;
    private String TAG = "MicrophoneDialog";
​
    public void setAudioListener(AudioListener audioListener) {
        mAudioListener = audioListener;
    }
​
    @Override
    public void preparedDone() {
        mHandler.sendEmptyMessage(EXTRA_START);
    }
​
    public interface AudioListener {
        void finish(long time, String filePath);
​
        void cancel();
    }
​
    public void dismiss(boolean authDismiss) {
        this.authDismiss = authDismiss;
        dismiss();
    }
​
    public MicrophoneDialog(Context context) {
        super(context);
    }
​
    @Override
    public int getViewId() {
        return R.layout.dialog_microphone;
    }
​
    @Override
    public void initBasic(Bundle savedInstanceState) {
        mMediaHelper = MediaHelper.getInstance(mPath);
​
        setOnDismissListener(new OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                if (!authDismiss) {
                    mMediaHelper.release();
                    isRecording = false;
                    mTime = 0;
                    if (mAudioListener != null) {
                        mAudioListener.cancel();
                    }
                }
            }
        });
        mMediaHelper.setMediaStateListener(this);
    }
​
    @OnClick({R.id.btn_ok, R.id.btn_start})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_start:
                QToast.showToast("开始录音");
                mMediaHelper.prepare();
                break;
            case R.id.btn_ok:
                mMediaHelper.release();//要上传音频前释放资源,否则没办法上传音频
                QToast.showToast("结束录音");
                isRecording = false;
                Log.e(TAG, "onViewClicked: " + mTime + "," + mMediaHelper.getFilePath());
                if (mAudioListener != null) {
                    mAudioListener.finish(mTime / 1000, mMediaHelper.getFilePath());
                    mTime = 0;
                }
                break;
        }
    }
​
    /**
     * 这里使用Handle是为了在子线程中更新ui
     * 尽管我现在子线程只用来计时,没有更新ui
     * 但是万一以后会更新ui呢
     */
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
​
        public void handleMessage(android.os.Message msg) {
            switch (msg.what) {
                case EXTRA_START:
                    isRecording = true;
                    //开始计时
                    postDelayed(mRunnable, 1000);
                    break;
                case EXTRA_UPDATE_TIME:
                    postDelayed(mRunnable, 1000);
                    break;
​
            }
        }
    };
    /**
     * 开启个子线程计算时长
     */
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            if (isRecording) {
                mTime += 1000;
                mHandler.sendEmptyMessage(EXTRA_UPDATE_TIME);//TODO 通知修改时长显示
            }
        }
    };

上述代码中练习了Handle的使用,本来是为了在子线程中更新ui的,但是还是偷了懒,延时计时使用Thread.sleep也是可以的,用到了Butterknife框架进行控件声明以及点击事件声明,BaseDialog是封装好的基类,小伙伴们可将initBasic()方法中的代码移至onCreate()中去即可,需要注意的是上传文件之前需要先释放资源,否则会影响上传文件接口的调用,对话框做完之后就是在activity或者fragment中去显示对话框即可,录音完成之后调用相应的后台接口进行文件上传即可,随后返回录音地址,使用recyclerview开发录音列表,通过行点击进行录音播放,播放实现的效果有:

1.点击相同子项播放与重置播放,点击不同子项需要释放上一个资源重置下一个资源
2.监听有线耳机与蓝牙耳机的拔插,修改播放参数

(四)播放的代码如下:

//变量声明以及定义
private MediaPlayer mMediaPlayer;
private HeadSetReceiver mHeadSetReceiver;//广播监听
private AudioManager mAudioManager = null;
private int index;//当前点击的是不是本身
......
registerHeadsetReceiver();
mAudioManager = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);//切换耳机等播放模式
mAudioManager.setMode(AudioManager.MODE_NORMAL);//普通模式
mMediaPlayer = new MediaPlayer();//播放
......
//点击事件,先判断是否是音频类型的消息
 if (item.getMessageType() == 3) {
   if (!mMediaPlayer.isPlaying()) {//没在播放就播放
        play(item);
    } else {//同样的音频在播放就释放并重置,如果是新的音频就接着播放新的
            mMediaPlayer.reset();
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = new MediaPlayer();
            if (position != index) {
                play(item);//播放
            }
      }
   }
   index = position;
   ......
   //播放录音的方法
     private void play(MessageBean item) {
        try {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setDataSource(HttpHelper.picDomain + item.getMessage_content());//域名+文件路径
            mMediaPlayer.prepare();
            mMediaPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

(五)耳机(有线耳机&蓝牙耳机)拔插的监听以及注册监听代码如下:

 class HeadSetReceiver extends BroadcastReceiver {
​
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
                BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter();
                //记得加上蓝牙权限
                if (BluetoothHeadset.STATE_AUDIO_DISCONNECTED == defaultAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET)) {
                    QToast.showToast("耳机未连接");
                    mAudioManager.setSpeakerphoneOn(true);
                } else {
                    QToast.showToast("耳机已连接");
                    mAudioManager.setSpeakerphoneOn(false);
                }
            } else if (intent.hasExtra("state")) {
                if (intent.getIntExtra("state", 0) == 0) {
                    QToast.showToast("耳机未连接");
                    mAudioManager.setSpeakerphoneOn(true);
                } else {
                    QToast.showToast("耳机已连接");
                    mAudioManager.setSpeakerphoneOn(false);
​
                }
            }
        }
    }
    private void registerHeadsetReceiver() {
        mHeadSetReceiver = new HeadSetReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("android.intent.action.HEADSET_PLUG");
        registerReceiver(mHeadSetReceiver, intentFilter);
        IntentFilter bluetoothFilter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
        registerReceiver(mHeadSetReceiver, bluetoothFilter);
    }

(六)页面销毁时要释放资源(onDestroy)

if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        unregisterReceiver(mHeadSetReceiver);

到此为止,语音发送以及点击播放的功能就实现了,效果就是开头的两张静态图,因为没有什么花里胡哨的界面交互效果,所以就不放上gif效果了,感兴趣的小伙伴可以自己动手试一试,本项目的前后台均为本人完成,有疑惑的可以扫描下方二维码添加我微信,欢迎大家来与我交流Android前后台技术,大家共同进步!也欢迎大家订阅我的微信公众号(也是刚搞起来的),我会继续分享一些有趣的学习经历,最后,祝大家万事如意,身体健康,谢谢大家的支持与阅读!

猜你喜欢

转载自blog.csdn.net/qq_40785165/article/details/109658968