├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nan │ │ └── recordbutton │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nan │ │ │ └── recordbutton │ │ │ ├── Constants.java │ │ │ ├── MainActivity.java │ │ │ ├── utils │ │ │ ├── AudioRecordManager.java │ │ │ └── VibratorUtils.java │ │ │ └── widget │ │ │ ├── HorVoiceView.java │ │ │ └── RecorderButton.java │ └── res │ │ ├── drawable-xhdpi │ │ ├── bg_record.png │ │ ├── bg_recording.png │ │ ├── iv_bg.jpg │ │ └── wave.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── raw │ │ ├── fx.mp3 │ │ ├── fy.mp3 │ │ └── gj.mp3 │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── nan │ └── recordbutton │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.8 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ###一个绚丽的录音按钮,带有水波纹效果,这是从真实项目里面挖下来的。 2 | 由于时间匆促,代码还有待重构。 3 | 4 | ###效果: 5 | 6 | ![](http://i.imgur.com/pwbTBOA.png) 7 | 8 | ![](http://i.imgur.com/3de5MiJ.png) 9 | 10 | ![](http://i.imgur.com/pMsuzE4.gif) 11 | 12 | 13 | ###使用方法: 14 | 15 | 1. 在布局文件中添加 16 | 17 | 33 | 34 | 2. 在Activity中找到RecorderButton以后记得设置监听器RecorderButton.AudioStateRecorderListener,从而实现自己的业务逻辑。具体可以参考项目中的代码。 35 | 36 | private RecorderButton btn_record; 37 | 38 | btn_record = (RecorderButton) findViewById(R.id.btn_record); 39 | btn_record.setAudioStateRecorderListener(new MyRecordListener()); 40 | 41 | class MyRecordListener implements RecorderButton.AudioStateRecorderListener { 42 | 43 | @Override 44 | public void onStart(float time) { 45 | } 46 | 47 | @Override 48 | public void onUpdateTime(float currentTime, float minTime, float maxTime){ 49 | } 50 | 51 | @Override 52 | public void onReturnToRecord() { 53 | } 54 | 55 | @Override 56 | public void onWantToCancel() { 57 | } 58 | 59 | @Override 60 | public void onFinish(float seconds, String filePath) { 61 | } 62 | 63 | @Override 64 | public void onCancel(boolean isTooShort) { 65 | } 66 | 67 | @Override 68 | public void onVoiceChange(int voiceLevel) { 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "25.0.0" 6 | defaultConfig { 7 | applicationId "com.nan.recordbutton" 8 | minSdkVersion 14 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:24.2.1' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Users\Administrator\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/nan/recordbutton/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.nan.recordbutton", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/nan/recordbutton/Constants.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton; 2 | 3 | import android.os.Environment; 4 | 5 | import java.io.File; 6 | 7 | public class Constants { 8 | 9 | public static final class FilePath { 10 | public static final String RECORD_TEMP_PATH = Environment.getExternalStorageDirectory() + File.separator + "com.sxbb" + File.separator + "recordTemp" + File.separator; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/nan/recordbutton/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.media.MediaRecorder; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.os.Handler; 11 | import android.os.Message; 12 | import android.support.annotation.NonNull; 13 | import android.support.v4.app.ActivityCompat; 14 | import android.support.v4.content.ContextCompat; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.view.View; 17 | import android.view.animation.AlphaAnimation; 18 | import android.view.animation.Animation; 19 | import android.view.animation.AnimationSet; 20 | import android.view.animation.ScaleAnimation; 21 | import android.widget.ImageView; 22 | import android.widget.TextView; 23 | import android.widget.Toast; 24 | 25 | import com.nan.recordbutton.widget.RecorderButton; 26 | import com.nan.recordbutton.widget.HorVoiceView; 27 | 28 | import java.io.File; 29 | 30 | public class MainActivity extends AppCompatActivity { 31 | 32 | private RecorderButton btn_record; 33 | private TextView tv_state; 34 | private TextView tv_time; 35 | private HorVoiceView hv_voice; 36 | private String str_record1; 37 | private String str_record2; 38 | 39 | private static final int MAX_RECORD_TIME = 15; 40 | 41 | private ImageView mWave1; 42 | private ImageView mWave2; 43 | private ImageView mWave3; 44 | private AnimationSet mAnimationSet1; 45 | private AnimationSet mAnimationSet2; 46 | private AnimationSet mAnimationSet3; 47 | private static final int OFFSET = 600; //每个动画的播放时间间隔 48 | private static final int MSG_WAVE2_ANIMATION = 2; 49 | private static final int MSG_WAVE3_ANIMATION = 3; 50 | // 这个标志用于防止 51 | private boolean isShowingWave = false; 52 | 53 | private boolean isRecordEnable = false; 54 | 55 | @SuppressLint("HandlerLeak") 56 | private Handler mHandler = new Handler() { 57 | @Override 58 | public void handleMessage(Message msg) { 59 | switch (msg.what) { 60 | case MSG_WAVE2_ANIMATION: 61 | if (isShowingWave) { 62 | mWave2.startAnimation(mAnimationSet2); 63 | } else { 64 | mWave2.clearAnimation(); 65 | } 66 | break; 67 | case MSG_WAVE3_ANIMATION: 68 | if (isShowingWave) { 69 | mWave3.startAnimation(mAnimationSet3); 70 | } else { 71 | mWave3.clearAnimation(); 72 | } 73 | break; 74 | } 75 | } 76 | }; 77 | private ImageView iv_bg; 78 | private Context mCtx; 79 | private MediaRecorder mMediaRecorder; 80 | private TextView tv_txt0; 81 | 82 | @Override 83 | protected void onCreate(Bundle savedInstanceState) { 84 | super.onCreate(savedInstanceState); 85 | setContentView(R.layout.activity_main); 86 | 87 | findView(); 88 | init(); 89 | 90 | } 91 | 92 | public static final int CODE_REQUEST_RECORD = 100; 93 | 94 | /** 95 | * android6.0权限管理 96 | */ 97 | private void checkRecordPermission() { 98 | 99 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 100 | if (ContextCompat.checkSelfPermission(this, 101 | Manifest.permission.CALL_PHONE) 102 | != PackageManager.PERMISSION_GRANTED) { 103 | 104 | ActivityCompat.requestPermissions(this, 105 | new String[]{Manifest.permission.CALL_PHONE}, 106 | CODE_REQUEST_RECORD); 107 | } 108 | } 109 | } 110 | 111 | @Override 112 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 113 | 114 | if (requestCode == CODE_REQUEST_RECORD) { 115 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 116 | 117 | Toast.makeText(MainActivity.this, "成功授权录音权限", Toast.LENGTH_SHORT).show(); 118 | 119 | } else { 120 | // Permission Denied 121 | Toast.makeText(MainActivity.this, "没有录音权限", Toast.LENGTH_SHORT).show(); 122 | } 123 | return; 124 | } 125 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 126 | } 127 | 128 | 129 | @Override 130 | protected void onDestroy() { 131 | super.onDestroy(); 132 | 133 | if (mMediaRecorder != null) { 134 | mMediaRecorder.release(); 135 | } 136 | } 137 | 138 | private void findView() { 139 | iv_bg = (ImageView) findViewById(R.id.iv_bg); 140 | mWave1 = (ImageView) findViewById(R.id.wave1); 141 | mWave2 = (ImageView) findViewById(R.id.wave2); 142 | mWave3 = (ImageView) findViewById(R.id.wave3); 143 | hv_voice = (HorVoiceView) findViewById(R.id.hv_voice); 144 | btn_record = (RecorderButton) findViewById(R.id.btn_record); 145 | tv_state = (TextView) findViewById(R.id.tv_state); 146 | tv_time = (TextView) findViewById(R.id.tv_time); 147 | tv_txt0 = (TextView) findViewById(R.id.tv_txt0); 148 | } 149 | 150 | private void init() { 151 | 152 | mCtx = this; 153 | 154 | checkRecordPermission(); 155 | // TintBarUtils.setStatusBarTextStyle(this, true); 156 | // immerseLayout(); 157 | 158 | mAnimationSet1 = initAnimationSet(); 159 | mAnimationSet2 = initAnimationSet(); 160 | mAnimationSet3 = initAnimationSet(); 161 | 162 | str_record1 = getResources().getString(R.string.record_txt4); 163 | str_record2 = getResources().getString(R.string.record_txt5); 164 | updateTime(MAX_RECORD_TIME); 165 | 166 | btn_record.setAudioStateRecorderListener(new MyRecordListener()); 167 | } 168 | 169 | /** 170 | * 上传录音 171 | * 需要修改URL 172 | * 173 | * @param seconds 语音的时间长度 174 | * @param recordFile 录音文件 175 | */ 176 | private void uploadRecord(final int seconds, final File recordFile) { 177 | 178 | String ts = System.currentTimeMillis() + ""; 179 | 180 | //...... 181 | } 182 | 183 | 184 | /** 185 | * 更新时间 186 | * 187 | * @param time 更新显示的时间 188 | */ 189 | private void updateTime(int time) { 190 | tv_time.setText(str_record1 + time + str_record2); 191 | } 192 | 193 | /** 194 | * 文案的还原 195 | */ 196 | private void resetTextAndTime() { 197 | 198 | tv_state.setText(R.string.record_normal); 199 | updateTime(MAX_RECORD_TIME); 200 | 201 | } 202 | 203 | private AnimationSet initAnimationSet() { 204 | AnimationSet as = new AnimationSet(true); 205 | ScaleAnimation sa = new ScaleAnimation(1f, 3.5f, 1f, 3.5f, ScaleAnimation.RELATIVE_TO_SELF, 0.5f, ScaleAnimation.RELATIVE_TO_SELF, 0.5f); 206 | sa.setDuration(OFFSET * 3); 207 | sa.setRepeatCount(Animation.INFINITE);// 设置循环 208 | AlphaAnimation aa = new AlphaAnimation(1, 0.1f); 209 | aa.setDuration(OFFSET * 3); 210 | aa.setRepeatCount(Animation.INFINITE);//设置循环 211 | as.addAnimation(sa); 212 | as.addAnimation(aa); 213 | return as; 214 | } 215 | 216 | private void startWaveAnimation() { 217 | mWave1.startAnimation(mAnimationSet1); 218 | mHandler.sendEmptyMessageDelayed(MSG_WAVE2_ANIMATION, OFFSET); 219 | mHandler.sendEmptyMessageDelayed(MSG_WAVE3_ANIMATION, OFFSET * 2); 220 | isShowingWave = true; 221 | } 222 | 223 | private void stopWaveAnimation() { 224 | mWave1.clearAnimation(); 225 | mWave2.clearAnimation(); 226 | mWave3.clearAnimation(); 227 | isShowingWave = false; 228 | } 229 | 230 | class MyRecordListener implements RecorderButton.AudioStateRecorderListener { 231 | 232 | @Override 233 | public void onStart(float time) { 234 | 235 | tv_txt0.setVisibility(View.GONE); 236 | startWaveAnimation(); 237 | tv_time.setVisibility(View.VISIBLE); 238 | tv_state.setText(R.string.record_ing); 239 | // Toast.makeText(AudioRecordActivity.this, "开始录制", Toast.LENGTH_SHORT).show(); 240 | } 241 | 242 | @Override 243 | public void onUpdateTime(float currentTime, float minTime, float maxTime) { 244 | 245 | //保留一位小数 246 | int max = (int) maxTime; 247 | int time = (int) currentTime; 248 | hv_voice.setText(" " + (max - time) + " "); 249 | updateTime(max - time); 250 | 251 | if (time >= 10) { 252 | tv_txt0.setText(R.string.record_enougth); 253 | tv_txt0.setVisibility(View.VISIBLE); 254 | } 255 | } 256 | 257 | @Override 258 | public void onReturnToRecord() { 259 | tv_state.setText(R.string.record_ing); 260 | } 261 | 262 | @Override 263 | public void onWantToCancel() { 264 | tv_state.setText(R.string.record_want_to_cancel); 265 | } 266 | 267 | @Override 268 | public void onFinish(float seconds, String filePath) { 269 | 270 | btn_record.setEnabled(false); 271 | stopWaveAnimation(); 272 | tv_state.setText(R.string.record_success); 273 | tv_time.setVisibility(View.GONE); 274 | 275 | uploadRecord((int) seconds, new File(filePath)); 276 | // Toast.makeText(AudioRecordActivity.this, (int) seconds + "\n" + filePath, Toast.LENGTH_SHORT).show(); 277 | } 278 | 279 | @Override 280 | public void onCancel(boolean isTooShort) { 281 | stopWaveAnimation(); 282 | tv_state.setText(R.string.record_normal); 283 | if (isTooShort) { 284 | tv_time.setText(R.string.record_too_short); 285 | } 286 | 287 | } 288 | 289 | @Override 290 | public void onVoiceChange(int voiceLevel) { 291 | hv_voice.setVoice(voiceLevel); 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /app/src/main/java/com/nan/recordbutton/utils/AudioRecordManager.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton.utils; 2 | 3 | import android.media.MediaRecorder; 4 | import android.util.Log; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.UUID; 9 | 10 | /** 11 | * Created by 焕楠 2016-6-27 12 | */ 13 | public class AudioRecordManager { 14 | 15 | private MediaRecorder mMediaRecorder; 16 | private String mDir; 17 | private String mCurrentFilePath; 18 | 19 | private static AudioRecordManager mInstance; 20 | 21 | private boolean isPrepared = false; 22 | 23 | private AudioRecordManager(String dir) { 24 | mDir = dir; 25 | } 26 | 27 | public String getCurrentFilePath() { 28 | return mCurrentFilePath; 29 | } 30 | 31 | /** 32 | * 回调准备完毕 33 | */ 34 | public interface AudioStateListener { 35 | void wellPrepared(); 36 | } 37 | 38 | public AudioStateListener mListener; 39 | 40 | public void setOnAudioStateListner(AudioStateListener listner) { 41 | mListener = listner; 42 | } 43 | 44 | public static AudioRecordManager getInstance(String dir) { 45 | if (null == mInstance) { 46 | synchronized (AudioRecordManager.class) { 47 | if (null == mInstance) { 48 | mInstance = new AudioRecordManager(dir); 49 | } 50 | } 51 | } 52 | return mInstance; 53 | } 54 | 55 | public void prepareAudio() { 56 | Log.d("LONG", "preparedAudio"); 57 | try { 58 | isPrepared = false; 59 | File dir = new File(mDir); 60 | if (!dir.exists()) { 61 | dir.mkdirs(); 62 | } 63 | 64 | String fileName = generateFileName(); 65 | File file = new File(dir, fileName); 66 | 67 | Log.d("LONG", "the file name is " + fileName); 68 | 69 | mCurrentFilePath = file.getAbsolutePath(); 70 | mMediaRecorder = new MediaRecorder(); 71 | // 设置输出文件 72 | mMediaRecorder.setOutputFile(file.getAbsolutePath()); 73 | Log.d("LONG", "1"); 74 | // 设置MediaRecorder的音频源为麦克风 75 | mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 76 | Log.d("LONG", "2"); 77 | // 设置音频格式 AMR_NB 78 | // mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); 79 | mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS); 80 | Log.d("LONG", "3"); 81 | // 设置音频的编码为AMR 82 | // mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 83 | mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); 84 | Log.d("LONG", "4"); 85 | mMediaRecorder.prepare(); 86 | Log.d("LONG", "5"); 87 | mMediaRecorder.start(); 88 | Log.d("LONG", "6"); 89 | // 准备结束 90 | isPrepared = true; 91 | Log.d("LONG", "7"); 92 | 93 | if (mListener != null) { 94 | Log.d("LONG", "AudioStateListener is not null"); 95 | mListener.wellPrepared(); 96 | } else { 97 | Log.d("LONG", "lisetner null"); 98 | } 99 | } catch (IOException e) { 100 | e.printStackTrace(); 101 | } 102 | 103 | } 104 | 105 | /** 106 | * 随机生成文件的名称 107 | * 108 | * @return 109 | */ 110 | private String generateFileName() { 111 | // return UUID.randomUUID().toString() + ".amr"; 112 | return UUID.randomUUID().toString() + ".aac"; 113 | } 114 | 115 | public int getVoiceLevel(int maxLevel) { 116 | if (isPrepared) { 117 | // mMediaRecorder.getMaxAmplitude() 1-32767 118 | try { 119 | return maxLevel * mMediaRecorder.getMaxAmplitude() / 32768 + 1; 120 | } catch (Exception e) { 121 | // 忽略产生的异常 122 | } 123 | } 124 | return 1; 125 | } 126 | 127 | public void release() { 128 | if (mMediaRecorder != null) { 129 | mMediaRecorder.stop(); 130 | mMediaRecorder.release(); 131 | mMediaRecorder = null; 132 | } 133 | } 134 | 135 | /** 136 | * 释放资源 同时删除音频文件 137 | */ 138 | public void cancel() { 139 | release(); 140 | if (mCurrentFilePath != null) { 141 | File file = new File(mCurrentFilePath); 142 | file.delete(); 143 | mCurrentFilePath = null; 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nan/recordbutton/utils/VibratorUtils.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton.utils; 2 | 3 | import android.app.Service; 4 | import android.content.Context; 5 | import android.os.Vibrator; 6 | 7 | /** 8 | * Created by huannan on 2016/7/25. 9 | * 震动的工具类 10 | */ 11 | public class VibratorUtils { 12 | 13 | /** 14 | * final Activity activity :调用该方法的Activity实例 15 | * long milliseconds :震动的时长,单位是毫秒 16 | * long[] pattern :自定义震动模式 。数组中数字的含义依次是[静止时长,震动时长,静止时长,震动时长。。。]时长的单位是毫秒 17 | * boolean isRepeat : 是否反复震动,如果是true,反复震动,如果是false,只震动一次 18 | */ 19 | 20 | public static void vibrate(final Context context, long milliseconds) { 21 | Vibrator vib = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE); 22 | vib.vibrate(milliseconds); 23 | } 24 | 25 | public static void vibrate(final Context context, long[] pattern, boolean isRepeat) { 26 | Vibrator vib = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE); 27 | vib.vibrate(pattern, isRepeat ? 1 : -1); 28 | } 29 | 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nan/recordbutton/widget/HorVoiceView.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.RectF; 9 | import android.graphics.Typeface; 10 | import android.util.AttributeSet; 11 | import android.util.Log; 12 | import android.view.View; 13 | 14 | import com.nan.recordbutton.R; 15 | 16 | import java.util.LinkedList; 17 | 18 | public class HorVoiceView extends View { 19 | 20 | private Paint paint; 21 | private int color; 22 | private float lineHeight = 8; 23 | private float maxLineheight; 24 | private float lineWidth; 25 | private float textSize; 26 | private String text = " 15 "; 27 | private int textColor; 28 | private Thread mThread; 29 | private int milliSeconds; 30 | private boolean isStart = false; 31 | private Runnable mRunable; 32 | 33 | LinkedList list = new LinkedList<>(); 34 | 35 | public HorVoiceView(Context context) { 36 | super(context); 37 | } 38 | 39 | public HorVoiceView(Context context, AttributeSet attrs) { 40 | this(context, attrs, 0); 41 | } 42 | 43 | public HorVoiceView(Context context, AttributeSet attrs, int defStyleAttr) { 44 | super(context, attrs, defStyleAttr); 45 | for (int i = 0; i < 10; i++) { 46 | list.add(1); 47 | } 48 | paint = new Paint(); 49 | mRunable = new Runnable() { 50 | @Override 51 | public void run() { 52 | while (isStart) { 53 | milliSeconds += 200; 54 | text = milliSeconds / 1000 < 10 ? "0" + milliSeconds / 1000 : milliSeconds / 1000 + ""; 55 | Log.e("horvoiceview", "text " + text); 56 | setVoice(1); 57 | try { 58 | Thread.sleep(200); 59 | } catch (InterruptedException e) { 60 | e.printStackTrace(); 61 | } 62 | // postInvalidate(); 63 | } 64 | } 65 | }; 66 | TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HorVoiceView); 67 | color = mTypedArray.getColor(R.styleable.HorVoiceView_voiceLineColor, Color.BLACK); 68 | lineWidth = mTypedArray.getDimension(R.styleable.HorVoiceView_voiceLineWidth, 35); 69 | lineHeight = mTypedArray.getDimension(R.styleable.HorVoiceView_voiceLineHeight, 8); 70 | maxLineheight = mTypedArray.getDimension(R.styleable.HorVoiceView_voiceLineHeight, 32); 71 | textSize = mTypedArray.getDimension(R.styleable.HorVoiceView_voiceTextSize, 45); 72 | textColor = mTypedArray.getColor(R.styleable.HorVoiceView_voiceTextColor, Color.BLACK); 73 | mTypedArray.recycle(); 74 | } 75 | 76 | @Override 77 | protected void onDraw(Canvas canvas) { 78 | super.onDraw(canvas); 79 | int widthcentre = getWidth() / 2; 80 | int heightcentre = getHeight() / 2; 81 | 82 | paint.setStrokeWidth(0); 83 | paint.setColor(textColor); 84 | paint.setTextSize(textSize); 85 | paint.setTypeface(Typeface.DEFAULT_BOLD); 86 | float textWidth = paint.measureText(text); 87 | canvas.drawText(text, widthcentre - textWidth / 2, heightcentre - (paint.ascent() + paint.descent()) / 2, paint); 88 | 89 | // Log.e("Voice", text); 90 | 91 | paint.setColor(color); 92 | paint.setStyle(Paint.Style.FILL); 93 | paint.setStrokeWidth(lineWidth); 94 | paint.setAntiAlias(true); 95 | for (int i = 0; i < 10; i++) { 96 | RectF rect = new RectF(widthcentre + 2 * i * lineHeight + textWidth / 2 + lineHeight, heightcentre - list.get(i) * lineHeight / 2, widthcentre + 2 * i * lineHeight + 2 * lineHeight + textWidth / 2, heightcentre + list.get(i) * lineHeight / 2); 97 | RectF rect2 = new RectF(widthcentre - (2 * i * lineHeight + 2 * lineHeight + textWidth / 2), heightcentre - list.get(i) * lineHeight / 2, widthcentre - (2 * i * lineHeight + textWidth / 2 + lineHeight), heightcentre + list.get(i) * lineHeight / 2); 98 | canvas.drawRect(rect, paint); 99 | canvas.drawRect(rect2, paint); 100 | } 101 | } 102 | 103 | public synchronized void setVoice(Integer height) { 104 | for (int i = 0; i <= height / 30; i++) { 105 | list.remove(9 - i); 106 | list.add(i, (height / 20 - i) < 1 ? 1 : height / 20 - i); 107 | } 108 | // Log.e("波峰", "height" + height); 109 | postInvalidate(); 110 | } 111 | 112 | public synchronized void setText(String text) { 113 | this.text = text; 114 | postInvalidate(); 115 | } 116 | 117 | public synchronized void startRecording() { 118 | milliSeconds = 0; 119 | isStart = true; 120 | new Thread(mRunable).start(); 121 | } 122 | 123 | public synchronized void stopRecord() { 124 | isStart = false; 125 | list.clear(); 126 | for (int i = 0; i < 10; i++) { 127 | list.add(1); 128 | } 129 | text = "00"; 130 | postInvalidate(); 131 | } 132 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nan/recordbutton/widget/RecorderButton.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.media.MediaPlayer; 6 | import android.os.Environment; 7 | import android.os.Handler; 8 | import android.os.Message; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.widget.Button; 14 | 15 | import com.nan.recordbutton.R; 16 | import com.nan.recordbutton.utils.AudioRecordManager; 17 | import com.nan.recordbutton.utils.VibratorUtils; 18 | 19 | import java.io.File; 20 | 21 | 22 | /** 23 | * Created by 焕楠 2016-6-27 24 | */ 25 | public class RecorderButton extends Button implements AudioRecordManager.AudioStateListener { 26 | 27 | private static final String TAG = "AudioRecorderButton"; 28 | 29 | private static final int DISTANCE_CANCEL = 50; 30 | private static final int STATE_NORMAL = 1; 31 | private static final int STATE_RECORDING = 2; 32 | private static final int STATE_WANT_TO_CANCEL = 3; 33 | 34 | private int mCurState = STATE_NORMAL; 35 | private boolean isRecording = false; // 已经开始录音 36 | 37 | private AudioRecordManager mAudioRecordManager; 38 | 39 | private float mTime; 40 | // 是否触发longclick 41 | private boolean mReady; 42 | private String str_recorder_normal; 43 | private String str_recorder_recording; 44 | private String str_recorder_want_cancel; 45 | private int bg_recorder_normal; 46 | private int bg_recorder_recording; 47 | private int bg_recorder_cancel; 48 | private float max_record_time; 49 | private float min_record_time; 50 | private int max_voice_level; 51 | private MediaPlayer mMediaPlayer; 52 | private Context mCtx; 53 | 54 | public RecorderButton(Context context) { 55 | this(context, null, 0); 56 | } 57 | 58 | public RecorderButton(Context context, AttributeSet attrs) { 59 | this(context, attrs, 0); 60 | } 61 | 62 | public RecorderButton(Context context, AttributeSet attrs, int defStyleAttr) { 63 | super(context, attrs, defStyleAttr); 64 | 65 | final TypedArray a = context.obtainStyledAttributes(attrs, 66 | R.styleable.RecorderButton); 67 | 68 | mCtx = context; 69 | 70 | str_recorder_normal = a.getString(R.styleable.RecorderButton_txt_normal); 71 | str_recorder_recording = a.getString(R.styleable.RecorderButton_txt_recording); 72 | str_recorder_want_cancel = a.getString(R.styleable.RecorderButton_txt_want_cancel); 73 | 74 | bg_recorder_normal = a.getResourceId(R.styleable.RecorderButton_bg_normal, 0); 75 | bg_recorder_recording = a.getResourceId(R.styleable.RecorderButton_bg_recording, 0); 76 | bg_recorder_cancel = a.getResourceId(R.styleable.RecorderButton_bg_want_cancel, 0); 77 | 78 | //最大录音时间,默认为15秒 79 | //最小录音时间,默认为10秒 80 | max_record_time = a.getFloat(R.styleable.RecorderButton_max_record_time, 15); 81 | min_record_time = a.getFloat(R.styleable.RecorderButton_min_record_time, 10); 82 | 83 | max_voice_level = a.getInt(R.styleable.RecorderButton_max_voice_level, 5); 84 | 85 | a.recycle(); 86 | 87 | String dir = Environment.getExternalStorageDirectory() + File.separator + "sxbb" + File.separator + "record"; 88 | mAudioRecordManager = AudioRecordManager.getInstance(dir); 89 | mAudioRecordManager.setOnAudioStateListner(this); 90 | 91 | setText(str_recorder_normal); 92 | setBackgroundResource(bg_recorder_normal); 93 | 94 | // setOnClickListener(new OnClickListener() { 95 | // @Override 96 | // public void onClick(View view) { 97 | // mHandler.postDelayed(new Runnable() { 98 | // @Override 99 | // public void run() { 100 | // mReady = true; 101 | // Log.e(TAG, "OnLongClick"); 102 | // 103 | // //播放提示音 104 | // MediaPlayer.create(mCtx, R.raw.fx).start(); 105 | // mAudioRecordManager.prepareAudio(); 106 | // } 107 | // }, 1000); 108 | // } 109 | // }); 110 | 111 | setOnLongClickListener(new OnLongClickListener() { 112 | @Override 113 | public boolean onLongClick(View v) { 114 | 115 | mReady = true; 116 | Log.e(TAG, "OnLongClick"); 117 | 118 | VibratorUtils.vibrate(mCtx, 60); 119 | 120 | //播放提示音 121 | MediaPlayer.create(mCtx, R.raw.fx).start(); 122 | mAudioRecordManager.prepareAudio(); 123 | 124 | return false; 125 | } 126 | }); 127 | 128 | // setOnClickListener(new OnClickListener() { 129 | // @Override 130 | // public void onClick(View view) { 131 | // if (mListener != null) { 132 | // mListener.onCancel(true); 133 | // } 134 | // } 135 | // }); 136 | } 137 | 138 | /** 139 | * 录音完成后的回调 140 | */ 141 | public interface AudioStateRecorderListener { 142 | void onFinish(float seconds, String filePath); 143 | 144 | void onCancel(boolean isTooShort); 145 | 146 | void onVoiceChange(int voiceLevel); 147 | 148 | void onStart(float time); 149 | 150 | void onUpdateTime(float currentTime, float minTime, float maxTime); 151 | 152 | void onReturnToRecord(); 153 | 154 | void onWantToCancel(); 155 | } 156 | 157 | private AudioStateRecorderListener mListener; 158 | 159 | public void setAudioStateRecorderListener(AudioStateRecorderListener listener) { 160 | mListener = listener; 161 | } 162 | 163 | /** 164 | * 获取音量大小的Runnable 165 | */ 166 | private Runnable mGetVoiceLevelRunnable = new Runnable() { 167 | 168 | @Override 169 | public void run() { 170 | while (isRecording) { 171 | try { 172 | Thread.sleep(100); 173 | mTime += 0.1f; 174 | mHandler.sendEmptyMessage(MSG_UPDATE_TIME); 175 | mHandler.sendEmptyMessage(MSG_VOICE_CHANGE); 176 | if (mTime >= max_record_time) { 177 | //如果时间到了最大的录音时间 178 | mAudioRecordManager.release(); 179 | mHandler.sendEmptyMessage(MSG_TIME_LIMIT); 180 | } 181 | } catch (InterruptedException e) { 182 | e.printStackTrace(); 183 | } 184 | } 185 | } 186 | }; 187 | 188 | private static final int MSG_AUDIO_PREPARED = 0x110; 189 | private static final int MSG_VOICE_CHANGE = 0x111; 190 | private static final int MSG_UPDATE_TIME = 0x113; 191 | //最大的录音时间到了 192 | private static final int MSG_TIME_LIMIT = 0x114; 193 | private Handler mHandler = new Handler() { 194 | @Override 195 | public void handleMessage(Message msg) { 196 | switch (msg.what) { 197 | case MSG_AUDIO_PREPARED: 198 | 199 | // audio end prepared以后开始录音 200 | isRecording = true; 201 | 202 | if (mListener != null) { 203 | mListener.onStart(mTime); 204 | } 205 | 206 | // 开启线程,监听音量变化 207 | new Thread(mGetVoiceLevelRunnable).start(); 208 | break; 209 | case MSG_VOICE_CHANGE: 210 | if (mListener != null) { 211 | //根据用户设置的最大值去获取音量级别 212 | mListener.onVoiceChange(mAudioRecordManager.getVoiceLevel(max_voice_level)); 213 | } 214 | break; 215 | case MSG_UPDATE_TIME: 216 | if (mListener != null) { 217 | mListener.onUpdateTime(mTime, min_record_time, max_record_time); 218 | } 219 | break; 220 | case MSG_TIME_LIMIT: 221 | if (mListener != null) { 222 | //到达时间限制了 223 | MediaPlayer.create(mCtx, R.raw.gj).start(); 224 | mListener.onFinish(mTime, mAudioRecordManager.getCurrentFilePath()); 225 | } 226 | changeState(STATE_NORMAL); 227 | reset(); 228 | break; 229 | } 230 | super.handleMessage(msg); 231 | } 232 | }; 233 | 234 | @Override 235 | public void wellPrepared() { 236 | Log.d("LONG", "wellPrepared"); 237 | mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED); 238 | } 239 | 240 | @Override 241 | public boolean onTouchEvent(MotionEvent event) { 242 | int action = event.getAction(); 243 | int x = (int) event.getX(); 244 | int y = (int) event.getY(); 245 | 246 | switch (action) { 247 | case MotionEvent.ACTION_DOWN: 248 | if (mReady) { 249 | changeState(STATE_RECORDING); 250 | } 251 | break; 252 | case MotionEvent.ACTION_MOVE: 253 | // 根据x, y的坐标,判断是否想要取消 254 | if (mReady) { 255 | if (wantToCancel(x, y)) { 256 | changeState(STATE_WANT_TO_CANCEL); 257 | if (mListener != null) { 258 | mListener.onWantToCancel(); 259 | } 260 | } else { 261 | changeState(STATE_RECORDING); 262 | if (mListener != null) { 263 | mListener.onReturnToRecord(); 264 | } 265 | } 266 | } 267 | break; 268 | case MotionEvent.ACTION_UP: 269 | /** 270 | * 1. 未触发onLongClick 271 | * 2. prepared没有完毕已经up 272 | * 3. 录音时间小于预定的值,这个值我们设置为在onLongClick之前 273 | */ 274 | if (!mReady) { // 未触发onLongClick 275 | changeState(STATE_NORMAL); 276 | reset(); 277 | return super.onTouchEvent(event); 278 | } 279 | 280 | if (!isRecording || mTime < min_record_time) { // prepared没有完毕 或 录音时间过短 281 | isRecording = false; 282 | mAudioRecordManager.cancel(); 283 | // 用户录音时间太短,取消 284 | if (mListener != null) { 285 | MediaPlayer.create(mCtx, R.raw.fy).start(); 286 | mListener.onCancel(true); 287 | } 288 | } else if (STATE_RECORDING == mCurState) { // 正常录制结束 289 | mAudioRecordManager.release(); 290 | if (mListener != null) { 291 | MediaPlayer.create(mCtx, R.raw.gj).start(); 292 | mListener.onFinish(mTime, mAudioRecordManager.getCurrentFilePath()); 293 | } 294 | } else if (STATE_WANT_TO_CANCEL == mCurState) { 295 | mAudioRecordManager.cancel(); 296 | if (mListener != null) { 297 | MediaPlayer.create(mCtx, R.raw.fy).start(); 298 | mListener.onCancel(false); 299 | } 300 | } 301 | changeState(STATE_NORMAL); 302 | reset(); 303 | break; 304 | } 305 | 306 | return super.onTouchEvent(event); 307 | } 308 | 309 | /** 310 | * 恢复状态及标志位 311 | */ 312 | private void reset() { 313 | isRecording = false; 314 | mReady = false; 315 | mTime = 0; 316 | mCurState = STATE_NORMAL; 317 | } 318 | 319 | /** 320 | * 根据坐标去判断是否应该取消 321 | * 322 | * @param x 323 | * @param y 324 | * @return 325 | */ 326 | private boolean wantToCancel(int x, int y) { 327 | if (x < 0 || x > getWidth()) { 328 | return true; 329 | } 330 | if (y < -DISTANCE_CANCEL || y > getHeight() + DISTANCE_CANCEL) { 331 | return true; 332 | } 333 | return false; 334 | } 335 | 336 | /** 337 | * 按钮状态改变 338 | * 339 | * @param state 340 | */ 341 | private void changeState(int state) { 342 | if (mCurState != state) { 343 | mCurState = state; 344 | switch (mCurState) { 345 | case STATE_NORMAL: 346 | setBackgroundResource(bg_recorder_normal); 347 | setText(str_recorder_normal); 348 | break; 349 | case STATE_RECORDING: 350 | setBackgroundResource(bg_recorder_recording); 351 | setText(str_recorder_recording); 352 | break; 353 | case STATE_WANT_TO_CANCEL: 354 | setBackgroundResource(bg_recorder_cancel); 355 | setText(str_recorder_want_cancel); 356 | break; 357 | } 358 | } 359 | } 360 | 361 | 362 | } 363 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/bg_record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/drawable-xhdpi/bg_record.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/bg_recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/drawable-xhdpi/bg_recording.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/iv_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/drawable-xhdpi/iv_bg.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/wave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/drawable-xhdpi/wave.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 20 | 21 | 27 | 28 | 34 | 35 | 51 | 52 | 60 | 61 | 71 | 72 | 82 | 83 | 94 | 95 | 105 | 106 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/raw/fx.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/raw/fx.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/fy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/raw/fy.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/gj.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/app/src/main/res/raw/gj.mp3 -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #ef4133 7 | #707070 8 | #A4A3A3 9 | #737373 10 | #FE7308 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 100dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RecordButton 3 | 4 | 确认后你们可以自由沟通 5 | 对方将通过你的描述决定是否派单 6 | 按住开始说话 7 | 语音将在满 8 | s后自动发送哦 9 | 录音完毕,正在发送... 10 | 按住开始说话 11 | 正在录音 12 | 松开取消 13 | 录音时间过短 14 | 录音发送失败,请重试 15 | 16 | 可以松手了 17 | 没有录音权限 18 | 没有录音权限,请在安全中心打开录音权限 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | -------------------------------------------------------------------------------- /app/src/test/java/com/nan/recordbutton/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.nan.recordbutton; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.2' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huannan/RecorderButton/3a472feacbb8680cbbe30c240e64c09a902bf1c8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------