原文地址: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添加权限:
1
2
3
|
<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方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
<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布局:
1
2
3
4
5
6
7
8
|
<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布局:
1
2
3
4
5
6
7
8
|
<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,对话框样式
1
2
3
4
5
6
7
8
|
<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 录音策略接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
<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> |
个人写的一个录音实践策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
<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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<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> |