├── .gitignore ├── CHANGELOG.md ├── README.md ├── art └── demo.gif ├── build.gradle ├── example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tougee │ │ └── recorderview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tougee │ │ │ └── demo │ │ │ ├── AudioBuffer.java │ │ │ ├── AudioPlayer.java │ │ │ ├── AudioRecorder.java │ │ │ ├── Constants.java │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_play_arrow_black_24dp.xml │ │ └── ic_stop_black_24dp.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── tougee │ └── recorderview │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── recorder-view ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tougee │ │ └── recorderview │ │ ├── AudioRecordView.kt │ │ ├── AudioScaleView.kt │ │ ├── BlinkingDrawable.kt │ │ ├── Extensions.kt │ │ ├── RecordCircleView.kt │ │ └── SlidePanelView.kt │ └── res │ ├── drawable-xxhdpi │ ├── bg_record_tip.9.png │ ├── lock_arrow.png │ ├── lock_close.png │ ├── lock_middle.png │ ├── lock_open.png │ ├── lock_round.9.png │ ├── lock_round_shadow.9.png │ └── lock_top.png │ ├── drawable │ ├── bg_mic_expand.xml │ ├── ic_blinking.xml │ ├── ic_chevron_left_gray_24dp.xml │ ├── ic_launcher_background.xml │ ├── ic_mic_blue_24dp.xml │ ├── ic_record_mic_black.xml │ ├── ic_record_mic_white.xml │ └── ic_send_white_24dp.xml │ ├── layout │ ├── view_audio_record.xml │ └── view_slide_panel.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | reports 3 | local.properties 4 | .idea 5 | *.iml 6 | build 7 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | ## Version 1.0.4 5 | _2021_06-10_ 6 | 7 | This release contains Migration to AndroidX and ViewBinding 8 | 9 | ## Version 1.0.3 10 | _2020-11-12_ 11 | 12 | This release support more customizations 13 | 14 | ## Version 1.0.2 15 | _2020-11-05_ 16 | 17 | This release support customize multi properties. 18 | 19 | ## Version 1.0.1 20 | _2020-11-03_ 21 | 22 | This release add setTimeoutSeconds for [AudioRecorderView]. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AudioRecorderView 2 | 3 | Simple Audio Recorder View with 'swap-to-cancel' Like Telegram 4 | 5 | 6 | 7 | ## Usage 8 | ```xml 9 | 10 | // AndroidManifest.xml 11 | 12 | 13 | // layout include AudioRecordView 14 | 31 | 32 | ``` 33 | ``` kotlin 34 | 35 | // activity implemented AudioRecordView.Callback 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | setContentView(R.layout.activity_main) 40 | 41 | record_view.apply { 42 | activity = this@MainActivity 43 | callback = this@MainActivity 44 | 45 | micIcon = R.drawable.ic_chevron_left_gray_24dp 46 | micActiveIcon = R.drawable.ic_play_arrow_black_24dp 47 | micHintEnable = false 48 | micHintText = "Custom hint text" 49 | micHintColor = ContextCompat.getColor(this@MainActivity, android.R.color.holo_red_light) 50 | micHintBg = R.drawable.ic_launcher_background 51 | blinkColor = ContextCompat.getColor(this@MainActivity, R.color.color_blue) 52 | circleColor = ContextCompat.getColor(this@MainActivity, R.color.color_blink) 53 | cancelIconColor = ContextCompat.getColor(this@MainActivity, R.color.color_blue) 54 | slideCancelText = "Custom Slide to cancel" 55 | cancelText = "Custom Cancel" 56 | vibrationEnable = false 57 | } 58 | 59 | record_view.setTimeoutSeconds(20) 60 | } 61 | 62 | override fun onRecordStart() {} 63 | 64 | override fun isReady() = true 65 | 66 | override fun onRecordEnd() {} 67 | 68 | override fun onRecordCancel() {} 69 | 70 | ``` 71 | 72 | ## Setup 73 | ### Android Studio / Gradle 74 | Add the following dependency in your root build.gradle at the end of repositories: 75 | ```Gradle 76 | allprojects { 77 | repositories { 78 | //... 79 | maven { url = 'https://jitpack.io' } 80 | } 81 | } 82 | ``` 83 | Add the dependency: 84 | ```Gradle 85 | dependencies { 86 | implementation 'com.github.tougee:audiorecorderview:1.0.4' 87 | } 88 | ``` 89 | 90 | ## License details 91 | 92 | ``` 93 | Copyright 2018 Touge 94 | 95 | Licensed under the Apache License, Version 2.0 (the "License"); 96 | you may not use this file except in compliance with the License. 97 | You may obtain a copy of the License at 98 | 99 | http://www.apache.org/licenses/LICENSE-2.0 100 | 101 | Unless required by applicable law or agreed to in writing, software 102 | distributed under the License is distributed on an "AS IS" BASIS, 103 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 104 | See the License for the specific language governing permissions and 105 | limitations under the License. 106 | ``` -------------------------------------------------------------------------------- /art/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/art/demo.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.5.10' 5 | ext.app_compat_version = '1.3.0' 6 | 7 | repositories { 8 | google() 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:4.2.1' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 30 6 | defaultConfig { 7 | applicationId "com.tougee.demo" 8 | minSdkVersion 21 9 | targetSdkVersion 30 10 | versionCode 1 11 | versionName "0.1" 12 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | buildFeatures { 21 | viewBinding true 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(include: ['*.jar'], dir: 'libs') 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 28 | implementation "androidx.appcompat:appcompat:$app_compat_version" 29 | testImplementation 'junit:junit:4.13.2' 30 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 31 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 32 | implementation project(':recorder-view') 33 | } 34 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /example/src/androidTest/java/com/tougee/recorderview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.recorderview 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.tougee.recorderview", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/src/main/java/com/tougee/demo/AudioBuffer.java: -------------------------------------------------------------------------------- 1 | package com.tougee.demo; 2 | 3 | /** 4 | * A buffer that grabs the smallest supported sample rate for {@link android.media.AudioTrack} and 5 | * {@link android.media.AudioRecord}. 6 | */ 7 | public abstract class AudioBuffer { 8 | private static final int[] POSSIBLE_SAMPLE_RATES = 9 | new int[] {8000, 11025, 16000, 22050, 44100, 48000}; 10 | 11 | final int size; 12 | final int sampleRate; 13 | final byte[] data; 14 | 15 | protected AudioBuffer() { 16 | int size = -1; 17 | int sampleRate = -1; 18 | 19 | // Iterate over all possible sample rates, and try to find the shortest one. The shorter 20 | // it is, the faster it'll stream. 21 | for (int rate : POSSIBLE_SAMPLE_RATES) { 22 | sampleRate = rate; 23 | size = getMinBufferSize(sampleRate); 24 | if (validSize(size)) { 25 | break; 26 | } 27 | } 28 | 29 | // If none of them were good, then just pick 1kb 30 | if (!validSize(size)) { 31 | size = 1024; 32 | } 33 | 34 | this.size = size; 35 | this.sampleRate = sampleRate; 36 | data = new byte[size]; 37 | } 38 | 39 | protected abstract boolean validSize(int size); 40 | 41 | protected abstract int getMinBufferSize(int sampleRate); 42 | } -------------------------------------------------------------------------------- /example/src/main/java/com/tougee/demo/AudioPlayer.java: -------------------------------------------------------------------------------- 1 | package com.tougee.demo; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.AudioManager; 5 | import android.media.AudioTrack; 6 | import android.util.Log; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | import static android.os.Process.THREAD_PRIORITY_AUDIO; 12 | import static android.os.Process.setThreadPriority; 13 | import static com.tougee.demo.Constants.TAG; 14 | 15 | /** 16 | * A fire-once class. When created, you must pass a {@link InputStream}. Once {@link #start()} is 17 | * called, the input stream will be read from until either {@link #stop()} is called or the stream 18 | * ends. 19 | */ 20 | public class AudioPlayer { 21 | /** The audio stream we're reading from. */ 22 | private final InputStream mInputStream; 23 | 24 | /** 25 | * If true, the background thread will continue to loop and play audio. Once false, the thread 26 | * will shut down. 27 | */ 28 | private volatile boolean mAlive; 29 | 30 | /** The background thread recording audio for us. */ 31 | private Thread mThread; 32 | 33 | /** 34 | * A simple audio player. 35 | * 36 | * @param inputStream The input stream of the recording. 37 | */ 38 | public AudioPlayer(InputStream inputStream) { 39 | mInputStream = inputStream; 40 | } 41 | 42 | /** @return True if currently playing. */ 43 | public boolean isPlaying() { 44 | return mAlive; 45 | } 46 | 47 | /** Starts playing the stream. */ 48 | public void start() { 49 | mAlive = true; 50 | mThread = 51 | new Thread() { 52 | @Override 53 | public void run() { 54 | setThreadPriority(THREAD_PRIORITY_AUDIO); 55 | 56 | Buffer buffer = new Buffer(); 57 | AudioTrack audioTrack = 58 | new AudioTrack( 59 | AudioManager.STREAM_MUSIC, 60 | buffer.sampleRate, 61 | AudioFormat.CHANNEL_OUT_MONO, 62 | AudioFormat.ENCODING_PCM_16BIT, 63 | buffer.size, 64 | AudioTrack.MODE_STREAM); 65 | audioTrack.play(); 66 | 67 | int len; 68 | try { 69 | while (isPlaying() && (len = mInputStream.read(buffer.data)) > 0) { 70 | audioTrack.write(buffer.data, 0, len); 71 | } 72 | } catch (IOException e) { 73 | Log.e(TAG, "Exception with playing stream", e); 74 | } finally { 75 | stopInternal(); 76 | audioTrack.release(); 77 | onFinish(); 78 | } 79 | } 80 | }; 81 | mThread.start(); 82 | } 83 | 84 | private void stopInternal() { 85 | mAlive = false; 86 | try { 87 | mInputStream.close(); 88 | } catch (IOException e) { 89 | Log.e(TAG, "Failed to close input stream", e); 90 | } 91 | } 92 | 93 | /** Stops playing the stream. */ 94 | public void stop() { 95 | stopInternal(); 96 | try { 97 | mThread.join(); 98 | } catch (InterruptedException e) { 99 | Log.e(TAG, "Interrupted while joining AudioRecorder thread", e); 100 | Thread.currentThread().interrupt(); 101 | } 102 | } 103 | 104 | /** The stream has now ended. */ 105 | protected void onFinish() {} 106 | 107 | private static class Buffer extends AudioBuffer { 108 | @Override 109 | protected boolean validSize(int size) { 110 | return size != AudioTrack.ERROR && size != AudioTrack.ERROR_BAD_VALUE; 111 | } 112 | 113 | @Override 114 | protected int getMinBufferSize(int sampleRate) { 115 | return AudioTrack.getMinBufferSize( 116 | sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /example/src/main/java/com/tougee/demo/AudioRecorder.java: -------------------------------------------------------------------------------- 1 | package com.tougee.demo; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.AudioRecord; 5 | import android.media.MediaRecorder; 6 | import android.os.ParcelFileDescriptor; 7 | import android.util.Log; 8 | 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | 12 | import static android.os.Process.THREAD_PRIORITY_AUDIO; 13 | import static android.os.Process.setThreadPriority; 14 | import static com.tougee.demo.Constants.TAG; 15 | 16 | /** 17 | * When created, you must pass a {@link ParcelFileDescriptor}. Once {@link #start()} is called, the 18 | * file descriptor will be written to until {@link #stop()} is called. 19 | */ 20 | public class AudioRecorder { 21 | 22 | /** The stream to write to. */ 23 | private final OutputStream mOutputStream; 24 | 25 | /** 26 | * If true, the background thread will continue to loop and record audio. Once false, the thread 27 | * will shut down. 28 | */ 29 | private volatile boolean mAlive; 30 | 31 | /** The background thread recording audio for us. */ 32 | private Thread mThread; 33 | 34 | /** 35 | * A simple audio recorder. 36 | * 37 | * @param file The output stream of the recording. 38 | */ 39 | public AudioRecorder(ParcelFileDescriptor file) { 40 | mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(file); 41 | } 42 | 43 | /** @return True if actively recording. False otherwise. */ 44 | public boolean isRecording() { 45 | return mAlive; 46 | } 47 | 48 | /** Starts recording audio. */ 49 | public void start() { 50 | if (isRecording()) { 51 | Log.w(TAG, "Already running"); 52 | return; 53 | } 54 | 55 | mAlive = true; 56 | mThread = 57 | new Thread() { 58 | @Override 59 | public void run() { 60 | setThreadPriority(THREAD_PRIORITY_AUDIO); 61 | 62 | Buffer buffer = new Buffer(); 63 | AudioRecord record = 64 | new AudioRecord( 65 | MediaRecorder.AudioSource.DEFAULT, 66 | buffer.sampleRate, 67 | AudioFormat.CHANNEL_IN_MONO, 68 | AudioFormat.ENCODING_PCM_16BIT, 69 | buffer.size); 70 | 71 | if (record.getState() != AudioRecord.STATE_INITIALIZED) { 72 | Log.w(TAG, "Failed to start recording"); 73 | mAlive = false; 74 | return; 75 | } 76 | 77 | record.startRecording(); 78 | 79 | // While we're running, we'll read the bytes from the AudioRecord and write them 80 | // to our output stream. 81 | try { 82 | while (isRecording()) { 83 | int len = record.read(buffer.data, 0, buffer.size); 84 | if (len >= 0 && len <= buffer.size) { 85 | mOutputStream.write(buffer.data, 0, len); 86 | mOutputStream.flush(); 87 | } else { 88 | Log.w(TAG, "Unexpected length returned: " + len); 89 | } 90 | } 91 | } catch (IOException e) { 92 | Log.e(TAG, "Exception with recording stream", e); 93 | } finally { 94 | stopInternal(); 95 | try { 96 | record.stop(); 97 | } catch (IllegalStateException e) { 98 | Log.e(TAG, "Failed to stop AudioRecord", e); 99 | } 100 | record.release(); 101 | } 102 | } 103 | }; 104 | mThread.start(); 105 | } 106 | 107 | private void stopInternal() { 108 | mAlive = false; 109 | try { 110 | mOutputStream.close(); 111 | } catch (IOException e) { 112 | Log.e(TAG, "Failed to close output stream", e); 113 | } 114 | } 115 | 116 | /** Stops recording audio. */ 117 | public void stop() { 118 | stopInternal(); 119 | try { 120 | mThread.join(); 121 | } catch (InterruptedException e) { 122 | Log.e(TAG, "Interrupted while joining AudioRecorder thread", e); 123 | Thread.currentThread().interrupt(); 124 | } 125 | } 126 | 127 | private static class Buffer extends AudioBuffer { 128 | @Override 129 | protected boolean validSize(int size) { 130 | return size != AudioRecord.ERROR && size != AudioRecord.ERROR_BAD_VALUE; 131 | } 132 | 133 | @Override 134 | protected int getMinBufferSize(int sampleRate) { 135 | return AudioRecord.getMinBufferSize( 136 | sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /example/src/main/java/com/tougee/demo/Constants.java: -------------------------------------------------------------------------------- 1 | package com.tougee.demo; 2 | 3 | /** A set of constants used within the app. */ 4 | public class Constants { 5 | public static final String TAG = "AudioRecorderView"; 6 | } -------------------------------------------------------------------------------- /example/src/main/java/com/tougee/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.demo 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.os.ParcelFileDescriptor 9 | import androidx.core.content.ContextCompat 10 | import androidx.appcompat.app.AppCompatActivity 11 | import android.widget.Toast 12 | import com.tougee.demo.databinding.ActivityMainBinding 13 | import com.tougee.recorderview.AudioRecordView 14 | import com.tougee.recorderview.toast 15 | import java.io.File 16 | import java.io.FileInputStream 17 | import java.io.PrintWriter 18 | 19 | class MainActivity : AppCompatActivity(), AudioRecordView.Callback { 20 | 21 | companion object { 22 | const val REQUEST_CAMERA_PERMISSION_RESULT = 123 23 | } 24 | lateinit var binding: ActivityMainBinding 25 | 26 | private val file: File by lazy { 27 | val f = File("$externalCacheDir${File.separator}audio.pcm") 28 | if (!f.exists()) { 29 | f.createNewFile() 30 | } 31 | f 32 | } 33 | 34 | private val tmpFile: File by lazy { 35 | val f = File("$externalCacheDir${File.separator}tmp.pcm") 36 | if (!f.exists()) { 37 | f.createNewFile() 38 | } 39 | f 40 | } 41 | 42 | private var audioRecord: AudioRecorder? = null 43 | private var audioPlayer: AudioPlayer? = null 44 | 45 | @SuppressLint("SetTextI18n") 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | binding = ActivityMainBinding.inflate(layoutInflater) 49 | setContentView(binding.root) 50 | binding.fileTv.text = "path: ${file.absolutePath}\nlength: ${file.length()}" 51 | binding.recordView.apply { 52 | activity = this@MainActivity 53 | callback = this@MainActivity 54 | 55 | // micIcon = R.drawable.ic_chevron_left_gray_24dp 56 | // micActiveIcon = R.drawable.ic_play_arrow_black_24dp 57 | // micHintEnable = false 58 | // micHintText = "Custom hint text" 59 | // micHintColor = ContextCompat.getColor(this@MainActivity, android.R.color.holo_red_light) 60 | // micHintBg = R.drawable.ic_launcher_background 61 | // blinkColor = ContextCompat.getColor(this@MainActivity, R.color.color_blue) 62 | // circleColor = ContextCompat.getColor(this@MainActivity, R.color.color_blink) 63 | // cancelIconColor = ContextCompat.getColor(this@MainActivity, R.color.color_blue) 64 | // slideCancelText = "Custom Slide to cancel" 65 | // cancelText = "Custom Cancel" 66 | // vibrationEnable = false 67 | } 68 | binding.recordView.setTimeoutSeconds(20) 69 | binding.playIv.setOnClickListener { 70 | if (audioPlayer != null && audioPlayer!!.isPlaying) { 71 | binding.playIv.setImageResource(R.drawable.ic_play_arrow_black_24dp) 72 | audioPlayer!!.stop() 73 | } else { 74 | binding.playIv.setImageResource(R.drawable.ic_stop_black_24dp) 75 | audioPlayer = AudioPlayer(FileInputStream(file)) 76 | audioPlayer!!.start() 77 | } 78 | } 79 | } 80 | 81 | override fun onResume() { 82 | super.onResume() 83 | requestPermission() 84 | } 85 | 86 | 87 | override fun onRecordStart() { 88 | toast("onRecordStart") 89 | 90 | clearFile(tmpFile) 91 | 92 | audioRecord = AudioRecorder(ParcelFileDescriptor.open(tmpFile, ParcelFileDescriptor.MODE_READ_WRITE)) 93 | audioRecord?.start() 94 | } 95 | 96 | override fun isReady() = true 97 | 98 | override fun onRecordEnd() { 99 | toast("onEnd") 100 | audioRecord?.stop() 101 | 102 | tmpFile.copyTo(file, true) 103 | } 104 | 105 | override fun onRecordCancel() { 106 | toast("onCancel") 107 | audioRecord?.stop() 108 | } 109 | 110 | private fun clearFile(f: File) { 111 | PrintWriter(f).run { 112 | print("") 113 | close() 114 | } 115 | } 116 | 117 | private fun requestPermission() { 118 | @Suppress("ControlFlowWithEmptyBody") 119 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 120 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == 121 | PackageManager.PERMISSION_GRANTED) { 122 | } else { 123 | if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { 124 | Toast.makeText(this, 125 | "App required access to audio", Toast.LENGTH_SHORT).show() 126 | } 127 | requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_CAMERA_PERMISSION_RESULT) 128 | } 129 | } else { 130 | // put your code for Version < Marshmallow 131 | } 132 | } 133 | 134 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 135 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 136 | 137 | if (requestCode == REQUEST_CAMERA_PERMISSION_RESULT) { 138 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { 139 | Toast.makeText(applicationContext, 140 | "Application will not have audio on record", Toast.LENGTH_SHORT).show() 141 | } 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /example/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_play_arrow_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_stop_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #1987FF 7 | 8 | -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RecorderView 3 | Cancel 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/test/java/com/tougee/recorderview/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.recorderview 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 07 16:52:05 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /recorder-view/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /recorder-view/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 30 6 | 7 | defaultConfig { 8 | minSdkVersion 21 9 | targetSdkVersion 30 10 | versionCode 5 11 | versionName "1.0.4" 12 | 13 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 14 | 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | configurations { 25 | ktlint 26 | } 27 | 28 | buildFeatures { 29 | viewBinding true 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | 36 | ktlint "com.pinterest:ktlint:0.41.0" 37 | 38 | implementation "androidx.appcompat:appcompat:$app_compat_version" 39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 40 | } 41 | 42 | 43 | task ktlint(type: JavaExec, group: "verification") { 44 | description = "Check Kotlin code style." 45 | classpath = configurations.ktlint 46 | main = "com.pinterest.ktlint.Main" 47 | args "src/**/*.kt" 48 | // to generate report in checkstyle format prepend following args: 49 | // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml" 50 | // see https://github.com/pinterest/ktlint#usage for more 51 | } 52 | check.dependsOn ktlint 53 | 54 | task ktlintFormat(type: JavaExec, group: "formatting") { 55 | description = "Fix Kotlin code style deviations." 56 | classpath = configurations.ktlint 57 | main = "com.pinterest.ktlint.Main" 58 | args "-F", "src/**/*.kt" 59 | } 60 | 61 | repositories { 62 | mavenCentral() 63 | } -------------------------------------------------------------------------------- /recorder-view/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /recorder-view/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /recorder-view/src/main/java/com/tougee/recorderview/AudioRecordView.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.recorderview 2 | 3 | import android.Manifest 4 | import android.animation.ObjectAnimator 5 | import android.annotation.SuppressLint 6 | import android.app.Activity 7 | import android.content.Context 8 | import android.content.pm.PackageManager 9 | import android.util.AttributeSet 10 | import android.view.LayoutInflater 11 | import android.view.MotionEvent.* 12 | import android.view.View 13 | import android.view.View.OnTouchListener 14 | import android.view.animation.AccelerateInterpolator 15 | import android.view.animation.DecelerateInterpolator 16 | import android.widget.FrameLayout 17 | import androidx.annotation.ColorInt 18 | import androidx.annotation.DrawableRes 19 | import androidx.core.app.ActivityCompat 20 | import androidx.core.content.ContextCompat 21 | import androidx.core.content.res.ResourcesCompat 22 | import com.tougee.recorderview.databinding.ViewAudioRecordBinding 23 | import com.tougee.recorderview.databinding.ViewSlidePanelBinding 24 | 25 | @Suppress("MemberVisibilityCanBePrivate") 26 | class AudioRecordView : FrameLayout { 27 | 28 | companion object { 29 | const val RECORD_DELAY = 200L 30 | const val RECORD_TIP_MILLIS = 2000L 31 | 32 | const val ANIMATION_DURATION = 250L 33 | } 34 | 35 | lateinit var callback: Callback 36 | lateinit var activity: Activity 37 | lateinit var binding: ViewAudioRecordBinding 38 | lateinit var _binding: ViewSlidePanelBinding 39 | 40 | private var isRecording = false 41 | private var upBeforeGrant = false 42 | 43 | @DrawableRes 44 | var micIcon: Int = R.drawable.ic_record_mic_black 45 | set(value) { 46 | if (value == field) return 47 | 48 | field = value 49 | binding.recordIb.setImageResource(value) 50 | } 51 | 52 | @DrawableRes 53 | var micActiveIcon: Int = R.drawable.ic_record_mic_white 54 | set(value) { 55 | if (value == field) return 56 | 57 | field = value 58 | binding.recordCircle.audioDrawable = ResourcesCompat.getDrawable(resources, value, null)!! 59 | } 60 | 61 | var micHintEnable: Boolean = true 62 | 63 | @ColorInt 64 | var micHintColor: Int = ContextCompat.getColor(context, android.R.color.white) 65 | set(value) { 66 | if (value == field) return 67 | 68 | field = value 69 | binding.recordTipTv.setTextColor(value) 70 | } 71 | 72 | var micHintText: String = context.getString(R.string.hold_to_record_audio) 73 | set(value) { 74 | if (value == field) return 75 | 76 | field = value 77 | binding.recordTipTv.text = micHintText 78 | } 79 | 80 | @DrawableRes 81 | var micHintBg: Int = R.drawable.bg_record_tip 82 | set(value) { 83 | if (value == field) return 84 | 85 | field = value 86 | binding.recordTipTv.setBackgroundResource(value) 87 | } 88 | 89 | @ColorInt 90 | var circleColor: Int = ContextCompat.getColor(context, R.color.color_blue) 91 | set(value) { 92 | if (value == field) return 93 | 94 | field = value 95 | binding.recordCircle.circlePaint.color = value 96 | } 97 | 98 | @ColorInt 99 | var cancelIconColor: Int = ContextCompat.getColor(context, R.color.color_blink) 100 | set(value) { 101 | if (value == field) return 102 | 103 | field = value 104 | binding.recordCircle.cancelIconPaint.color = value 105 | } 106 | 107 | @ColorInt 108 | var blinkColor: Int = ContextCompat.getColor(context, R.color.color_blink) 109 | set(value) { 110 | if (value == field) return 111 | 112 | field = value 113 | binding.slidePanel.updateBlinkDrawable(value) 114 | } 115 | 116 | var slideCancelText: String = context.getString(R.string.slide_to_cancel) 117 | set(value) { 118 | if (value == field) return 119 | 120 | field = value 121 | _binding.slideCancelTv.text = value 122 | } 123 | 124 | var cancelText: String = context.getString(R.string.cancel) 125 | set(value) { 126 | if (value == field) return 127 | 128 | field = value 129 | _binding.cancelTv.text = value 130 | } 131 | 132 | var vibrationEnable: Boolean = true 133 | 134 | constructor(context: Context) : this(context, null) 135 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 136 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 137 | binding = ViewAudioRecordBinding.inflate(LayoutInflater.from(context), this) 138 | _binding = ViewSlidePanelBinding.bind(binding.root) 139 | 140 | val ta = context.obtainStyledAttributes(attrs, R.styleable.AudioRecordView) 141 | if (ta.hasValue(R.styleable.AudioRecordView_mic_icon)) { 142 | micIcon = ta.getResourceId(R.styleable.AudioRecordView_mic_icon, 0) 143 | } 144 | if (ta.hasValue(R.styleable.AudioRecordView_mic_active_icon)) { 145 | micActiveIcon = ta.getResourceId(R.styleable.AudioRecordView_mic_active_icon, 0) 146 | } 147 | if (ta.hasValue(R.styleable.AudioRecordView_mic_hint_enable)) { 148 | micHintEnable = ta.getBoolean(R.styleable.AudioRecordView_mic_hint_enable, true) 149 | } 150 | if (ta.hasValue(R.styleable.AudioRecordView_mic_hint_text)) { 151 | ta.getString(R.styleable.AudioRecordView_mic_hint_text)?.let { 152 | micHintText = it 153 | } 154 | } 155 | if (ta.hasValue(R.styleable.AudioRecordView_mic_hint_color)) { 156 | micHintColor = ta.getColor(R.styleable.AudioRecordView_mic_hint_color, 0) 157 | } 158 | if (ta.hasValue(R.styleable.AudioRecordView_mic_hint_bg)) { 159 | micHintBg = ta.getResourceId(R.styleable.AudioRecordView_mic_hint_bg, 0) 160 | } 161 | if (ta.hasValue(R.styleable.AudioRecordView_circle_color)) { 162 | circleColor = ta.getColor(R.styleable.AudioRecordView_circle_color, 0) 163 | } 164 | if (ta.hasValue(R.styleable.AudioRecordView_cancel_icon_color)) { 165 | cancelIconColor = ta.getColor(R.styleable.AudioRecordView_cancel_icon_color, 0) 166 | } 167 | if (ta.hasValue(R.styleable.AudioRecordView_blink_color)) { 168 | blinkColor = ta.getColor(R.styleable.AudioRecordView_blink_color, 0) 169 | } 170 | if (ta.hasValue(R.styleable.AudioRecordView_slide_cancel_text)) { 171 | ta.getString(R.styleable.AudioRecordView_slide_cancel_text)?.let { 172 | slideCancelText = it 173 | } 174 | } 175 | if (ta.hasValue(R.styleable.AudioRecordView_cancel_text)) { 176 | ta.getString(R.styleable.AudioRecordView_cancel_text)?.let { 177 | cancelText = it 178 | } 179 | } 180 | if (ta.hasValue(R.styleable.AudioRecordView_vibration_enable)) { 181 | vibrationEnable = ta.getBoolean(R.styleable.AudioRecordView_vibration_enable, true) 182 | } 183 | 184 | ta.recycle() 185 | 186 | binding.slidePanel.callback = chatSlideCallback 187 | binding.recordCircle.callback = recordCircleCallback 188 | binding.recordIb.setOnTouchListener(recordOnTouchListener) 189 | } 190 | 191 | @Suppress("unused") 192 | fun cancelExternal() { 193 | removeCallbacks(recordRunnable) 194 | cleanUp() 195 | updateRecordCircleAndSendIcon() 196 | binding.slidePanel.parent.requestDisallowInterceptTouchEvent(false) 197 | } 198 | 199 | fun setTimeoutSeconds(seconds: Int) { 200 | binding.slidePanel.timeoutSeconds = seconds 201 | } 202 | 203 | private fun cleanUp() { 204 | startX = 0f 205 | originX = 0f 206 | isRecording = false 207 | } 208 | 209 | private fun handleCancelOrEnd(cancel: Boolean) { 210 | if (cancel) callback.onRecordCancel() else callback.onRecordEnd() 211 | if (vibrationEnable) { 212 | context.vibrate(longArrayOf(0, 10)) 213 | } 214 | cleanUp() 215 | updateRecordCircleAndSendIcon() 216 | } 217 | 218 | private fun updateRecordCircleAndSendIcon() { 219 | if (isRecording) { 220 | binding.recordCircle.visibility = View.VISIBLE 221 | binding.recordCircle.setAmplitude(.0) 222 | ObjectAnimator.ofFloat(binding.recordCircle, "scale", 1f).apply { 223 | interpolator = DecelerateInterpolator() 224 | duration = 200 225 | addListener( 226 | onEnd = { 227 | binding.recordCircle.visibility = View.VISIBLE 228 | }, 229 | onCancel = { 230 | binding.recordCircle.visibility = View.VISIBLE 231 | } 232 | ) 233 | }.start() 234 | binding.recordIb.animate().setDuration(200).alpha(0f).start() 235 | binding.slidePanel.onStart() 236 | } else { 237 | ObjectAnimator.ofFloat(binding.recordCircle, "scale", 0f).apply { 238 | interpolator = AccelerateInterpolator() 239 | duration = 200 240 | addListener( 241 | onEnd = { 242 | binding.recordCircle.visibility = View.GONE 243 | binding.recordCircle.setSendButtonInvisible() 244 | }, 245 | onCancel = { 246 | binding.recordCircle.visibility = View.GONE 247 | binding.recordCircle.setSendButtonInvisible() 248 | } 249 | ) 250 | }.start() 251 | binding.recordIb.animate().setDuration(200).alpha(1f).start() 252 | binding.slidePanel.onEnd() 253 | } 254 | } 255 | 256 | private fun clickSend() { 257 | if (micHintEnable && binding.recordTipTv.visibility == View.INVISIBLE) { 258 | binding.recordTipTv.fadeIn(ANIMATION_DURATION) 259 | if (vibrationEnable) { 260 | context.vibrate(longArrayOf(0, 10)) 261 | } 262 | postDelayed(hideRecordTipRunnable, RECORD_TIP_MILLIS) 263 | } else { 264 | removeCallbacks(hideRecordTipRunnable) 265 | } 266 | postDelayed(hideRecordTipRunnable, RECORD_TIP_MILLIS) 267 | } 268 | 269 | private var startX = 0f 270 | private var originX = 0f 271 | private var startTime = 0L 272 | private var triggeredCancel = false 273 | private var hasStartRecord = false 274 | private var locked = false 275 | private var maxScrollX = context.dip(100f) 276 | 277 | @SuppressLint("ClickableViewAccessibility") 278 | private val recordOnTouchListener = OnTouchListener { _, event -> 279 | when (event.action) { 280 | ACTION_DOWN -> { 281 | if (binding.recordCircle.sendButtonVisible) { 282 | return@OnTouchListener false 283 | } 284 | 285 | originX = event.rawX 286 | startX = event.rawX 287 | val w = binding.slidePanel.slideWidth 288 | if (w > 0) { 289 | maxScrollX = w 290 | } 291 | startTime = System.currentTimeMillis() 292 | hasStartRecord = false 293 | locked = false 294 | postDelayed(recordRunnable, RECORD_DELAY) 295 | return@OnTouchListener true 296 | } 297 | ACTION_MOVE -> { 298 | if (binding.recordCircle.sendButtonVisible || !hasStartRecord) return@OnTouchListener false 299 | 300 | val x = binding.recordCircle.setLockTranslation(event.y) 301 | if (x == 2) { 302 | ObjectAnimator.ofFloat( 303 | binding.recordCircle, "lockAnimatedTranslation", 304 | binding.recordCircle.startTranslation 305 | ).apply { 306 | duration = 150 307 | interpolator = DecelerateInterpolator() 308 | doOnEnd { locked = true } 309 | }.start() 310 | binding.slidePanel.toCancel() 311 | return@OnTouchListener false 312 | } 313 | 314 | val moveX = event.rawX 315 | if (moveX != 0f) { 316 | binding.slidePanel.slideText(startX - moveX) 317 | if (originX - moveX > maxScrollX) { 318 | removeCallbacks(recordRunnable) 319 | removeCallbacks(checkReadyRunnable) 320 | handleCancelOrEnd(true) 321 | binding.slidePanel.parent.requestDisallowInterceptTouchEvent(false) 322 | triggeredCancel = true 323 | return@OnTouchListener false 324 | } 325 | } 326 | startX = moveX 327 | } 328 | ACTION_UP, ACTION_CANCEL -> { 329 | if (triggeredCancel) { 330 | cleanUp() 331 | triggeredCancel = false 332 | return@OnTouchListener false 333 | } 334 | 335 | if (!hasStartRecord) { 336 | removeCallbacks(recordRunnable) 337 | removeCallbacks(checkReadyRunnable) 338 | cleanUp() 339 | if (!post(sendClickRunnable)) { 340 | clickSend() 341 | } 342 | } else if (hasStartRecord && !locked && System.currentTimeMillis() - startTime < 500) { 343 | removeCallbacks(recordRunnable) 344 | removeCallbacks(checkReadyRunnable) 345 | // delay check sendButtonVisible 346 | postDelayed( 347 | { 348 | if (!binding.recordCircle.sendButtonVisible) { 349 | handleCancelOrEnd(true) 350 | } else { 351 | binding.recordCircle.sendButtonVisible = false 352 | } 353 | }, 354 | 200 355 | ) 356 | return@OnTouchListener false 357 | } 358 | 359 | if (isRecording && !binding.recordCircle.sendButtonVisible) { 360 | handleCancelOrEnd(event.action == ACTION_CANCEL) 361 | } else { 362 | cleanUp() 363 | } 364 | 365 | if (!callback.isReady()) { 366 | upBeforeGrant = true 367 | } 368 | } 369 | } 370 | return@OnTouchListener true 371 | } 372 | 373 | private val sendClickRunnable = Runnable { clickSend() } 374 | 375 | private val hideRecordTipRunnable = Runnable { 376 | if (binding.recordTipTv.visibility == View.VISIBLE) { 377 | binding.recordTipTv.fadeOut(ANIMATION_DURATION) 378 | } 379 | } 380 | 381 | private val recordRunnable: Runnable by lazy { 382 | Runnable { 383 | hasStartRecord = true 384 | removeCallbacks(hideRecordTipRunnable) 385 | post(hideRecordTipRunnable) 386 | 387 | if (ContextCompat.checkSelfPermission( 388 | activity, 389 | (Manifest.permission.RECORD_AUDIO) 390 | ) != PackageManager.PERMISSION_GRANTED 391 | ) { 392 | ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), 99) 393 | return@Runnable 394 | } 395 | 396 | callback.onRecordStart() 397 | if (vibrationEnable) { 398 | context.vibrate(longArrayOf(0, 10)) 399 | } 400 | upBeforeGrant = false 401 | post(checkReadyRunnable) 402 | binding.recordIb.parent.requestDisallowInterceptTouchEvent(true) 403 | } 404 | } 405 | 406 | private val checkReadyRunnable: Runnable by lazy { 407 | Runnable { 408 | if (callback.isReady()) { 409 | if (upBeforeGrant) { 410 | upBeforeGrant = false 411 | return@Runnable 412 | } 413 | isRecording = true 414 | updateRecordCircleAndSendIcon() 415 | binding.recordCircle.setLockTranslation(10000f) 416 | } else { 417 | postDelayed(checkReadyRunnable, 50) 418 | } 419 | } 420 | } 421 | 422 | private val chatSlideCallback = object : SlidePanelView.Callback { 423 | override fun onTimeout() { 424 | handleCancelOrEnd(false) 425 | } 426 | 427 | override fun onCancel() { 428 | handleCancelOrEnd(true) 429 | } 430 | } 431 | 432 | private val recordCircleCallback = object : RecordCircleView.Callback { 433 | override fun onSend() { 434 | handleCancelOrEnd(false) 435 | } 436 | 437 | override fun onCancel() { 438 | handleCancelOrEnd(true) 439 | } 440 | } 441 | 442 | interface Callback { 443 | fun onRecordStart() 444 | fun isReady(): Boolean 445 | fun onRecordEnd() 446 | fun onRecordCancel() 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /recorder-view/src/main/java/com/tougee/recorderview/AudioScaleView.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.tougee.recorderview 4 | 5 | import android.animation.Animator 6 | import android.animation.AnimatorListenerAdapter 7 | import android.animation.ValueAnimator 8 | import android.content.Context 9 | import android.graphics.Canvas 10 | import android.graphics.Color 11 | import android.graphics.Paint 12 | import android.util.AttributeSet 13 | import android.view.View 14 | import android.view.animation.AccelerateInterpolator 15 | import java.util.concurrent.ConcurrentLinkedDeque 16 | import java.util.concurrent.CopyOnWriteArrayList 17 | 18 | class AudioScaleView : View { 19 | 20 | companion object { 21 | const val SCALE_COLOR = Color.GRAY 22 | const val COUNT = 60 23 | const val ITEM_MAX_DURATION = 1000 24 | } 25 | 26 | private val scales = CopyOnWriteArrayList() 27 | private val animQueue = ConcurrentLinkedDeque() 28 | private var count = COUNT 29 | private var itemWidth = 0f 30 | private var curStopY = 0f 31 | private var animPos = 0f 32 | private var animating = false 33 | 34 | @Suppress("MemberVisibilityCanBePrivate") 35 | var canceled = false 36 | set(value) { 37 | field = value 38 | if (value) { 39 | scales.clear() 40 | animQueue.clear() 41 | curStopY = 0f 42 | animPos = 0f 43 | animating = false 44 | invalidate() 45 | } 46 | } 47 | 48 | private var scaleColor = SCALE_COLOR 49 | 50 | private val scalePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 51 | style = Paint.Style.STROKE 52 | strokeCap = Paint.Cap.ROUND 53 | color = scaleColor 54 | } 55 | 56 | constructor(context: Context) : this(context, null) 57 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 58 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 59 | 60 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 61 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 62 | if (itemWidth == 0f) { 63 | itemWidth = measuredWidth / (count * 2f + 1) 64 | scalePaint.strokeWidth = itemWidth 65 | } 66 | } 67 | 68 | override fun onDraw(canvas: Canvas) { 69 | if (canceled) { 70 | canceled = false 71 | canvas.drawColor(Color.TRANSPARENT) 72 | return 73 | } 74 | 75 | if (scales.isEmpty()) return 76 | 77 | if (animQueue.isNotEmpty()) { 78 | if (animating) { 79 | val startX = itemWidth + animPos * itemWidth * 2 80 | canvas.drawLine(startX, height.toFloat(), startX, curStopY, scalePaint) 81 | } else { 82 | animating = true 83 | startScaleAnim() 84 | } 85 | } 86 | 87 | scales.forEachIndexed { i, h -> 88 | if (i >= animPos) return@forEachIndexed 89 | 90 | val startX = itemWidth + i * itemWidth * 2 91 | canvas.drawLine(startX, height.toFloat(), startX, height - h, scalePaint) 92 | } 93 | } 94 | 95 | private fun startScaleAnim() { 96 | val h = animQueue.peekLast() ?: return 97 | val valueAnimator = ValueAnimator.ofFloat(0f, h).apply { 98 | duration = ((h / height) * ITEM_MAX_DURATION).toLong() 99 | interpolator = AccelerateInterpolator() 100 | } 101 | valueAnimator?.addUpdateListener { 102 | curStopY = height - it.animatedValue as Float 103 | invalidate() 104 | } 105 | valueAnimator?.addListener(object : AnimatorListenerAdapter() { 106 | override fun onAnimationEnd(animation: Animator?) { 107 | animQueue.pollLast() 108 | animPos++ 109 | animating = false 110 | } 111 | 112 | override fun onAnimationCancel(animation: Animator?) { 113 | animQueue.pollLast() 114 | animPos++ 115 | animating = false 116 | } 117 | }) 118 | valueAnimator?.start() 119 | } 120 | 121 | @Suppress("unused") 122 | fun addScale(h: Float) { 123 | if (scales.size >= count) return 124 | 125 | scales.add(h) 126 | animQueue.offerFirst(h) 127 | postInvalidate() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /recorder-view/src/main/java/com/tougee/recorderview/BlinkingDrawable.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.recorderview 2 | 3 | import android.animation.ObjectAnimator 4 | import android.animation.ValueAnimator.INFINITE 5 | import android.animation.ValueAnimator.REVERSE 6 | import android.graphics.Canvas 7 | import android.graphics.ColorFilter 8 | import android.graphics.Paint 9 | import android.graphics.PixelFormat 10 | import android.graphics.Rect 11 | import android.graphics.RectF 12 | import android.graphics.drawable.Drawable 13 | 14 | class BlinkingDrawable(private val color: Int) : Drawable() { 15 | 16 | private val bounds = RectF() 17 | private var w = 0f 18 | private var h = 0f 19 | 20 | private var alphaAnimator: ObjectAnimator? = null 21 | 22 | private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 23 | color = this@BlinkingDrawable.color 24 | style = Paint.Style.FILL 25 | } 26 | 27 | override fun onBoundsChange(bounds: Rect) { 28 | this.bounds.set(bounds) 29 | w = this.bounds.width() 30 | h = this.bounds.height() 31 | } 32 | 33 | override fun draw(canvas: Canvas) { 34 | canvas.drawOval(0f, 0f, w, h, paint) 35 | } 36 | 37 | override fun setAlpha(alpha: Int) { 38 | paint.alpha = alpha 39 | invalidateSelf() 40 | } 41 | 42 | override fun getOpacity() = PixelFormat.TRANSLUCENT 43 | 44 | override fun setColorFilter(colorFilter: ColorFilter?) { 45 | paint.colorFilter = colorFilter 46 | invalidateSelf() 47 | } 48 | 49 | fun blinking() { 50 | if (alphaAnimator != null) { 51 | alphaAnimator?.cancel() 52 | } 53 | 54 | alphaAnimator = ObjectAnimator.ofInt(this, "alpha", 255, 0).apply { 55 | duration = 1000 56 | repeatMode = REVERSE 57 | repeatCount = INFINITE 58 | } 59 | alphaAnimator?.start() 60 | } 61 | 62 | fun stopBlinking() { 63 | alphaAnimator?.cancel() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /recorder-view/src/main/java/com/tougee/recorderview/Extensions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package com.tougee.recorderview 4 | 5 | import android.animation.Animator 6 | import android.content.Context 7 | import android.os.Build 8 | import android.os.VibrationEffect 9 | import android.os.Vibrator 10 | import android.view.Gravity 11 | import android.view.View 12 | import android.widget.Toast 13 | import androidx.core.view.ViewCompat 14 | import androidx.core.view.ViewPropertyAnimatorListener 15 | import java.util.Formatter 16 | import java.util.Locale 17 | 18 | fun Context.dip(value: Float): Float = (value * resources.displayMetrics.density) 19 | 20 | fun Context.dipInt(value: Float): Int = dip(value).toInt() 21 | 22 | inline fun Context.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT): Toast { 23 | return Toast.makeText(this, text, duration).apply { 24 | setGravity(Gravity.CENTER, 0, 0) 25 | show() 26 | } 27 | } 28 | 29 | @Suppress("DEPRECATION") 30 | fun Context.vibrate(pattern: LongArray) { 31 | if (Build.VERSION.SDK_INT >= 26) { 32 | (getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(VibrationEffect.createWaveform(pattern, -1)) 33 | } else { 34 | (getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(pattern, -1) 35 | } 36 | } 37 | 38 | fun Long.formatMillis(): String { 39 | val formatBuilder = StringBuilder() 40 | val formatter = Formatter(formatBuilder, Locale.getDefault()) 41 | getStringForTime(formatBuilder, formatter, this) 42 | return formatBuilder.toString() 43 | } 44 | 45 | /** 46 | * Returns the specified millisecond time formatted as a string. 47 | * 48 | * @param builder The builder that `formatter` will write to. 49 | * @param formatter The formatter. 50 | * @param timeMs The time to format as a string, in milliseconds. 51 | * @return The time formatted as a string. 52 | */ 53 | fun getStringForTime(builder: StringBuilder, formatter: Formatter, timeMs: Long): String { 54 | val totalSeconds = (timeMs + 500) / 1000 55 | val seconds = totalSeconds % 60 56 | val minutes = totalSeconds / 60 % 60 57 | val hours = totalSeconds / 3600 58 | builder.setLength(0) 59 | return if (hours > 0) 60 | formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() 61 | else 62 | formatter.format("%02d:%02d", minutes, seconds).toString() 63 | } 64 | 65 | /** 66 | * Add an action which will be invoked when the animation has ended. 67 | * 68 | * @return the [Animator.AnimatorListener] added to the Animator 69 | * @see Animator.end 70 | */ 71 | fun Animator.doOnEnd(action: (animator: Animator) -> Unit) = addListener(onEnd = action) 72 | 73 | /** 74 | * Add a listener to this Animator using the provided actions. 75 | */ 76 | fun Animator.addListener( 77 | onEnd: ((animator: Animator) -> Unit)? = null, 78 | onStart: ((animator: Animator) -> Unit)? = null, 79 | onCancel: ((animator: Animator) -> Unit)? = null, 80 | onRepeat: ((animator: Animator) -> Unit)? = null 81 | ): Animator.AnimatorListener { 82 | val listener = object : Animator.AnimatorListener { 83 | override fun onAnimationRepeat(animator: Animator) { 84 | onRepeat?.invoke(animator) 85 | } 86 | 87 | override fun onAnimationEnd(animator: Animator) { 88 | onEnd?.invoke(animator) 89 | } 90 | 91 | override fun onAnimationCancel(animator: Animator) { 92 | onCancel?.invoke(animator) 93 | } 94 | 95 | override fun onAnimationStart(animator: Animator) { 96 | onStart?.invoke(animator) 97 | } 98 | } 99 | addListener(listener) 100 | return listener 101 | } 102 | 103 | fun View.fadeIn(duration: Long) { 104 | this.visibility = View.VISIBLE 105 | this.alpha = 0f 106 | ViewCompat.animate(this).alpha(1f).setDuration(duration).setListener(object : ViewPropertyAnimatorListener { 107 | override fun onAnimationStart(view: View) { 108 | } 109 | 110 | override fun onAnimationEnd(view: View) { 111 | } 112 | 113 | override fun onAnimationCancel(view: View) {} 114 | }).start() 115 | } 116 | 117 | fun View.fadeOut(duration: Long, delay: Long = 0) { 118 | this.alpha = 1f 119 | ViewCompat.animate(this).alpha(0f).setStartDelay(delay).setDuration(duration).setListener(object : ViewPropertyAnimatorListener { 120 | override fun onAnimationStart(view: View) { 121 | @Suppress("DEPRECATION") 122 | view.isDrawingCacheEnabled = true 123 | } 124 | 125 | override fun onAnimationEnd(view: View) { 126 | view.visibility = View.INVISIBLE 127 | view.alpha = 0f 128 | @Suppress("DEPRECATION") 129 | view.isDrawingCacheEnabled = false 130 | } 131 | 132 | override fun onAnimationCancel(view: View) {} 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /recorder-view/src/main/java/com/tougee/recorderview/RecordCircleView.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.recorderview 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.graphics.PorterDuff 9 | import android.graphics.PorterDuffColorFilter 10 | import android.graphics.Rect 11 | import android.graphics.RectF 12 | import android.graphics.drawable.Drawable 13 | import android.util.AttributeSet 14 | import android.view.MotionEvent 15 | import android.view.View 16 | import androidx.core.content.ContextCompat 17 | import androidx.core.content.res.ResourcesCompat 18 | import kotlin.math.max 19 | import kotlin.math.min 20 | 21 | class RecordCircleView : View { 22 | 23 | private val colorCircle: Int by lazy { ContextCompat.getColor(context, R.color.color_record_circle_bg) } 24 | private val colorLock: Int by lazy { ContextCompat.getColor(context, R.color.text_gray) } 25 | private val colorOrange: Int by lazy { ContextCompat.getColor(context, R.color.color_blink) } 26 | 27 | val circlePaint: Paint by lazy { 28 | Paint(Paint.ANTI_ALIAS_FLAG).apply { 29 | color = colorCircle 30 | } 31 | } 32 | 33 | val cancelIconPaint: Paint by lazy { 34 | Paint(Paint.ANTI_ALIAS_FLAG).apply { 35 | color = colorOrange 36 | } 37 | } 38 | 39 | private val rect = RectF() 40 | private var sendClickBound = Rect() 41 | var scale = 0f 42 | set(value) { 43 | field = value 44 | invalidate() 45 | } 46 | private var amplitude = 0f 47 | private var animateToAmplitude = 0f 48 | private var animateAmplitudeDiff = 0f 49 | private var lastUpdateTime = 0L 50 | private var lockAnimatedTranslation = 0f 51 | set(value) { 52 | field = value 53 | invalidate() 54 | } 55 | var startTranslation = 0f 56 | var sendButtonVisible = false 57 | private var pressedEnd = false 58 | private var pressedSend = false 59 | 60 | lateinit var callback: Callback 61 | 62 | var audioDrawable: Drawable = ResourcesCompat.getDrawable(resources, R.drawable.ic_record_mic_white, null)!! 63 | private val sendDrawable: Drawable by lazy { ResourcesCompat.getDrawable(resources, R.drawable.ic_send_white_24dp, null)!! } 64 | 65 | private val lockDrawable: Drawable by lazy { 66 | ResourcesCompat.getDrawable(resources, R.drawable.lock_middle, null)!!.apply { 67 | colorFilter = PorterDuffColorFilter(colorLock, PorterDuff.Mode.MULTIPLY) 68 | } 69 | } 70 | private val lockTopDrawable: Drawable by lazy { 71 | ResourcesCompat.getDrawable(resources, R.drawable.lock_top, null)!!.apply { 72 | colorFilter = PorterDuffColorFilter(colorLock, PorterDuff.Mode.MULTIPLY) 73 | } 74 | } 75 | private val lockArrowDrawable: Drawable by lazy { 76 | ResourcesCompat.getDrawable(resources, R.drawable.lock_arrow, null)!!.apply { 77 | colorFilter = PorterDuffColorFilter(colorLock, PorterDuff.Mode.MULTIPLY) 78 | } 79 | } 80 | private val lockBackgroundDrawable: Drawable by lazy { 81 | ResourcesCompat.getDrawable(resources, R.drawable.lock_round, null)!!.apply { 82 | colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY) 83 | } 84 | } 85 | private val lockShadowDrawable: Drawable by lazy { 86 | ResourcesCompat.getDrawable(resources, R.drawable.lock_round_shadow, null)!!.apply { 87 | colorFilter = PorterDuffColorFilter(colorCircle, PorterDuff.Mode.MULTIPLY) 88 | } 89 | } 90 | 91 | constructor(context: Context) : this(context, null) 92 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 93 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 94 | 95 | fun setAmplitude(value: Double) { 96 | animateToAmplitude = min(100.0, value).toFloat() / 100.0f 97 | animateAmplitudeDiff = (animateToAmplitude - amplitude) / 150.0f 98 | lastUpdateTime = System.currentTimeMillis() 99 | invalidate() 100 | } 101 | 102 | fun setSendButtonInvisible() { 103 | sendButtonVisible = false 104 | invalidate() 105 | } 106 | 107 | fun setLockTranslation(value: Float): Int { 108 | if (value == 10000f) { 109 | sendButtonVisible = false 110 | lockAnimatedTranslation = -1f 111 | startTranslation = -1f 112 | invalidate() 113 | return 0 114 | } else { 115 | if (sendButtonVisible) { 116 | return 2 117 | } 118 | if (lockAnimatedTranslation == -1f) { 119 | startTranslation = value 120 | } 121 | lockAnimatedTranslation = value 122 | invalidate() 123 | if (startTranslation - lockAnimatedTranslation >= context.dipInt(57f)) { 124 | sendButtonVisible = true 125 | return 2 126 | } 127 | } 128 | return 1 129 | } 130 | 131 | @SuppressLint("ClickableViewAccessibility") 132 | override fun onTouchEvent(event: MotionEvent): Boolean { 133 | if (sendButtonVisible) { 134 | val x = event.x.toInt() 135 | val y = event.y.toInt() 136 | if (event.action == MotionEvent.ACTION_DOWN) { 137 | pressedEnd = lockBackgroundDrawable.bounds.contains(x, y) 138 | pressedSend = sendClickBound.contains(x, y) 139 | if (pressedEnd || pressedSend) { 140 | return true 141 | } 142 | } else if (pressedEnd) { 143 | if (event.action == MotionEvent.ACTION_UP) { 144 | if (lockBackgroundDrawable.bounds.contains(x, y)) { 145 | callback.onCancel() 146 | } 147 | } 148 | return true 149 | } else if (pressedSend) { 150 | if (event.action == MotionEvent.ACTION_UP) { 151 | if (sendClickBound.contains(x, y)) { 152 | callback.onSend() 153 | } 154 | } 155 | return true 156 | } 157 | } 158 | return false 159 | } 160 | 161 | override fun onDraw(canvas: Canvas) { 162 | val cx = measuredWidth / 2 163 | var cy = context.dipInt(170f) 164 | var yAdd = 0f 165 | 166 | if (lockAnimatedTranslation != 10000f) { 167 | yAdd = max(0f, startTranslation - lockAnimatedTranslation) 168 | if (yAdd > context.dipInt(57f)) { 169 | yAdd = context.dipInt(57f).toFloat() 170 | } 171 | } 172 | cy -= yAdd.toInt() 173 | 174 | val sc: Float 175 | val alpha: Float 176 | when { 177 | scale <= 0.5f -> { 178 | sc = scale / 0.5f 179 | alpha = sc 180 | } 181 | scale <= 0.75f -> { 182 | sc = 1.0f - (scale - 0.5f) / 0.25f * 0.1f 183 | alpha = 1f 184 | } 185 | else -> { 186 | sc = 0.9f + (scale - 0.75f) / 0.25f * 0.1f 187 | alpha = 1f 188 | } 189 | } 190 | val dt = System.currentTimeMillis() - lastUpdateTime 191 | if (animateToAmplitude != amplitude) { 192 | amplitude += animateAmplitudeDiff * dt 193 | if (animateAmplitudeDiff > 0) { 194 | if (amplitude > animateToAmplitude) { 195 | amplitude = animateToAmplitude 196 | } 197 | } else { 198 | if (amplitude < animateToAmplitude) { 199 | amplitude = animateToAmplitude 200 | } 201 | } 202 | invalidate() 203 | } 204 | lastUpdateTime = System.currentTimeMillis() 205 | if (amplitude != 0f) { 206 | canvas.drawCircle(measuredWidth / 2.0f, cy.toFloat(), (context.dipInt(42f) + context.dipInt(20f) * amplitude) * scale, cancelIconPaint) 207 | } 208 | canvas.drawCircle(measuredWidth / 2.0f, cy.toFloat(), context.dipInt(42f) * sc, circlePaint) 209 | val drawable: Drawable = if (sendButtonVisible) { 210 | sendDrawable 211 | } else { 212 | audioDrawable 213 | } 214 | drawable.setBounds(cx - drawable.intrinsicWidth / 2, cy - drawable.intrinsicHeight / 2, cx + drawable.intrinsicWidth / 2, cy + drawable.intrinsicHeight / 2) 215 | sendClickBound.set(cx - context.dipInt(42f), cy - context.dipInt(42f), cx + context.dipInt(42f), cy + context.dipInt(42f)) 216 | drawable.alpha = (255 * alpha).toInt() 217 | drawable.draw(canvas) 218 | 219 | val moveProgress = 1.0f - yAdd / context.dipInt(57f) 220 | val moveProgress2 = max(0.0f, 1.0f - yAdd / context.dipInt(57f) * 2) 221 | val lockSize: Int 222 | val lockY: Int 223 | val lockTopY: Int 224 | val lockMiddleY: Int 225 | val lockArrowY: Int 226 | var intAlpha = (alpha * 255).toInt() 227 | if (sendButtonVisible) { 228 | lockSize = context.dipInt(31f) 229 | lockY = context.dipInt(57f) + (context.dipInt(30f) * (1.0f - sc) - yAdd + context.dipInt(20f) * moveProgress).toInt() 230 | lockTopY = lockY + context.dipInt(5f) 231 | lockMiddleY = lockY + context.dipInt(11f) 232 | lockArrowY = lockY + context.dipInt(25f) 233 | 234 | intAlpha *= (yAdd / context.dipInt(57f)).toInt() 235 | lockBackgroundDrawable.alpha = 255 236 | lockShadowDrawable.alpha = 255 237 | lockTopDrawable.alpha = intAlpha 238 | lockDrawable.alpha = intAlpha 239 | lockArrowDrawable.alpha = (intAlpha * moveProgress2).toInt() 240 | } else { 241 | lockSize = context.dipInt(31f) + (context.dipInt(29f) * moveProgress).toInt() 242 | lockY = context.dipInt(57f) + (context.dipInt(30f) * (1.0f - sc)).toInt() - yAdd.toInt() 243 | lockTopY = lockY + context.dipInt(5f) + (context.dipInt(4f) * moveProgress).toInt() 244 | lockMiddleY = lockY + context.dipInt(11f) + (context.dipInt(10f) * moveProgress).toInt() 245 | lockArrowY = lockY + context.dipInt(25f) + (context.dipInt(16f) * moveProgress).toInt() 246 | 247 | lockBackgroundDrawable.alpha = intAlpha 248 | lockShadowDrawable.alpha = intAlpha 249 | lockTopDrawable.alpha = intAlpha 250 | lockDrawable.alpha = intAlpha 251 | lockArrowDrawable.alpha = (intAlpha * moveProgress2).toInt() 252 | } 253 | 254 | lockBackgroundDrawable.setBounds(cx - context.dipInt(15f), lockY, cx + context.dipInt(15f), lockY + lockSize) 255 | lockBackgroundDrawable.draw(canvas) 256 | lockShadowDrawable.setBounds(cx - context.dipInt(16f), lockY - context.dipInt(1f), cx + context.dipInt(16f), lockY + lockSize + context.dipInt(1f)) 257 | lockShadowDrawable.draw(canvas) 258 | lockTopDrawable.setBounds(cx - context.dipInt(6f), lockTopY, cx + context.dipInt(6f), lockTopY + context.dipInt(14f)) 259 | lockTopDrawable.draw(canvas) 260 | lockDrawable.setBounds(cx - context.dipInt(7f), lockMiddleY, cx + context.dipInt(7f), lockMiddleY + context.dipInt(12f)) 261 | lockDrawable.draw(canvas) 262 | lockArrowDrawable.setBounds(cx - context.dipInt(7.5f), lockArrowY, cx + context.dipInt(7.5f), lockArrowY + context.dipInt(9f)) 263 | lockArrowDrawable.draw(canvas) 264 | if (sendButtonVisible) { 265 | rect.set(cx - context.dipInt(6.5f).toFloat(), lockY + context.dipInt(9f).toFloat(), cx + context.dipInt(6.5f).toFloat(), lockY.toFloat() + context.dipInt((9 + 13).toFloat())) 266 | canvas.drawRoundRect(rect, context.dipInt(1f).toFloat(), context.dipInt(1f).toFloat(), cancelIconPaint) 267 | } 268 | } 269 | 270 | interface Callback { 271 | fun onSend() 272 | fun onCancel() 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /recorder-view/src/main/java/com/tougee/recorderview/SlidePanelView.kt: -------------------------------------------------------------------------------- 1 | package com.tougee.recorderview 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.AnimatorSet 6 | import android.animation.ObjectAnimator 7 | import android.content.Context 8 | import android.graphics.Color 9 | import android.util.AttributeSet 10 | import android.view.LayoutInflater 11 | import android.view.animation.AccelerateInterpolator 12 | import android.view.animation.DecelerateInterpolator 13 | import android.widget.RelativeLayout 14 | import androidx.core.content.ContextCompat 15 | import com.tougee.recorderview.databinding.ViewSlidePanelBinding 16 | import kotlin.math.abs 17 | 18 | class SlidePanelView : RelativeLayout { 19 | 20 | private val blinkSize = context.resources.getDimensionPixelSize(R.dimen.blink_size) 21 | private var blinkingDrawable: BlinkingDrawable? = null 22 | private var timeValue = 0 23 | private var toCanceled = false 24 | private var isEnding = false 25 | 26 | var timeoutSeconds = 60 27 | 28 | var callback: Callback? = null 29 | lateinit var binding: ViewSlidePanelBinding 30 | 31 | constructor(context: Context) : this(context, null) 32 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 33 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 34 | binding = ViewSlidePanelBinding.inflate(LayoutInflater.from(context), this) 35 | setBackgroundColor(Color.WHITE) 36 | isClickable = true 37 | 38 | updateBlinkDrawable(ContextCompat.getColor(context, R.color.color_blink)) 39 | binding.cancelTv.setOnClickListener { callback?.onCancel() } 40 | binding.timeTv.text = 0L.formatMillis() 41 | } 42 | 43 | fun onStart() { 44 | visibility = VISIBLE 45 | translationX = measuredWidth.toFloat() 46 | val animSet = AnimatorSet().apply { 47 | interpolator = DecelerateInterpolator() 48 | duration = 200 49 | addListener(object : AnimatorListenerAdapter() { 50 | override fun onAnimationEnd(animation: Animator?) { 51 | blinkingDrawable?.blinking() 52 | postDelayed(updateTimeRunnable, 200) 53 | } 54 | }) 55 | } 56 | animSet.playTogether( 57 | ObjectAnimator.ofFloat(this, "translationX", 0f), 58 | ObjectAnimator.ofFloat(this, "alpha", 1f) 59 | ) 60 | animSet.start() 61 | } 62 | 63 | val slideWidth by lazy { 64 | val location = IntArray(2) 65 | binding.slideLl.getLocationOnScreen(location) 66 | location[0] - context.dip(64f) 67 | } 68 | 69 | fun slideText(x: Float) { 70 | val preX = binding.slideLl.translationX 71 | if (preX - x > 0) { 72 | binding.slideLl.translationX = 0f 73 | } else { 74 | binding.slideLl.translationX -= x * 1.5f 75 | } 76 | val alpha = abs(binding.slideLl.translationX * 1.5f / binding.slideLl.width) 77 | binding.slideLl.alpha = 1 - alpha 78 | } 79 | 80 | fun toCancel() { 81 | if (isEnding) return 82 | 83 | val animSet = AnimatorSet().apply { 84 | duration = 200 85 | interpolator = DecelerateInterpolator() 86 | } 87 | animSet.playTogether( 88 | ObjectAnimator.ofFloat(binding.slideLl, "alpha", 0f), 89 | ObjectAnimator.ofFloat(binding.slideLl, "translationY", context.dip(20f)), 90 | ObjectAnimator.ofFloat(binding.cancelTv, "alpha", 1f), 91 | ObjectAnimator.ofFloat(binding.cancelTv, "translationY", -context.dip(20f), 0f) 92 | ) 93 | animSet.start() 94 | toCanceled = true 95 | } 96 | 97 | fun onEnd() { 98 | isEnding = true 99 | val animSet = AnimatorSet().apply { 100 | interpolator = AccelerateInterpolator() 101 | duration = 200 102 | addListener(object : AnimatorListenerAdapter() { 103 | override fun onAnimationEnd(animation: Animator?) { 104 | handleEnd() 105 | isEnding = false 106 | } 107 | 108 | override fun onAnimationCancel(animation: Animator?) { 109 | handleEnd() 110 | isEnding = false 111 | } 112 | }) 113 | } 114 | animSet.playTogether( 115 | ObjectAnimator.ofFloat(this, "translationX", measuredWidth.toFloat()), 116 | ObjectAnimator.ofFloat(this, "alpha", 0f) 117 | ) 118 | animSet.start() 119 | } 120 | 121 | fun updateBlinkDrawable(color: Int) { 122 | blinkingDrawable = BlinkingDrawable(color).apply { 123 | setBounds(0, 0, blinkSize, blinkSize) 124 | } 125 | binding.timeTv.setCompoundDrawables(blinkingDrawable, null, null, null) 126 | } 127 | 128 | private fun handleEnd() { 129 | toCanceled = false 130 | binding.cancelTv.alpha = 0f 131 | binding.cancelTv.translationY = 0f 132 | binding.slideLl.alpha = 1f 133 | binding.slideLl.translationY = 0f 134 | binding.slideLl.translationX = 0f 135 | 136 | blinkingDrawable?.stopBlinking() 137 | removeCallbacks(updateTimeRunnable) 138 | timeValue = 0 139 | binding.timeTv.text = 0L.formatMillis() 140 | } 141 | 142 | private val updateTimeRunnable: Runnable by lazy { 143 | Runnable { 144 | if (timeValue >= timeoutSeconds) { 145 | callback?.onTimeout() 146 | return@Runnable 147 | } 148 | 149 | timeValue++ 150 | binding.timeTv.text = (timeValue * 1000L).formatMillis() 151 | postDelayed(updateTimeRunnable, 1000) 152 | } 153 | } 154 | 155 | interface Callback { 156 | fun onTimeout() 157 | fun onCancel() 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/bg_record_tip.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/bg_record_tip.9.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_arrow.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_close.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_middle.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_open.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_round.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_round.9.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_round_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_round_shadow.9.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable-xxhdpi/lock_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tougee/AudioRecorderView/d6a4a434e16af67e988e9d5e17188ffc117d63ed/recorder-view/src/main/res/drawable-xxhdpi/lock_top.png -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/bg_mic_expand.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_blinking.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_chevron_left_gray_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_mic_blue_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_record_mic_black.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_record_mic_white.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/drawable/ic_send_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/layout/view_audio_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 24 | 25 | 32 | 33 | 44 | 45 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/layout/view_slide_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 28 | 29 | 38 | 39 | 44 | 45 | 52 | 53 | 54 | 55 | 66 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF5E5E 4 | #D6D6D6 5 | #007AFF 6 | #EE1987FF 7 | #bbbbbb 8 | 9 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 12dp 5 | 8dp 6 | 8dp 7 | 52dp 8 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | recorder-view 3 | Slide to Cancel 4 | Cancel 5 | %d\' 6 | Hold to record, release to send 7 | 8 | -------------------------------------------------------------------------------- /recorder-view/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':example', ':recorder-view' 2 | --------------------------------------------------------------------------------