Browse Source

增加录音功能

zk 1 year ago
parent
commit
612e9fb356

+ 36 - 16
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java

@@ -1,10 +1,15 @@
 package com.atmob.voiceai.module.clonevoice;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.text.method.ScrollingMovementMethod;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewTreeObserver;
 import android.widget.SeekBar;
 
 import androidx.activity.result.ActivityResult;
@@ -26,6 +31,7 @@ import com.atmob.voiceai.dialog.CloneDeleteSureDialog;
 import com.atmob.voiceai.dialog.RecordingInstructionsDialog;
 import com.atmob.voiceai.utils.ActivityForResultUtil;
 import com.atmob.voiceai.utils.AudioRecorder;
+import com.atmob.voiceai.utils.FileUtils;
 import com.atmob.voiceai.utils.ToastUtil;
 import com.gyf.immersionbar.ImmersionBar;
 
@@ -74,9 +80,7 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
         binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                if (fromUser) {
-                    player.seekTo(progress);
-                }
+
             }
 
             @Override
@@ -87,10 +91,9 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
             @Override
             public void onStopTrackingTouch(SeekBar seekBar) {
                 cloneVoiceViewModel.setSeekbarChanging(false);
-                cloneVoiceViewModel.setSeekbarTo(seekBar.getProgress());
+                player.seekTo(seekBar.getProgress());
             }
         });
-        cloneVoiceViewModel.startMediaTimer();
     }
 
 
@@ -106,12 +109,21 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
             }
 
             @Override
+            public void onEvents(Player player, Player.Events events) {
+                Player.Listener.super.onEvents(player, events);
+                if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
+                    boolean isLoading = player.isLoading();
+                    if (!isLoading) {
+                        cloneVoiceViewModel.setTotalDuration(player.getDuration());
+                        binding.voiceSeekBar.setMax((int) player.getDuration());
+                        cloneVoiceViewModel.startMediaTimer();
+                    }
+                }
+            }
+
+            @Override
             public void onPlaybackStateChanged(int state) {
-                AtmobLog.d("zk", "onPlaybackStateChanged: " + state);
-                if (Player.STATE_READY == state) {
-                    cloneVoiceViewModel.setTotalDuration(player.getDuration());
-                    binding.voiceSeekBar.setMax((int) player.getDuration());
-                } else if (Player.STATE_ENDED == state) {
+                if (Player.STATE_ENDED == state) {
                     setVoicePlayEnd();
                 }
             }
@@ -153,7 +165,7 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
     }
 
     private void initScrollView() {
-        maxScrollY = (int) SizeUtil.dp2px(89);
+        maxScrollY = (int) SizeUtil.dp2px(60);
         binding.scrollView.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
             if (scrollY > maxScrollY) {
                 binding.vStatusBar.setAlpha(1f);
@@ -195,11 +207,18 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
     }
 
     private void startRecordAudio() {
-        if (audioRecorder == null) {
-            audioRecorder = new AudioRecorder();
-            audioRecorder.setMaxDuration(CloneVoiceViewModel.RECORD_AUDIO_MAX_DURATION);
-//            audioRecorder.setupMediaRecorder();
-        }
+        AtmobLog.d("zk", "startRecordAudio");
+        binding.scrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                binding.scrollView.fullScroll(NestedScrollView.FOCUS_DOWN);
+                binding.scrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+            }
+        });
+        binding.tvRecordExample.startScroll();
+        audioRecorder = new AudioRecorder();
+        audioRecorder.setMaxDuration(CloneVoiceViewModel.RECORD_AUDIO_MAX_DURATION);
+        audioRecorder.setupMediaRecorder(cloneVoiceViewModel.getRecordAudioFilePath());
         try {
             audioRecorder.startRecording();
         } catch (IOException e) {
@@ -208,6 +227,7 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
     }
 
     private void stopRecordAudio() {
+        binding.tvRecordExample.stopScroll();
         if (audioRecorder != null) {
             audioRecorder.stopRecording();
         }

+ 134 - 25
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java

@@ -1,17 +1,22 @@
 package com.atmob.voiceai.module.clonevoice;
 
 import android.Manifest;
+import android.annotation.SuppressLint;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.CountDownTimer;
 import android.os.Handler;
 import android.os.Looper;
 
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MediatorLiveData;
 import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
 
 import com.atmob.app.lib.base.BaseViewModel;
 import com.atmob.app.lib.livedata.SingleLiveEvent;
 import com.atmob.common.data.KVUtils;
+import com.atmob.common.logging.AtmobLog;
 import com.atmob.common.runtime.ActivityUtil;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
@@ -22,11 +27,13 @@ import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.module.cloning.VoiceCloningActivity;
 import com.atmob.voiceai.module.setting.SettingActivity;
 import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
+import com.atmob.voiceai.utils.DateUtil;
 import com.atmob.voiceai.utils.FileUtils;
 import com.atmob.voiceai.utils.PermissionUtil;
 import com.atmob.voiceai.utils.SpannableUtil;
 import com.atmob.voiceai.utils.ToastUtil;
 
+import java.io.File;
 import java.util.List;
 import java.util.Timer;
 import java.util.TimerTask;
@@ -43,13 +50,20 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
 public class CloneVoiceViewModel extends BaseViewModel {
 
 
-    public static final int RECORD_AUDIO_MAX_DURATION = 25 * 1000;
+    public static final int RECORD_AUDIO_MAX_DURATION = 35 * 1000;
+    public final int RECORD_AUDIO_MIN_DURATION = 20 * 1000;
     public static final String PERMISSION_AUDIO = Manifest.permission.RECORD_AUDIO;
     public final String RECORD_DIALOG_SHOW = "record_dialog_show";
 
     public static final int FILE_UPLOAD = 1;
     public static final int RECORD_UPLOAD = 2;
 
+    private final LiveData<String> recordStateDesc;
+
+    private final LiveData<String> recordTimeTxt;
+    private final MutableLiveData<Integer> recordProgress = new MutableLiveData<>();
+    private final MutableLiveData<Drawable> recordProgressDrawable = new MutableLiveData<>();
+    private final MutableLiveData<Boolean> showStopRecord = new MutableLiveData<>();
     private final MutableLiveData<String> uploadPreviewName = new MutableLiveData<>();
     private final MutableLiveData<String> uploadPreviewSize = new MutableLiveData<>();
     private final SingleLiveEvent<Integer> deleteEvent = new SingleLiveEvent<>();
@@ -58,7 +72,6 @@ public class CloneVoiceViewModel extends BaseViewModel {
     private final SingleLiveEvent<?> startRecordAudioEvent = new SingleLiveEvent<>();
     private final SingleLiveEvent<?> stopRecordAudioEvent = new SingleLiveEvent<>();
     private final SingleLiveEvent<?> showRecordDialogEvent = new SingleLiveEvent<>();
-    private final MutableLiveData<Integer> seekBarProgress = new MutableLiveData<>();
     private final MutableLiveData<Boolean> isPlay = new MutableLiveData<>();
     private final MutableLiveData<Long> totalDuration = new MutableLiveData<>();
     private final MutableLiveData<Long> currentDuration = new MutableLiveData<>();
@@ -75,8 +88,8 @@ public class CloneVoiceViewModel extends BaseViewModel {
     private boolean isSeekbarChanging;
     private Timer timer;
 
-    private Handler countDownHandler;
-
+    File recordFile;
+    private CountDownTimer countDownTimer;
 
     @Inject
     public CloneVoiceViewModel(MemberRepository memberRepository, CloneRepository cloneRepository) {
@@ -84,6 +97,49 @@ public class CloneVoiceViewModel extends BaseViewModel {
         this.cloneRepository = cloneRepository;
         initCloneTipTxt();
         initCloneState();
+        recordStateDesc = Transformations.map(recordProgress, progress -> {
+            if (progress == null) {
+                return "";
+            }
+            if (progress >= 0 && progress <= 20) {
+                return ContextUtil.getContext().getString(R.string.record_can_not);
+            } else if (progress > 20 && progress < 30) {
+                return ContextUtil.getContext().getString(R.string.record_maybe_best);
+            } else {
+                return ContextUtil.getContext().getString(R.string.record_best);
+            }
+        });
+        recordTimeTxt = Transformations.map(recordProgress, progress -> {
+            if (progress == null) {
+                return "00:00.00";
+            }
+            return DateUtil.formatNormalDate("mm:ss.SS", progress);
+        });
+    }
+
+
+    public LiveData<String> getRecordTimeTxt() {
+        return recordTimeTxt;
+    }
+
+    public LiveData<String> getRecordStateDesc() {
+        return recordStateDesc;
+    }
+
+    public LiveData<Integer> getRecordProgress() {
+        return recordProgress;
+    }
+
+    public LiveData<Drawable> getRecordProgressDrawable() {
+        return recordProgressDrawable;
+    }
+
+    public long getRecordMaxTime() {
+        return RECORD_AUDIO_MAX_DURATION;
+    }
+
+    public LiveData<Boolean> getShowStopRecord() {
+        return showStopRecord;
     }
 
     public LiveData<?> getStopRecordAudioEvent() {
@@ -118,9 +174,6 @@ public class CloneVoiceViewModel extends BaseViewModel {
         return refreshAudioCurrentProgress;
     }
 
-    public LiveData<Integer> getSeekBarProgress() {
-        return seekBarProgress;
-    }
 
     public LiveData<Boolean> getIsPlay() {
         return isPlay;
@@ -168,7 +221,7 @@ public class CloneVoiceViewModel extends BaseViewModel {
 
     public void startMediaTimer() {
         if (timer != null) {
-            timer.cancel();
+            return;
         }
         timer = new Timer();
         timer.schedule(new TimerTask() {
@@ -182,10 +235,6 @@ public class CloneVoiceViewModel extends BaseViewModel {
         }, 0, 25);
     }
 
-    public void setSeekbarTo(int progress) {
-        this.seekBarProgress.setValue(progress);
-    }
-
 
     public void setCurrentDuration(long duration) {
         if (duration >= 0) {
@@ -265,10 +314,10 @@ public class CloneVoiceViewModel extends BaseViewModel {
         //判断是否符合要求
         //文件小于3M  时长大于20s且小于30s
         long durationLong = FileUtils.getAudioDurationFromUri(ContextUtil.getContext(), uri);
-        if (durationLong < 20000 || durationLong > 30000) {
-            ToastUtil.show(R.string.clone_voice_duration_error, ToastUtil.LENGTH_SHORT);
-            return;
-        }
+//        if (durationLong < 20000 || durationLong > 30000) {
+//            ToastUtil.show(R.string.clone_voice_duration_error, ToastUtil.LENGTH_SHORT);
+//            return;
+//        }
         long fileSize = FileUtils.getFileSizeFromUri(ContextUtil.getContext(), uri);
         if (fileSize > 3 * 1024 * 1024) {
             ToastUtil.show(R.string.clone_voice_size_error, ToastUtil.LENGTH_SHORT);
@@ -282,12 +331,11 @@ public class CloneVoiceViewModel extends BaseViewModel {
     }
 
     public void onLocalReSelectClick() {
-        this.uploadVoiceUri.setValue(null);
-        cloneState.setValue(CloneState.NO_UPLOAD_DATA);
+        returnOriginalState();
     }
 
     public void onReRecordClick() {
-
+        returnOriginalState();
     }
 
     public void onCloneVoiceClick(int state) {
@@ -322,16 +370,30 @@ public class CloneVoiceViewModel extends BaseViewModel {
         }
     }
 
+
+    @SuppressLint("UseCompatLoadingForDrawables")
     public void startRecordAudio() {
+        recordProgressDrawable.setValue(ContextUtil.getContext().getDrawable(R.drawable.bg_progress_bar_horizontal_record_0_20));
         startRecordAudioEvent.call();
+        recordProgress.setValue(0);
+        cloneState.setValue(CloneState.RECORDING_VOICE);
+        showStopRecord.setValue(true);
         startCountDown();
     }
 
     private void startCountDown() {
-        if (countDownHandler == null) {
-            countDownHandler = new Handler(Looper.getMainLooper());
-        }
-        countDownHandler.postDelayed(stopRecordAudioEvent::call, RECORD_AUDIO_MAX_DURATION);
+        countDownTimer = new CountDownTimer(RECORD_AUDIO_MAX_DURATION, 25) {
+            @Override
+            public void onTick(long millisUntilFinished) {
+                recordProgress.setValue((int) (RECORD_AUDIO_MAX_DURATION - millisUntilFinished));
+            }
+
+            @Override
+            public void onFinish() {
+                stopRecordAudio(true);
+            }
+        };
+        countDownTimer.start();
     }
 
     public void onDialogKnowClick() {
@@ -346,8 +408,55 @@ public class CloneVoiceViewModel extends BaseViewModel {
             timer.cancel();
             timer = null;
         }
-        if (countDownHandler != null) {
-            countDownHandler.removeCallbacksAndMessages(null);
+        if (countDownTimer != null) {
+            countDownTimer.cancel();
+        }
+    }
+
+    public String getRecordAudioFilePath() {
+        File cacheFile = FileUtils.getVoiceDownloadSaveRootFile();
+        recordFile = new File(cacheFile, "record_audio");
+        return recordFile.getPath();
+    }
+
+    public void onRecordingClick(boolean isRecording, int stopProgress) {
+        boolean isReset = stopProgress < RECORD_AUDIO_MIN_DURATION;
+        if (isReset) {
+            showStopRecord.setValue(false);
+        }
+        if (isRecording) {
+            stopRecordAudio(!isReset);
+        } else {
+            returnOriginalState();
+        }
+    }
+
+    private void stopRecordAudio(boolean isOk) {
+        if (countDownTimer != null) {
+            countDownTimer.cancel();
+        }
+        stopRecordAudioEvent.call();
+        if (isOk) {
+            Uri uri = Uri.fromFile(recordFile);
+            long fileSize = recordFile.length();
+            uploadVoiceUri.setValue(uri);
+            uploadPreviewName.setValue(recordFile.getName());
+            uploadPreviewSize.setValue(FileUtils.formatShortBytes(fileSize));
+            cloneState.setValue(CloneState.UPLOAD_CHOICE_RECORDING);
+        }
+    }
+
+    private void returnOriginalState() {
+        this.uploadVoiceUri.setValue(null);
+        this.isPlay.setValue(false);
+        setCurrentDuration(0);
+        cloneState.setValue(CloneState.NO_UPLOAD_DATA);
+        if (countDownTimer != null) {
+            countDownTimer.cancel();
+        }
+        if (timer != null) {
+            timer.cancel();
+            timer = null;
         }
     }
 }

+ 15 - 8
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java

@@ -96,9 +96,7 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
         binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                if (fromUser) {
-                    player.seekTo(progress);
-                }
+
             }
 
             @Override
@@ -109,7 +107,7 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
             @Override
             public void onStopTrackingTouch(SeekBar seekBar) {
                 voiceResultViewModel.setSeekbarChanging(false);
-                voiceResultViewModel.setSeekbarTo(seekBar.getProgress());
+                player.seekTo(seekBar.getProgress());
             }
         });
         voiceResultViewModel.startMediaTimer();
@@ -154,12 +152,21 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
             }
 
             @Override
+            public void onEvents(Player player, Player.Events events) {
+                Player.Listener.super.onEvents(player, events);
+                if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
+                    boolean isLoading = player.isLoading();
+                    if (!isLoading) {
+                        voiceResultViewModel.setTotalDuration(player.getDuration());
+                        binding.voiceSeekBar.setMax((int) player.getDuration());
+                    }
+                }
+            }
+
+            @Override
             public void onPlaybackStateChanged(int state) {
                 AtmobLog.d("zk", "onPlaybackStateChanged: " + state);
-                if (Player.STATE_READY == state) {
-                    voiceResultViewModel.setTotalDuration(player.getDuration());
-                    binding.voiceSeekBar.setMax((int) player.getDuration());
-                } else if (Player.STATE_ENDED == state) {
+                if (Player.STATE_ENDED == state) {
                     setVoicePlayEnd();
                 }
             }

+ 0 - 4
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java

@@ -43,7 +43,6 @@ public class VoiceResultViewModel extends BaseViewModel {
 
 
     private final VoiceAIRepository voiceAIRepository;
-    private final MutableLiveData<Integer> seekBarProgress = new MutableLiveData<>();
     private final MutableLiveData<Boolean> isPlay = new MutableLiveData<>();
     private final MutableLiveData<Long> totalDuration = new MutableLiveData<>();
     private final MutableLiveData<Long> currentDuration = new MutableLiveData<>();
@@ -109,9 +108,6 @@ public class VoiceResultViewModel extends BaseViewModel {
         isSeekbarChanging = seekbarChanging;
     }
 
-    public void setSeekbarTo(int progress) {
-        this.seekBarProgress.setValue(progress);
-    }
 
     public void startMediaTimer() {
         if (timer != null) {

+ 7 - 2
app/src/main/java/com/atmob/voiceai/utils/AudioRecorder.java

@@ -1,19 +1,22 @@
 package com.atmob.voiceai.utils;
 
 import android.media.MediaRecorder;
+import android.util.Log;
 
 import java.io.IOException;
 
 public class AudioRecorder {
 
+    private static final String TAG = "AudioRecorder";
+
     private MediaRecorder mediaRecorder;
     private String filePath;
 
     public AudioRecorder() {
         mediaRecorder = new MediaRecorder();
         mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
-        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
-        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
     }
 
     public void setupMediaRecorder(String outputPath) {
@@ -23,6 +26,7 @@ public class AudioRecorder {
 
 
     public void startRecording() throws IOException {
+        Log.d(TAG, "startRecording: " + filePath);
         mediaRecorder.prepare();
         mediaRecorder.start();
     }
@@ -32,6 +36,7 @@ public class AudioRecorder {
     }
 
     public void stopRecording() {
+        Log.d(TAG, "stopRecording: " + filePath);
         mediaRecorder.stop();
         mediaRecorder.release();
     }

+ 1 - 18
app/src/main/java/com/atmob/voiceai/utils/DownloadUtils.java

@@ -1,12 +1,7 @@
 package com.atmob.voiceai.utils;
 
-import android.os.Environment;
-
-import com.atmob.common.runtime.ContextUtil;
-
 import java.io.File;
 import java.io.IOException;
-import java.util.Objects;
 
 import atmob.okhttp3.OkHttpClient;
 import atmob.okhttp3.Request;
@@ -20,24 +15,12 @@ public class DownloadUtils {
 
     public static final String FILE_PREFIX = "VoiceAI_";
 
-    public static File voiceFile = getVoiceSaveRootFile();
+    public static File voiceFile = FileUtils.getVoiceDownloadSaveRootFile();
 
     private DownloadUtils() {
 
     }
 
-    private static File getVoiceSaveRootFile() {
-        File cacheDir;
-        if (Objects.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)
-                && Environment.getExternalStorageDirectory().canWrite()) {
-            cacheDir = ContextUtil.getApplication().getExternalCacheDir();
-        } else {
-//            cacheDir = ContextUtil.getApplication().getExternalCacheDir();
-            cacheDir = ContextUtil.getApplication().getCacheDir();
-        }
-        return cacheDir;
-    }
-
 
     public static void downLoad(OkHttpClient client, String url, final File rootFile, final String fileName,
                                 final FileDownLoadObserver<File> fileDownLoadObserver) {

+ 12 - 0
app/src/main/java/com/atmob/voiceai/utils/FileUtils.java

@@ -5,6 +5,7 @@ import android.content.Context;
 import android.database.Cursor;
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
+import android.os.Environment;
 import android.provider.DocumentsContract;
 import android.text.format.Formatter;
 import android.webkit.MimeTypeMap;
@@ -15,12 +16,23 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Objects;
 
 import atmob.reactivex.rxjava3.annotations.NonNull;
 
 public class FileUtils {
 
 
+    public static File getVoiceDownloadSaveRootFile() {
+        File cacheDir;
+        if (Objects.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)
+                && Environment.getExternalStorageDirectory().canWrite()) {
+            cacheDir = ContextUtil.getApplication().getExternalCacheDir();
+        } else {
+            cacheDir = ContextUtil.getApplication().getCacheDir();
+        }
+        return cacheDir;
+    }
     public static File uriToFileApiQ(@NonNull Uri uri, Context context) {
         File file = null;
         if (uri == null) return file;

+ 66 - 0
app/src/main/java/com/atmob/voiceai/widget/AutoScrollTextView.java

@@ -0,0 +1,66 @@
+package com.atmob.voiceai.widget;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.text.method.ScrollingMovementMethod;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class AutoScrollTextView extends androidx.appcompat.widget.AppCompatTextView {
+
+
+    public AutoScrollTextView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public AutoScrollTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AutoScrollTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        setMovementMethod(ScrollingMovementMethod.getInstance());
+    }
+
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        getParent().requestDisallowInterceptTouchEvent(true);
+        return super.dispatchTouchEvent(ev);
+    }
+
+
+    public void startScroll() {
+        scrollTo(0, 0);
+        postScroll();
+    }
+
+    public void stopScroll() {
+        getHandler().removeCallbacksAndMessages(null);
+    }
+
+    private void postScroll() {
+        post(() -> {
+            int scrollY = getScrollY();
+            scrollY += 1;
+            scrollTo(0, scrollY);
+            if (scrollY >= getLineHeight() * getLineCount() - getHeight()) {
+//                scrollTo(0, 0);
+            }
+            postDelayed(this::postScroll, 50);
+        });
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        removeCallbacks(this::postScroll);
+    }
+}

BIN
app/src/main/res/drawable-xxhdpi/icon_clone_voice_record.webp


BIN
app/src/main/res/drawable-xxhdpi/icon_record_reset.webp


BIN
app/src/main/res/drawable-xxhdpi/icon_record_stop.webp


+ 15 - 0
app/src/main/res/drawable/bg_progress_bar_horizontal_record_0_20.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/background">
+        <shape>
+            <solid android:color="@color/transparent" />
+        </shape>
+    </item>
+    <item android:id="@android:id/progress">
+        <scale android:scaleWidth="100%">
+            <shape>
+                <solid android:color="#FF5656" />
+            </shape>
+        </scale>
+    </item>
+</layer-list>

+ 7 - 0
app/src/main/res/drawable/bg_record_bottom_shadow.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#32333C"
+        android:angle="90"
+        android:endColor="@color/transparent" />
+</shape>

+ 8 - 0
app/src/main/res/drawable/bg_recording_btn.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="24dp" />
+    <stroke
+        android:width="2dp"
+        android:color="#989898" />
+    <solid android:color="#32333C" />
+</shape>

+ 239 - 11
app/src/main/res/layout/fragment_clone_voice.xml

@@ -186,13 +186,13 @@
                         app:layout_constraintWidth_percent="0.1" />
 
                     <View
-                        android:onClick="@{()-> cloneVoiceViewModel.onRecordClick()}"
                         android:id="@+id/v_clone_record"
                         android:layout_width="match_parent"
                         android:layout_height="0dp"
                         android:layout_marginHorizontal="@dimen/clone_voice_padding"
                         android:layout_marginTop="8dp"
                         android:background="@drawable/bg_clone_card"
+                        android:onClick="@{()-> cloneVoiceViewModel.onRecordClick()}"
                         app:layout_constraintDimensionRatio="312:80"
                         app:layout_constraintTop_toBottomOf="@+id/v_clone_upload" />
 
@@ -418,11 +418,12 @@
                 </androidx.constraintlayout.widget.ConstraintLayout>
 
                 <androidx.constraintlayout.widget.ConstraintLayout
-                    tools:visibility="gone"
                     isGone="@{cloneVoiceViewModel.cloneState != CloneState.CLONE_SUCCESS}"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    app:layout_constraintTop_toBottomOf="@+id/space4">
+                    android:paddingBottom="@dimen/clone_voice_padding"
+                    app:layout_constraintTop_toBottomOf="@+id/space4"
+                    tools:visibility="gone">
 
                     <View
                         android:id="@+id/v_clone_success_preview"
@@ -475,12 +476,12 @@
                         app:layout_constraintTop_toBottomOf="@+id/tv_clone_success_title" />
 
                     <TextView
-                        android:onClick="@{()->cloneVoiceViewModel.useCloneVoiceClick()}"
                         android:id="@+id/tv_clone_used"
                         android:layout_width="0dp"
                         android:layout_height="0dp"
                         android:background="@drawable/bg_use_clone_voice"
                         android:gravity="center"
+                        android:onClick="@{()->cloneVoiceViewModel.useCloneVoiceClick()}"
                         android:text="@string/clone_voice_used_txt"
                         android:textColor="@color/white"
                         android:textSize="15sp"
@@ -495,17 +496,17 @@
                         app:layout_constraintWidth_percent="0.45" />
 
                     <TextView
-                        android:onClick="@{()-> cloneVoiceViewModel.onDeleteClick()}"
-                        android:text="@string/clone_voice_delete_txt"
                         android:id="@+id/tv_clone_delete"
                         android:layout_width="0dp"
-                        android:gravity="center"
-                        android:textStyle="bold"
-                        android:textSize="15sp"
-                        android:background="@drawable/bg_clone_voice_delete"
-                        android:textColor="@color/colorPrimary"
                         android:layout_height="0dp"
                         android:layout_marginStart="12dp"
+                        android:background="@drawable/bg_clone_voice_delete"
+                        android:gravity="center"
+                        android:onClick="@{()-> cloneVoiceViewModel.onDeleteClick()}"
+                        android:text="@string/clone_voice_delete_txt"
+                        android:textColor="@color/colorPrimary"
+                        android:textSize="15sp"
+                        android:textStyle="bold"
                         app:layout_constraintBottom_toBottomOf="@+id/tv_clone_used"
                         app:layout_constraintDimensionRatio="98:36"
                         app:layout_constraintLeft_toRightOf="@+id/tv_clone_used"
@@ -515,6 +516,233 @@
 
                 </androidx.constraintlayout.widget.ConstraintLayout>
 
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/clone_voice_record_layout"
+                    isGone="@{cloneVoiceViewModel.cloneState != CloneState.RECORDING_VOICE}"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingBottom="@dimen/clone_voice_padding"
+                    app:layout_constraintTop_toBottomOf="@+id/space4"
+                    tools:visibility="gone">
+
+                    <View
+                        android:id="@+id/v_record_audio_preview"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:background="@drawable/bg_clone_card"
+                        app:layout_constraintDimensionRatio="312:309"
+                        app:layout_constraintTop_toTopOf="parent" />
+
+
+                    <ImageView
+                        android:id="@+id/iv_record_audio"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginStart="12dp"
+                        android:layout_marginTop="10dp"
+                        android:src="@drawable/icon_clone_voice_record"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_record_audio_preview"
+                        app:layout_constraintWidth_percent="0.05" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="3dp"
+                        android:text="@string/record_title"
+                        android:textColor="@color/white80"
+                        android:textSize="12sp"
+                        app:layout_constraintBottom_toBottomOf="@+id/iv_record_audio"
+                        app:layout_constraintStart_toEndOf="@+id/iv_record_audio"
+                        app:layout_constraintTop_toTopOf="@+id/iv_record_audio" />
+
+                    <Space
+                        android:id="@+id/space6"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        app:layout_constraintDimensionRatio="360:8"
+                        app:layout_constraintTop_toBottomOf="@+id/iv_record_audio" />
+
+                    <View
+                        android:id="@+id/v_divider"
+                        android:layout_width="0dp"
+                        android:layout_height="1dp"
+                        android:layout_marginHorizontal="12dp"
+                        android:background="@color/white20"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
+                        app:layout_constraintTop_toBottomOf="@+id/space6" />
+
+                    <com.atmob.voiceai.widget.AutoScrollTextView
+                        android:id="@+id/tv_record_example"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="12dp"
+                        android:layout_marginTop="6dp"
+                        android:scrollbarSize="0dp"
+                        android:scrollbars="vertical"
+                        android:text="@string/record_example_txt"
+                        android:textColor="@color/white"
+                        android:textSize="16sp"
+                        app:layout_constraintDimensionRatio="288:122"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
+                        app:layout_constraintTop_toBottomOf="@+id/v_divider" />
+
+                    <View
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:background="@drawable/bg_record_bottom_shadow"
+                        app:layout_constraintBottom_toBottomOf="@+id/tv_record_example"
+                        app:layout_constraintDimensionRatio="288:40"
+                        app:layout_constraintEnd_toEndOf="@+id/tv_record_example"
+                        app:layout_constraintStart_toStartOf="@+id/tv_record_example" />
+
+                    <TextView
+                        android:id="@+id/tv_record_time"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:gravity="center"
+                        android:textColor="@color/white"
+                        android:textSize="25sp"
+                        android:text="@{cloneVoiceViewModel.recordTimeTxt}"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_record_audio_preview"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_record_audio_preview"
+                        app:layout_constraintVertical_bias="0.6928571428571429"
+                        tools:text="00:05.62" />
+
+                    <TextView
+                        android:text="@{cloneVoiceViewModel.recordStateDesc}"
+                        android:id="@+id/tv_record_state_desc"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:gravity="center"
+                        android:textSize="12sp"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
+                        app:layout_constraintTop_toBottomOf="@+id/tv_record_time"
+                        tools:text="Can't clone yet, Keep speaking loud and clear"
+                        tools:textColor="#FF5656" />
+
+                    <Space
+                        android:id="@+id/space7"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        app:layout_constraintDimensionRatio="360:25.5"
+                        app:layout_constraintTop_toBottomOf="@+id/tv_record_state_desc" />
+
+                    <View
+                        app:layout_constraintHorizontal_bias="0.5714285714285714"
+                        android:id="@+id/v_record_audio_20sec"
+                        android:layout_width="3dp"
+                        android:layout_height="14dp"
+                        android:background="#7C7D88"
+                        app:layout_constraintBottom_toBottomOf="@+id/progress_record_bar"
+                        app:layout_constraintEnd_toEndOf="@+id/progress_record_bar"
+                        app:layout_constraintStart_toStartOf="@+id/progress_record_bar"
+                        app:layout_constraintTop_toTopOf="@+id/progress_record_bar" />
+
+                    <TextView
+                        android:text="@string/record_20_sec"
+                        android:layout_marginTop="4dp"
+                        android:textColor="@color/white50"
+                        android:textSize="12sp"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_20sec"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_20sec"
+                        app:layout_constraintTop_toBottomOf="@+id/v_record_audio_20sec"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content" />
+
+                    <View
+                        app:layout_constraintHorizontal_bias="0.8571428571428571"
+                        android:id="@+id/v_record_audio_30sec"
+                        android:layout_width="3dp"
+                        android:layout_height="14dp"
+                        android:background="#7C7D88"
+                        app:layout_constraintBottom_toBottomOf="@+id/progress_record_bar"
+                        app:layout_constraintEnd_toEndOf="@+id/progress_record_bar"
+                        app:layout_constraintStart_toStartOf="@+id/progress_record_bar"
+                        app:layout_constraintTop_toTopOf="@+id/progress_record_bar" />
+
+
+                    <TextView
+                        android:text="@string/record_30_sec"
+                        android:layout_marginTop="4dp"
+                        android:textColor="@color/white50"
+                        android:textSize="12sp"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_30sec"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_30sec"
+                        app:layout_constraintTop_toBottomOf="@+id/v_record_audio_30sec"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content" />
+
+                    <ProgressBar
+                        android:id="@+id/progress_record_bar"
+                        style="?android:attr/progressBarStyleHorizontal"
+                        android:layout_width="0dp"
+                        android:layout_height="4dp"
+                        android:progress="@{cloneVoiceViewModel.recordProgress}"
+                        android:layout_marginHorizontal="16dp"
+                        android:background="#7C7D88"
+                        android:max="@{(int)cloneVoiceViewModel.recordMaxTime}"
+                        android:progressDrawable="@{cloneVoiceViewModel.recordProgressDrawable}"
+                        tools:progressDrawable="@drawable/bg_progress_bar_horizontal_record_0_20"
+                        app:layout_constraintEnd_toEndOf="@+id/v_record_audio_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_record_audio_preview"
+                        app:layout_constraintTop_toBottomOf="@+id/space7"
+                        tools:progress="60" />
+
+                    <Space
+                        android:id="@+id/space5"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        app:layout_constraintDimensionRatio="360:25"
+                        app:layout_constraintTop_toBottomOf="@+id/v_record_audio_preview" />
+
+                    <View
+                        android:id="@+id/v_record_audio"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:background="@drawable/bg_recording_btn"
+                        android:onClick="@{()-> cloneVoiceViewModel.onRecordingClick(cloneVoiceViewModel.showStopRecord,cloneVoiceViewModel.recordProgress)}"
+                        app:layout_constraintDimensionRatio="312:48"
+                        app:layout_constraintTop_toBottomOf="@+id/space5" />
+
+                    <ImageView
+                        android:id="@+id/iv_stop"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:src="@{cloneVoiceViewModel.showStopRecord ? @drawable/icon_record_stop: @drawable/icon_record_reset}"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_record_audio"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintHorizontal_chainStyle="packed"
+                        app:layout_constraintLeft_toLeftOf="@+id/v_record_audio"
+                        app:layout_constraintRight_toLeftOf="@+id/tv_stop"
+                        app:layout_constraintTop_toTopOf="@+id/v_record_audio"
+                        app:layout_constraintWidth_percent="0.0666666666666667" />
+
+                    <TextView
+                        android:id="@+id/tv_stop"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="6dp"
+                        android:text="@{cloneVoiceViewModel.showStopRecord ? @string/record_stop: @string/record_reset}"
+                        android:textColor="@color/white"
+                        android:textSize="17sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toBottomOf="@+id/iv_stop"
+                        app:layout_constraintLeft_toRightOf="@+id/iv_stop"
+                        app:layout_constraintRight_toRightOf="@+id/v_record_audio"
+                        app:layout_constraintTop_toTopOf="@+id/iv_stop" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
             </androidx.constraintlayout.widget.ConstraintLayout>
 
         </androidx.core.widget.NestedScrollView>

File diff suppressed because it is too large
+ 10 - 1
app/src/main/res/values/strings.xml