├── .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 |
--------------------------------------------------------------------------------