原文地址:http://www.2cto.com/kf/201502/378704.html
自微信出现以来取得了很好的成绩,语音对讲的实现更加方便了人与人之间的交流。今天来实践一下微信的语音对讲的录音实现,这个也比较容易实现。在 此,我将该按钮封装成为一个控件,并通过策略模式的方式实现录音和界面的解耦合,以方便我们在实际情况中对录音方法的不同需求(例如想要实现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添加权限:
123 | <code class = "hljs" xml= "" > <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" ></uses-permission></uses-permission></uses-permission></code> |
代码参考
RecordButton 类,我们的自定义控件,重新复写了onTouchEvent方法
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 | <code class = "hljs" java= "" > 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 stub init(context); } public RecordButton(Context context, AttributeSet attrs, int defStyle) { super (context, attrs, defStyle); // TODO Auto-generated constructor stub init(context); } public RecordButton(Context context, AttributeSet attrs) { super (context, attrs); // TODO Auto-generated constructor stub init(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; } // 录音时显示Dialog private 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() { @Override public void run() { recodeTime = 0 .0f; while (recordState == RECORD_ON) { { try { Thread.sleep( 100 ); recodeTime += 0.1 ; // 获取音量,更新dialog if (!isCanceled) { voiceValue = mAudioRecorder.getAmplitude(); recordHandler.sendEmptyMessage( 1 ); } } catch (InterruptedException e) { e.printStackTrace(); } } } } }; @SuppressLint (HandlerLeak) private Handler recordHandler = new Handler() { @Override public void handleMessage(Message msg) { setDialogImage(); } }; @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub switch (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); } } </code> |
Dialog布局:
12345678 | <code class = "hljs" xml= "" ><!--?xml version= 1.0 encoding=utf- 8 ?--> <linearlayout android:background= "@drawable/record_bg" android:gravity= "center" android:layout_gravity= "center" android:layout_height= "wrap_content" android:layout_width= "wrap_content" android:orientation= "vertical" android:padding= "20dp" xmlns:android= "http://schemas.android.com/apk/res/android" > <imageview android:id= "@+id/record_dialog_img" android:layout_height= "wrap_content" android:layout_width= "wrap_content" > <textview android:id= "@+id/record_dialog_txt" android:layout_height= "wrap_content" android:layout_margintop= "5dp" android:layout_width= "wrap_content" android:textcolor= "@android:color/white" > </textview></imageview></linearlayout></code> |
录音时间太短的Toast布局:
12345678 | <code class = "hljs" xml= "" ><!--?xml version= 1.0 encoding=utf- 8 ?--> <linearlayout android:background= "@drawable/record_bg" android:gravity= "center" android:layout_height= "wrap_content" android:layout_width= "wrap_content" android:orientation= "vertical" android:padding= "20dp" xmlns:android= "http://schemas.android.com/apk/res/android" > <imageview android:layout_height= "wrap_content" android:layout_width= "wrap_content" android:src= "@drawable/voice_to_short" > <textview android:layout_height= "wrap_content" android:layout_width= "wrap_content" android:text= "时间太短" android:textcolor= "@android:color/white" android:textsize= "15sp" > </textview></imageview></linearlayout></code> |
自定义的Dialogstyle,对话框样式
12345678 | <code applescript= "" class = "hljs" ><style name= "Dialogstyle" type= "text/css" ><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></code> |
RecordStrategy 录音策略接口
12345678910111213141516171819202122232425262728293031323334353637383940 | <code class = "hljs" java= "" > 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(); } </code> |
个人写的一个录音实践策略
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 | <code class = "hljs" java= "" > 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 ; @Override public void ready() { // TODO Auto-generated method stub File 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; } @Override public void start() { // TODO Auto-generated method stub if (!isRecording) { try { recorder.prepare(); recorder.start(); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } isRecording = true ; } } @Override public void stop() { // TODO Auto-generated method stub if (isRecording) { recorder.stop(); recorder.release(); isRecording = false ; } } @Override public void deleteOldFile() { // TODO Auto-generated method stub File file = new File(fileFolder + / + fileName + .amr); file.deleteOnExit(); } @Override public double getAmplitude() { // TODO Auto-generated method stub if (!isRecording) { return 0 ; } return recorder.getMaxAmplitude(); } @Override public String getFilePath() { // TODO Auto-generated method stub return fileFolder + / + fileName + .amr; } } </code> |
MainActivity
12345678910111213141516171819202122232425262728 | <code class = "hljs" java= "" > package com.example.recordtest; import android.os.Bundle; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity { RecordButton button; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = (RecordButton) findViewById(R.id.btn_record); button.setAudioRecord( new AudioRecorder()); } @Override public 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 ; } } </code> |