自微信出现以来取得了很好的成绩,语音对讲的实现更加方便了人与人之间的交流。今天来实践一下微信的语音对讲的录音实现,这个也比较容易实现。在此,我将该按钮封装成为一个控件,并通过策略模式的方式实现录音和界面的解耦合,以方便我们在实际情况中对录音方法的不同需求(例如想要实现wav格式的编码时我们也就不能再使用MediaRecorder,而只能使用AudioRecord进行处理)。
效果图:
实现思路
1.在微信中我们可以看到实现语音对讲的是通过点按按钮来完成的,因此在这里我选择重新自己的控件使其继承自Button并重写onTouchEvent方法,来实现对录音的判断。
2.在onTouchEvent方法中,
当我们按下按钮时,首先显示录音的对话框,然后调用录音准备方法并开始录音,接着开启一个计时线程,每隔0.1秒的时间获取一次录音音量的大小,并通过Handler根据音量大小更新Dialog中的显示图片;
当我们移动手指时,若手指向上移动距离大于50,在Dialog中显示松开手指取消录音的提示,并将isCanceled变量(表示我们最后是否取消了录音)置为true,上移动距离小于20时,我们恢复Dialog的图片,并将isCanceled置为false;
当抬起手指时,我们首先关闭录音对话框,接着调用录音停止方法并关闭计时线程,然后我们判断是否取消录音,若是的话则删除录音文件,否则判断计时时间是否太短,最后调用回调接口中的recordEnd方法。
3.在这里为了适应不同的录音需求,我使用了策略模式来进行处理,将每一个不同的录音方法视为一种不同的策略,根据自己的需要去改写。
注意问题
1.在onTouchEvent的返回值中应该返回true,这样才能屏蔽之后其他的触摸事件,否则当手指滑动离开Button之后将不能在响应我们的触摸方法。
2.不要忘记为自己的App添加权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
代码参考
RecordButton 类,我们的自定义控件,重新复写了onTouchEvent方法
package com.example.recordtest;import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;public class RecordButton extends Button {private static final int MIN_RECORD_TIME = 1; // 最短录音时间,单位秒private static final int RECORD_OFF = 0; // 不在录音private static final int RECORD_ON = 1; // 正在录音private Dialog mRecordDialog;private RecordStrategy mAudioRecorder;private Thread mRecordThread;private RecordListener listener;private int recordState = 0; // 录音状态private float recodeTime = 0.0f; // 录音时长,如果录音时间太短则录音失败private double voiceValue = 0.0; // 录音的音量值private boolean isCanceled = false; // 是否取消录音private float downY;private TextView dialogTextView;private ImageView dialogImg;private Context mContext;public RecordButton(Context context) {super(context);// TODO Auto-generated constructor stubinit(context);}public RecordButton(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);// TODO Auto-generated constructor stubinit(context);}public RecordButton(Context context, AttributeSet attrs) {super(context, attrs);// TODO Auto-generated constructor stubinit(context);}private void init(Context context) {mContext = context;this.setText("按住 说话");}public void setAudioRecord(RecordStrategy record) {this.mAudioRecorder = record;}public void setRecordListener(RecordListener listener) {this.listener = listener;}// 录音时显示Dialogprivate void showVoiceDialog(int flag) {if (mRecordDialog == null) {mRecordDialog = new Dialog(mContext, R.style.Dialogstyle);mRecordDialog.setContentView(R.layout.dialog_record);dialogImg = (ImageView) mRecordDialog.findViewById(R.id.record_dialog_img);dialogTextView = (TextView) mRecordDialog.findViewById(R.id.record_dialog_txt);}switch (flag) {case 1:dialogImg.setImageResource(R.drawable.record_cancel);dialogTextView.setText("松开手指可取消录音");this.setText("松开手指 取消录音");break;default:dialogImg.setImageResource(R.drawable.record_animate_01);dialogTextView.setText("向上滑动可取消录音");this.setText("松开手指 完成录音");break;}dialogTextView.setTextSize(14);mRecordDialog.show();}// 录音时间太短时Toast显示private void showWarnToast(String toastText) {Toast toast = new Toast(mContext);View warnView = LayoutInflater.from(mContext).inflate(R.layout.toast_warn, null);toast.setView(warnView);toast.setGravity(Gravity.CENTER, 0, 0);// 起点位置为中间toast.show();}// 开启录音计时线程private void callRecordTimeThread() {mRecordThread = new Thread(recordThread);mRecordThread.start();}// 录音Dialog图片随录音音量大小切换private void setDialogImage() {if (voiceValue < 600.0) {dialogImg.setImageResource(R.drawable.record_animate_01);} else if (voiceValue > 600.0 && voiceValue < 1000.0) {dialogImg.setImageResource(R.drawable.record_animate_02);} else if (voiceValue > 1000.0 && voiceValue < 1200.0) {dialogImg.setImageResource(R.drawable.record_animate_03);} else if (voiceValue > 1200.0 && voiceValue < 1400.0) {dialogImg.setImageResource(R.drawable.record_animate_04);} else if (voiceValue > 1400.0 && voiceValue < 1600.0) {dialogImg.setImageResource(R.drawable.record_animate_05);} else if (voiceValue > 1600.0 && voiceValue < 1800.0) {dialogImg.setImageResource(R.drawable.record_animate_06);} else if (voiceValue > 1800.0 && voiceValue < 2000.0) {dialogImg.setImageResource(R.drawable.record_animate_07);} else if (voiceValue > 2000.0 && voiceValue < 3000.0) {dialogImg.setImageResource(R.drawable.record_animate_08);} else if (voiceValue > 3000.0 && voiceValue < 4000.0) {dialogImg.setImageResource(R.drawable.record_animate_09);} else if (voiceValue > 4000.0 && voiceValue < 6000.0) {dialogImg.setImageResource(R.drawable.record_animate_10);} else if (voiceValue > 6000.0 && voiceValue < 8000.0) {dialogImg.setImageResource(R.drawable.record_animate_11);} else if (voiceValue > 8000.0 && voiceValue < 10000.0) {dialogImg.setImageResource(R.drawable.record_animate_12);} else if (voiceValue > 10000.0 && voiceValue < 12000.0) {dialogImg.setImageResource(R.drawable.record_animate_13);} else if (voiceValue > 12000.0) {dialogImg.setImageResource(R.drawable.record_animate_14);}}// 录音线程private Runnable recordThread = new Runnable() {@Overridepublic void run() {recodeTime = 0.0f;while (recordState == RECORD_ON) {{try {Thread.sleep(100);recodeTime += 0.1;// 获取音量,更新dialogif (!isCanceled) {voiceValue = mAudioRecorder.getAmplitude();recordHandler.sendEmptyMessage(1);}} catch (InterruptedException e) {e.printStackTrace();}}}}};@SuppressLint("HandlerLeak")private Handler recordHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {setDialogImage();}};@Overridepublic boolean onTouchEvent(MotionEvent event) {// TODO Auto-generated method stubswitch (event.getAction()) {case MotionEvent.ACTION_DOWN: // 按下按钮if (recordState != RECORD_ON) {showVoiceDialog(0);downY = event.getY();if (mAudioRecorder != null) {mAudioRecorder.ready();recordState = RECORD_ON;mAudioRecorder.start();callRecordTimeThread();}}break;case MotionEvent.ACTION_MOVE: // 滑动手指float moveY = event.getY();if (downY - moveY > 50) {isCanceled = true;showVoiceDialog(1);}if (downY - moveY < 20) {isCanceled = false;showVoiceDialog(0);}break;case MotionEvent.ACTION_UP: // 松开手指if (recordState == RECORD_ON) {recordState = RECORD_OFF;if (mRecordDialog.isShowing()) {mRecordDialog.dismiss();}mAudioRecorder.stop();mRecordThread.interrupt();voiceValue = 0.0;if (isCanceled) {mAudioRecorder.deleteOldFile();} else {if (recodeTime < MIN_RECORD_TIME) {showWarnToast("时间太短 录音失败");mAudioRecorder.deleteOldFile();} else {if (listener != null) {listener.recordEnd(mAudioRecorder.getFilePath());}}}isCanceled = false;this.setText("按住 说话");}break;}return true;}public interface RecordListener {public void recordEnd(String filePath);}
}
Dialog布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:gravity="center"android:background="@drawable/record_bg" android:padding="20dp" ><ImageView
android:id="@+id/record_dialog_img"android:layout_width="wrap_content"android:layout_height="wrap_content" /><TextView
android:id="@+id/record_dialog_txt"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textColor="@android:color/white"android:layout_marginTop="5dp" /></LinearLayout>
录音时间太短的Toast布局:
<?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:background="@drawable/record_bg"android:padding="20dp"android:gravity="center"android:orientation="vertical" ><ImageView
android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/voice_to_short" /><TextView
android:layout_width="wrap_content"android:layout_height="wrap_content"android:textColor="@android:color/white"android:textSize="15sp"android:text="时间太短 录音失败" /></LinearLayout>
自定义的Dialogstyle,对话框样式
<style name="Dialogstyle"><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:windowAnimationStyle">@android:style/Animation.Dialog</item><!-- 显示对话框时当前的屏幕是否变暗 --><item name="android:backgroundDimEnabled">false</item></style>
RecordStrategy 录音策略接口
package com.example.recordtest;/*** RecordStrategy 录音策略接口* @author acer*/
public interface RecordStrategy {/*** 在这里进行录音准备工作,重置录音文件名等*/public void ready();/*** 开始录音*/public void start();/*** 录音结束*/public void stop();/*** 录音失败时删除原来的旧文件*/public void deleteOldFile();/*** 获取录音音量的大小* @return */public double getAmplitude();/*** 返回录音文件完整路径* @return*/public String getFilePath();}
个人写的一个录音实践策略
package com.example.recordtest;import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;import android.media.MediaRecorder;
import android.os.Environment;public class AudioRecorder implements RecordStrategy {private MediaRecorder recorder;private String fileName;private String fileFolder = Environment.getExternalStorageDirectory().getPath() + "/TestRecord";private boolean isRecording = false;@Overridepublic void ready() {// TODO Auto-generated method stubFile file = new File(fileFolder);if (!file.exists()) {file.mkdir();}fileName = getCurrentDate();recorder = new MediaRecorder();recorder.setOutputFile(fileFolder + "/" + fileName + ".amr");recorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置MediaRecorder的音频源为麦克风recorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);// 设置MediaRecorder录制的音频格式recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);// 设置MediaRecorder录制音频的编码为amr}// 以当前时间作为文件名private String getCurrentDate() {SimpleDateFormat formatter = new SimpleDateFormat("yyyy_MM_dd_HHmmss");Date curDate = new Date(System.currentTimeMillis());// 获取当前时间String str = formatter.format(curDate);return str;}@Overridepublic void start() {// TODO Auto-generated method stubif (!isRecording) {try {recorder.prepare();recorder.start();} catch (IllegalStateException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}isRecording = true;}}@Overridepublic void stop() {// TODO Auto-generated method stubif (isRecording) {recorder.stop();recorder.release();isRecording = false;}}@Overridepublic void deleteOldFile() {// TODO Auto-generated method stubFile file = new File(fileFolder + "/" + fileName + ".amr");file.deleteOnExit();}@Overridepublic double getAmplitude() {// TODO Auto-generated method stubif (!isRecording) {return 0;}return recorder.getMaxAmplitude();}@Overridepublic String getFilePath() {// TODO Auto-generated method stubreturn fileFolder + "/" + fileName + ".amr";}}
MainActivity
package com.example.recordtest;import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;public class MainActivity extends Activity {RecordButton button;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);button = (RecordButton) findViewById(R.id.btn_record);button.setAudioRecord(new AudioRecorder());}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.main, menu);return true;}}
源码下载
点击下载源码