Browse Source

增加预览界面以及推荐列表显示

zk 1 year ago
parent
commit
d39bd32497
31 changed files with 959 additions and 43 deletions
  1. 1 0
      app/build.gradle
  2. 10 2
      app/src/main/java/com/atmob/voiceai/data/api/bean/VoiceListBean.java
  3. 7 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceListResponse.java
  4. 1 1
      app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java
  5. 56 22
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  6. 2 2
      app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java
  7. 7 6
      app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingViewModel.java
  8. 1 0
      app/src/main/java/com/atmob/voiceai/module/main/MainActivity.java
  9. 103 0
      app/src/main/java/com/atmob/voiceai/module/result/VoiceRecommendAdapter.java
  10. 172 1
      app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java
  11. 122 1
      app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java
  12. 17 0
      app/src/main/java/com/atmob/voiceai/utils/DateUtil.java
  13. 43 0
      app/src/main/java/com/atmob/voiceai/utils/SmoothScrollGridLayoutManager.java
  14. BIN
      app/src/main/res/drawable-xxhdpi/bg_voice_result_header_background.webp
  15. BIN
      app/src/main/res/drawable-xxhdpi/icon_result_back.webp
  16. BIN
      app/src/main/res/drawable-xxhdpi/icon_voice_playing.webp
  17. BIN
      app/src/main/res/drawable-xxhdpi/icon_voice_reduce.webp
  18. BIN
      app/src/main/res/drawable-xxhdpi/icon_voice_speed_up.webp
  19. BIN
      app/src/main/res/drawable-xxhdpi/icon_voice_suspend.webp
  20. 9 0
      app/src/main/res/drawable/bg_ripple_common_oval_mask.xml
  21. 7 0
      app/src/main/res/drawable/bg_voice_recommend_container.xml
  22. 5 0
      app/src/main/res/drawable/bg_voice_recommend_label.xml
  23. 7 0
      app/src/main/res/drawable/bg_voice_result_save.xml
  24. 20 0
      app/src/main/res/drawable/shape_preview_audio_seekbar.xml
  25. 11 0
      app/src/main/res/drawable/shape_preview_audio_seekbar_thumb.xml
  26. 5 3
      app/src/main/res/layout/activity_main.xml
  27. 295 4
      app/src/main/res/layout/activity_voice_result.xml
  28. 1 0
      app/src/main/res/layout/fragment_voice_ai.xml
  29. 0 1
      app/src/main/res/layout/item_voice_ai_list.xml
  30. 54 0
      app/src/main/res/layout/item_voice_ai_result_recommend.xml
  31. 3 0
      app/src/main/res/values/strings.xml

+ 1 - 0
app/build.gradle

@@ -166,4 +166,5 @@ dependencies {
 
     //lottie
     implementation "com.airbnb.android:lottie:$rootProject.lottie_version"
+
 }

+ 10 - 2
app/src/main/java/com/atmob/voiceai/data/api/bean/VoiceListBean.java

@@ -23,16 +23,17 @@ public class VoiceListBean extends BaseObservable {
     @SerializedName("hasPro")
     private boolean hasPro;
 
-    @SerializedName("showAdd")
     private boolean isAddIcon;
 
-    @SerializedName("type")
+    @SerializedName("voiceType")
     private int voiceType;
 
     private int viewType;
 
     private boolean check;
 
+    private String content;
+
     @VoicePlayState
     private int voicePlayState;
 
@@ -44,6 +45,13 @@ public class VoiceListBean extends BaseObservable {
         int PLAYING = 2;
     }
 
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
 
     public int getVoiceType() {
         return voiceType;

+ 7 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceListResponse.java

@@ -11,6 +11,13 @@ public class VoiceListResponse {
     @SerializedName("voiceList")
     private List<VoiceListBean> voiceList;
 
+    @SerializedName("showAdd")
+    private boolean showAdd;
+
+    public boolean isShowAdd() {
+        return showAdd;
+    }
+
     public List<VoiceListBean> getVoiceList() {
         return voiceList;
     }

+ 1 - 1
app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java

@@ -11,7 +11,7 @@ import javax.inject.Singleton;
 public class MemberRepository {
 
 
-    private final MutableLiveData<Boolean> isMember = new MutableLiveData<>();
+    private final MutableLiveData<Boolean> isMember = new MutableLiveData<>(true);
 
 
     @Inject

+ 56 - 22
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -1,11 +1,15 @@
 package com.atmob.voiceai.data.repositories;
 
 
+import android.util.Pair;
+
 import androidx.annotation.IntDef;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
 import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.common.runtime.ContextUtil;
+import com.atmob.voiceai.R;
 import com.atmob.voiceai.data.api.AtmobApi;
 import com.atmob.voiceai.data.api.GenerateApi;
 import com.atmob.voiceai.data.api.bean.UserVoiceBean;
@@ -17,6 +21,7 @@ import com.atmob.voiceai.data.api.response.TextToSpeechResponse;
 import com.atmob.voiceai.data.api.response.VoiceInfoResponse;
 import com.atmob.voiceai.data.api.response.VoiceListResponse;
 import com.atmob.voiceai.data.api.response.VoiceTypeResponse;
+import com.atmob.voiceai.utils.ToastUtil;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,10 +47,12 @@ public class VoiceAIRepository {
     private VoiceListBean requestCloneBean;
     private String textToSpeechTxt;
 
-    private final MutableLiveData<Integer> textToSpeechState = new MutableLiveData<>();
+    private final MutableLiveData<Pair<Integer, String>> textToSpeechState = new MutableLiveData<>();
 
     private final MutableLiveData<UserVoiceBean> resultBean = new MutableLiveData<>();
 
+    private final MutableLiveData<VoiceListBean> recommendClickBean = new MutableLiveData<>();
+
 
     @IntDef({TextToSpeechState.GENERATING, TextToSpeechState.GENERATED, TextToSpeechState.ERROR})
     public @interface TextToSpeechState {
@@ -60,6 +67,18 @@ public class VoiceAIRepository {
         this.generateApi = generateApi;
     }
 
+    public void setRecommendClickBean(VoiceListBean bean) {
+        recommendClickBean.setValue(bean);
+    }
+
+    public LiveData<VoiceListBean> getRecommendClickBean() {
+        return recommendClickBean;
+    }
+
+    public LiveData<UserVoiceBean> getResultBean() {
+        return resultBean;
+    }
+
     public VoiceListBean getRequestCloneBean() {
         return requestCloneBean;
     }
@@ -68,7 +87,7 @@ public class VoiceAIRepository {
         return textToSpeechTxt;
     }
 
-    public LiveData<Integer> getTextToSpeechState() {
+    public LiveData<Pair<Integer, String>> getTextToSpeechState() {
         return textToSpeechState;
     }
 
@@ -96,26 +115,41 @@ public class VoiceAIRepository {
         }
         requestCloneBean = bean;
         textToSpeechTxt = content;
-        textToSpeech(bean.getId(), bean.getVoiceType(), content).subscribe(new SingleObserver<TextToSpeechResponse>() {
-            @Override
-            public void onSubscribe(@NonNull Disposable d) {
-                textToSpeechRequest = true;
-                textToSpeechState.setValue(TextToSpeechState.GENERATING);
-            }
-
-            @Override
-            public void onSuccess(@NonNull TextToSpeechResponse textToSpeechResponse) {
-                textToSpeechRequest = false;
-
-                textToSpeechState.setValue(TextToSpeechState.GENERATED);
-            }
-
-            @Override
-            public void onError(@NonNull Throwable e) {
-                textToSpeechRequest = false;
-                textToSpeechState.setValue(TextToSpeechState.ERROR);
-            }
-        });
+        /********仅测试使用******/
+        textToSpeechState.setValue(new Pair<>(TextToSpeechState.GENERATED, null));
+        UserVoiceBean userVoiceBean = new UserVoiceBean();
+        userVoiceBean.setVoiceAvatar(bean.getAvatarUrl());
+        userVoiceBean.setVoiceId(bean.getId());
+        userVoiceBean.setVoiceName(bean.getName());
+        userVoiceBean.setContent(content);
+        userVoiceBean.setVoiceUrl(bean.getVoiceUrl());
+        resultBean.setValue(userVoiceBean);
+        /********仅测试使用******/
+//        textToSpeech(bean.getId(), bean.getVoiceType(), content).subscribe(new SingleObserver<TextToSpeechResponse>() {
+//            @Override
+//            public void onSubscribe(@NonNull Disposable d) {
+//                textToSpeechRequest = true;
+//                textToSpeechState.setValue(new Pair<>(TextToSpeechState.GENERATING, null));
+//            }
+//
+//            @Override
+//            public void onSuccess(@NonNull TextToSpeechResponse textToSpeechResponse) {
+//                textToSpeechRequest = false;
+//                resultBean.setValue(textToSpeechResponse.getUserVoice());
+//                textToSpeechState.setValue(new Pair<>(TextToSpeechState.GENERATED, null));
+//            }
+//
+//            @Override
+//            public void onError(@NonNull Throwable throwable) {
+//                textToSpeechRequest = false;
+//                if (throwable instanceof RxHttpHandler.ServerErrorException) {
+//                    RxHttpHandler.ServerErrorException serverErrorException = (RxHttpHandler.ServerErrorException) throwable;
+//                    textToSpeechState.setValue(new Pair<>(TextToSpeechState.ERROR, serverErrorException.getMsg()));
+//                } else {
+//                    textToSpeechState.setValue(new Pair<>(TextToSpeechState.ERROR, ContextUtil.getContext().getString(R.string.generate_error)));
+//                }
+//            }
+//        });
     }
 
     private Single<TextToSpeechResponse> textToSpeech(int id, int type, String content) {

+ 2 - 2
app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java

@@ -43,9 +43,9 @@ public class VoiceGeneratingActivity extends BaseActivity<ActivityVoiceGeneratin
             VoiceResultActivity.start(this);
             finish();
         });
-        voiceGeneratingViewModel.getGenerateError().observe(this, o -> {
+        voiceGeneratingViewModel.getGenerateError().observe(this, txt -> {
             finish();
-            ToastUtil.show(R.string.generate_error, ToastUtil.LENGTH_SHORT);
+            ToastUtil.show(txt, ToastUtil.LENGTH_SHORT);
         });
     }
 

+ 7 - 6
app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingViewModel.java

@@ -1,6 +1,7 @@
 package com.atmob.voiceai.module.generating;
 
 import android.text.TextUtils;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.LiveData;
@@ -27,11 +28,11 @@ public class VoiceGeneratingViewModel extends BaseViewModel {
     private final MutableLiveData<String> generatingTime = new MutableLiveData<>();
 
     private final SingleLiveEvent<?> generateSuccess = new SingleLiveEvent<>();
-    private final SingleLiveEvent<?> generateError = new SingleLiveEvent<>();
+    private final SingleLiveEvent<String> generateError = new SingleLiveEvent<>();
 
     private final VoiceAIRepository voiceAIRepository;
 
-    private final Observer<Integer> stateObserver;
+    private final Observer<Pair<Integer, String>> stateObserver;
 
     @Inject
     public VoiceGeneratingViewModel(VoiceAIRepository voiceAIRepository) {
@@ -41,9 +42,9 @@ public class VoiceGeneratingViewModel extends BaseViewModel {
             if (state == null) {
                 return;
             }
-            if (state == VoiceAIRepository.TextToSpeechState.ERROR) {
-                generateError.call();
-            } else if (state == VoiceAIRepository.TextToSpeechState.GENERATED) {
+            if (state.first == VoiceAIRepository.TextToSpeechState.ERROR) {
+                generateError.setValue(state.second);
+            } else if (state.first == VoiceAIRepository.TextToSpeechState.GENERATED) {
                 generateSuccess.call();
             }
         };
@@ -54,7 +55,7 @@ public class VoiceGeneratingViewModel extends BaseViewModel {
         return generateSuccess;
     }
 
-    public LiveData<?> getGenerateError() {
+    public LiveData<String> getGenerateError() {
         return generateError;
     }
 

+ 1 - 0
app/src/main/java/com/atmob/voiceai/module/main/MainActivity.java

@@ -47,6 +47,7 @@ public class MainActivity extends BaseActivity<ActivityMainBinding> {
         context.startActivity(intent);
     }
 
+
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);

+ 103 - 0
app/src/main/java/com/atmob/voiceai/module/result/VoiceRecommendAdapter.java

@@ -0,0 +1,103 @@
+package com.atmob.voiceai.module.result;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.atmob.voiceai.data.api.bean.VoiceListBean;
+import com.atmob.voiceai.databinding.ItemVoiceAiListBinding;
+import com.atmob.voiceai.databinding.ItemVoiceAiResultRecommendBinding;
+import com.atmob.voiceai.databinding.ItemVoiceListFootBinding;
+
+import java.util.List;
+import java.util.Objects;
+
+public class VoiceRecommendAdapter extends RecyclerView.Adapter<VoiceRecommendAdapter.ViewHolder> {
+
+    private final AsyncListDiffer<VoiceListBean> listDiffer;
+
+    private final LifecycleOwner lifecycleOwner;
+
+
+    private ActionHandler actionHandler;
+
+    public void setActionHandler(ActionHandler actionHandler) {
+        this.actionHandler = actionHandler;
+    }
+
+    public VoiceRecommendAdapter(@NonNull LifecycleOwner lifecycleOwner) {
+        this.lifecycleOwner = lifecycleOwner;
+        this.listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<VoiceListBean>() {
+            @Override
+            public boolean areItemsTheSame(@NonNull VoiceListBean oldItem, @NonNull VoiceListBean newItem) {
+                return oldItem.getId() == newItem.getId();
+            }
+
+            @Override
+            public boolean areContentsTheSame(@NonNull VoiceListBean oldItem, @NonNull VoiceListBean newItem) {
+                if (!Objects.equals(oldItem.getName(), newItem.getName())) {
+                    return false;
+                }
+                if (!Objects.equals(oldItem.getAvatarUrl(), newItem.getAvatarUrl())) {
+                    return false;
+                }
+                return true;
+            }
+        });
+    }
+
+    @NonNull
+    @Override
+    public VoiceRecommendAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        Context context = parent.getContext();
+        LayoutInflater layoutInflater = LayoutInflater.from(context);
+        return new ViewHolder(ItemVoiceAiResultRecommendBinding.inflate(layoutInflater, parent, false));
+    }
+
+
+    @Override
+    public void onBindViewHolder(@NonNull VoiceRecommendAdapter.ViewHolder holder, int position) {
+        holder.bind(listDiffer.getCurrentList().get(position));
+    }
+
+    public void submit(@NonNull List<VoiceListBean> itemBeanList) {
+        listDiffer.submitList(itemBeanList);
+    }
+
+    @Override
+    public int getItemCount() {
+        return listDiffer.getCurrentList().size();
+    }
+
+
+    public class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final ItemVoiceAiResultRecommendBinding binding;
+
+        public ViewHolder(@NonNull ItemVoiceAiResultRecommendBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+            binding.setLifecycleOwner(lifecycleOwner);
+            binding.setChoiceClick(v -> {
+                if (actionHandler != null) actionHandler.voiceItemClick(binding.getBean());
+            });
+        }
+
+
+        public void bind(VoiceListBean itemBean) {
+            binding.setBean(itemBean);
+        }
+    }
+
+    public interface ActionHandler {
+
+        void voiceItemClick(VoiceListBean voiceListBean);
+    }
+
+}

+ 172 - 1
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultActivity.java

@@ -1,12 +1,31 @@
 package com.atmob.voiceai.module.result;
 
+
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.SeekBar;
+
+import androidx.annotation.Nullable;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.PlaybackException;
+import androidx.media3.common.Player;
+import androidx.media3.exoplayer.ExoPlayer;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.atmob.app.lib.base.BaseActivity;
+import com.atmob.common.logging.AtmobLog;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
+import com.atmob.voiceai.data.api.bean.VoiceListBean;
 import com.atmob.voiceai.databinding.ActivityVoiceResultBinding;
-import com.atmob.voiceai.module.generating.VoiceGeneratingActivity;
+import com.atmob.voiceai.module.main.MainActivity;
+import com.atmob.voiceai.module.voiceai.VoiceAIFragment;
+import com.atmob.voiceai.module.voiceai.VoiceAIListAdapter;
+import com.atmob.voiceai.utils.ToastUtil;
 
 import dagger.hilt.android.AndroidEntryPoint;
 
@@ -15,6 +34,9 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
 
 
     private VoiceResultViewModel voiceResultViewModel;
+    private ExoPlayer player;
+
+    private VoiceRecommendAdapter voiceRecommendAdapter;
 
     public static void start(Context context) {
         Intent intent = new Intent(context, VoiceResultActivity.class);
@@ -26,6 +48,154 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
 
 
     @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        initView();
+        initObserver();
+    }
+
+    private void initView() {
+        intVoiceRecommendList();
+        initMedia3ExoPlayer();
+        initSeekBar();
+        initPlay();
+    }
+
+    private void intVoiceRecommendList() {
+        voiceRecommendAdapter = new VoiceRecommendAdapter(this);
+        voiceRecommendAdapter.setActionHandler(voiceListBean -> {
+            if (player.isLoading()) {
+                return;
+            }
+            UserVoiceBean value = voiceResultViewModel.getResultBean().getValue();
+            if (value == null) {
+                return;
+            }
+            if (player.isPlaying()) {
+                player.pause();
+                voiceResultViewModel.setVoicePlay(false);
+            }
+            MainActivity.start(this, VoiceAIFragment.class);
+            voiceListBean.setContent(value.getContent());
+            voiceResultViewModel.setRecommendVoice(voiceListBean);
+            finish();
+        });
+        binding.rvRecommend.setAdapter(voiceRecommendAdapter);
+        binding.rvRecommend.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
+    }
+
+    private void initSeekBar() {
+        binding.voiceSeekBar.setProgress(0);
+        binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (fromUser) {
+                    player.seekTo(progress);
+                }
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+                voiceResultViewModel.setSeekbarChanging(true);
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                voiceResultViewModel.setSeekbarChanging(false);
+                voiceResultViewModel.setSeekbarTo(seekBar.getProgress());
+            }
+        });
+        voiceResultViewModel.startMediaTimer();
+    }
+
+    private void initPlay() {
+        binding.ivPlay.setOnClickListener(view -> {
+            if (player.isLoading()) {
+                return;
+            }
+            if (player.isPlaying()) {
+                player.pause();
+                voiceResultViewModel.setVoicePlay(false);
+            } else {
+                player.play();
+                voiceResultViewModel.setVoicePlay(true);
+            }
+        });
+        binding.ivVoiceReduce.setOnClickListener(view -> {
+            if (player.isLoading()) {
+                return;
+            }
+            player.seekTo(player.getCurrentPosition() - 5000);
+        });
+        binding.ivVoiceSpeed.setOnClickListener(view -> {
+            if (player.isLoading()) {
+                return;
+            }
+            player.seekTo(player.getCurrentPosition() + 5000);
+        });
+    }
+
+    private void initMedia3ExoPlayer() {
+        player = new ExoPlayer.Builder(this).build();
+        player.addListener(new Player.Listener() {
+
+            @Override
+            public void onPlayerError(PlaybackException error) {
+                AtmobLog.d("zk", "onPlayerError: " + error.getMessage());
+                ToastUtil.show(R.string.voice_play_error, ToastUtil.LENGTH_SHORT);
+                setVoicePlayEnd();
+            }
+
+            @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) {
+                    setVoicePlayEnd();
+                }
+            }
+
+        });
+    }
+
+    private void setVoicePlayEnd() {
+        player.seekTo(0);
+        voiceResultViewModel.setCurrentDuration(0);
+        player.pause();
+        voiceResultViewModel.setVoicePlay(false);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (player != null) {
+            player.stop();
+            player.release();
+        }
+    }
+
+    private void initObserver() {
+        voiceResultViewModel.getVoiceList().observe(this, list -> voiceRecommendAdapter.submit(list));
+        voiceResultViewModel.getRefreshAudioCurrentProgress().observe(this, o -> {
+            long currentPosition = player.getCurrentPosition();
+            voiceResultViewModel.setCurrentDuration(currentPosition);
+            binding.voiceSeekBar.setProgress((int) currentPosition);
+        });
+        voiceResultViewModel.getResultBean().observe(this, bean -> {
+            if (bean == null) {
+                return;
+            }
+            Uri videoUri = Uri.parse(bean.getVoiceUrl());
+            MediaItem mediaItem = MediaItem.fromUri(videoUri);
+            player.setMediaItem(mediaItem);
+            player.prepare();
+            player.setPlayWhenReady(false);
+        });
+    }
+
+    @Override
     protected boolean shouldImmersion() {
         return true;
     }
@@ -34,5 +204,6 @@ public class VoiceResultActivity extends BaseActivity<ActivityVoiceResultBinding
     protected void initViewModel() {
         super.initViewModel();
         voiceResultViewModel = getViewModelProvider().get(VoiceResultViewModel.class);
+        binding.setVoiceResultViewModel(voiceResultViewModel);
     }
 }

+ 122 - 1
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java

@@ -1,16 +1,137 @@
 package com.atmob.voiceai.module.result;
 
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
 import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.app.lib.livedata.SingleLiveEvent;
+import com.atmob.common.runtime.ActivityUtil;
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
+import com.atmob.voiceai.data.api.bean.VoiceListBean;
+import com.atmob.voiceai.data.api.response.VoiceListResponse;
+import com.atmob.voiceai.data.repositories.VoiceAIRepository;
+
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
 
 import javax.inject.Inject;
 
+import atmob.reactivex.rxjava3.annotations.NonNull;
+import atmob.reactivex.rxjava3.core.SingleObserver;
+import atmob.reactivex.rxjava3.disposables.Disposable;
 import dagger.hilt.android.lifecycle.HiltViewModel;
 
 @HiltViewModel
 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<>();
+    private final SingleLiveEvent<?> refreshAudioCurrentProgress = new SingleLiveEvent<>();
+    private final MutableLiveData<List<VoiceListBean>> voiceList = new MutableLiveData<>();
+    private boolean isSeekbarChanging;
+    private Timer timer;
+
     @Inject
-    public VoiceResultViewModel() {
+    public VoiceResultViewModel(VoiceAIRepository voiceAIRepository) {
+        this.voiceAIRepository = voiceAIRepository;
+        refreshVoiceRecommendList();
+    }
+
+    public LiveData<List<VoiceListBean>> getVoiceList() {
+        return voiceList;
+    }
+
+    public LiveData<Boolean> getIsPlay() {
+        return isPlay;
+    }
+
+    public LiveData<?> getRefreshAudioCurrentProgress() {
+        return refreshAudioCurrentProgress;
+    }
+
+    public LiveData<Long> getCurrentDuration() {
+        return currentDuration;
+    }
+
+    public LiveData<Long> getTotalDuration() {
+        return totalDuration;
+    }
+
+    public LiveData<UserVoiceBean> getResultBean() {
+        return voiceAIRepository.getResultBean();
+    }
+
+
+    public void setVoicePlay(boolean isPlay) {
+        this.isPlay.setValue(isPlay);
+    }
+
+    public void setCurrentDuration(long duration) {
+        if (duration >= 0) {
+            currentDuration.setValue(duration);
+        }
+    }
+
+    public void setTotalDuration(long duration) {
+        if (duration >= 0) {
+            totalDuration.setValue(duration);
+        }
+    }
+
+    public void setSeekbarChanging(boolean seekbarChanging) {
+        isSeekbarChanging = seekbarChanging;
     }
+
+    public void setSeekbarTo(int progress) {
+        this.seekBarProgress.setValue(progress);
+    }
+
+    public void startMediaTimer() {
+        if (timer != null) {
+            return;
+        }
+        timer = new Timer();
+        timer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                if (isSeekbarChanging) {
+                    return;
+                }
+                refreshAudioCurrentProgress.postValue(null);
+            }
+        }, 0, 25);
+    }
+
+    public void onBackClick() {
+        ActivityUtil.getTopActivity().finish();
+    }
+
+    private void refreshVoiceRecommendList() {
+        voiceAIRepository.requestVoiceList("").subscribe(new SingleObserver<VoiceListResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                addDisposable(d);
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceListResponse voiceListResponse) {
+                voiceList.setValue(voiceListResponse.getVoiceList());
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+
+            }
+        });
+    }
+
+    public void setRecommendVoice(VoiceListBean voiceListBean) {
+        voiceAIRepository.setRecommendClickBean(voiceListBean);
+    }
+
 }

+ 17 - 0
app/src/main/java/com/atmob/voiceai/utils/DateUtil.java

@@ -0,0 +1,17 @@
+package com.atmob.voiceai.utils;
+
+import java.util.Locale;
+
+public class DateUtil {
+
+    private DateUtil() {
+
+    }
+
+    public static String formatDuration(long duration) {
+        long seconds = duration / 1000;
+        long minutes = seconds / 60;
+        seconds = seconds % 60;
+        return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
+    }
+}

+ 43 - 0
app/src/main/java/com/atmob/voiceai/utils/SmoothScrollGridLayoutManager.java

@@ -0,0 +1,43 @@
+package com.atmob.voiceai.utils;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class SmoothScrollGridLayoutManager extends GridLayoutManager {
+
+
+    public SmoothScrollGridLayoutManager(Context context, int spanCount) {
+        super(context, spanCount);
+    }
+
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView,
+                                       RecyclerView.State state, final int position) {
+
+        LinearSmoothScroller smoothScroller =
+                new LinearSmoothScroller(recyclerView.getContext()) {
+                    @Override
+                    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+                        return 200f / displayMetrics.densityDpi;
+                    }
+
+                    @Override
+                    protected int getHorizontalSnapPreference() {
+                        return LinearSmoothScroller.SNAP_TO_START;
+                    }
+
+                    @Override
+                    protected int getVerticalSnapPreference() {
+                        return LinearSmoothScroller.SNAP_TO_START;
+                    }
+                };
+
+        smoothScroller.setTargetPosition(position);
+        startSmoothScroll(smoothScroller);
+    }
+
+}

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


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


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


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


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


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


+ 9 - 0
app/src/main/res/drawable/bg_ripple_common_oval_mask.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple android:color="#33000000"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/mask">
+        <shape android:shape="oval">
+            <solid android:color="#ff000000" />
+        </shape>
+    </item>
+</ripple>

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

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#32333C" />
+    <corners
+        android:topLeftRadius="20dp"
+        android:topRightRadius="20dp" />
+</shape>

+ 5 - 0
app/src/main/res/drawable/bg_voice_recommend_label.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="3dp" />
+    <solid android:color="#14FFFFFF" />
+</shape>

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

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

+ 20 - 0
app/src/main/res/drawable/shape_preview_audio_seekbar.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- Background -->
+    <item android:id="@android:id/background">
+        <shape>
+            <solid android:color="#32333C" />
+            <size android:height="1dp" />
+        </shape>
+    </item>
+
+    <!-- Progress -->
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape>
+                <solid android:color="@color/colorPrimaryVariant" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>

+ 11 - 0
app/src/main/res/drawable/shape_preview_audio_seekbar_thumb.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <stroke
+        android:width="2dp"
+        android:color="@color/white" />
+    <solid android:color="#32333C" />
+    <size
+        android:width="12dp"
+        android:height="12dp" />
+</shape>

+ 5 - 3
app/src/main/res/layout/activity_main.xml

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
+    android:orientation="vertical"
     android:layout_height="match_parent"
     android:background="@color/colorPrimary">
 
@@ -9,6 +10,7 @@
         android:id="@+id/main_view_pager"
         android:layout_width="match_parent"
         android:layout_height="0dp"
+        android:layout_weight="1"
         app:layout_constraintBottom_toTopOf="@id/v_tab_divider"
         app:layout_constraintTop_toTopOf="parent" />
 
@@ -22,10 +24,10 @@
     <com.google.android.material.tabs.TabLayout
         android:id="@+id/main_tab_layout"
         android:layout_width="match_parent"
-        android:layout_height="0dp"
+        android:layout_height="56dp"
         android:background="@color/colorPrimary"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintDimensionRatio="360:56"
         app:tabIconTint="@android:color/transparent"
         app:tabIndicator="@null" />
-</androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>

+ 295 - 4
app/src/main/res/layout/activity_voice_result.xml

@@ -1,6 +1,297 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+    <data>
+
+        <variable
+            name="voiceResultViewModel"
+            type="com.atmob.voiceai.module.result.VoiceResultViewModel" />
+
+        <import type="com.atmob.common.ui.SizeUtil" />
+
+        <import type="com.atmob.voiceai.utils.DateUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/bg_voice_result_header_background"
+            app:layout_constraintDimensionRatio="1080:1254"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <Space
+            android:id="@+id/space_status_bar"
+            android:layout_width="match_parent"
+            android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/space_status_bar">
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <Space
+                    android:id="@+id/sapce2"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:60"
+                    app:layout_constraintTop_toTopOf="parent" />
+
+
+                <ImageView
+                    android:id="@+id/iv_avatar"
+                    imageUrl="@{voiceResultViewModel.resultBean.voiceAvatar}"
+                    radius="@{20}"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="1:1"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@+id/sapce2"
+                    app:layout_constraintWidth_percent="0.6" />
+
+                <TextView
+                    android:id="@+id/tv_name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="16dp"
+                    android:text="@{voiceResultViewModel.resultBean.voiceName}"
+                    android:textColor="@color/white80"
+                    android:textSize="30sp"
+                    android:textStyle="bold"
+                    app:layout_constraintEnd_toEndOf="@+id/iv_avatar"
+                    app:layout_constraintStart_toStartOf="@+id/iv_avatar"
+                    app:layout_constraintTop_toBottomOf="@+id/iv_avatar"
+                    tools:text="Taylor Swift" />
+
+                <Space
+                    android:id="@+id/sapce3"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:31"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_name" />
+
+                <SeekBar
+                    android:id="@+id/voice_seek_bar"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="20dp"
+                    android:max="@{(int)voiceResultViewModel.totalDuration}"
+                    android:maxHeight="2dp"
+                    android:minHeight="2dp"
+                    android:paddingStart="6dp"
+                    android:paddingEnd="6dp"
+                    android:progress="@{(int)voiceResultViewModel.currentDuration}"
+                    android:progressDrawable="@drawable/shape_preview_audio_seekbar"
+                    android:splitTrack="false"
+                    android:thumb="@drawable/shape_preview_audio_seekbar_thumb"
+                    app:layout_constraintTop_toBottomOf="@id/sapce3"
+                    tools:progress="30" />
+
+                <TextView
+                    android:id="@+id/tv_current_time"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="6dp"
+                    android:layout_marginTop="3dp"
+                    android:text="@{DateUtil.formatDuration(voiceResultViewModel.currentDuration)}"
+                    android:textColor="@color/white50"
+                    android:textSize="12sp"
+                    app:layout_constraintLeft_toLeftOf="@+id/voice_seek_bar"
+                    app:layout_constraintTop_toBottomOf="@+id/voice_seek_bar"
+                    tools:text="00:53" />
+
+                <TextView
+                    android:id="@+id/tv_total_time"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginEnd="6dp"
+                    android:text="@{DateUtil.formatDuration(voiceResultViewModel.totalDuration)}"
+                    android:textColor="@color/white50"
+                    android:textSize="12sp"
+                    app:layout_constraintEnd_toEndOf="@id/voice_seek_bar"
+                    app:layout_constraintTop_toBottomOf="@+id/voice_seek_bar"
+                    app:layout_constraintTop_toTopOf="@+id/tv_current_time"
+                    tools:text="02:53" />
+
+                <Space
+                    android:id="@+id/sapce4"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:16"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_current_time" />
+
+
+                <Space
+                    android:id="@+id/space_play"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="216:48"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@+id/sapce4"
+                    app:layout_constraintWidth_percent="0.6" />
+
+                <ImageView
+                    android:id="@+id/iv_voice_reduce"
+                    android:src="@drawable/icon_voice_reduce"
+                    app:layout_constraintDimensionRatio="1:1"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    app:layout_constraintBottom_toBottomOf="@+id/iv_play"
+                    app:layout_constraintStart_toStartOf="@+id/space_play"
+                    app:layout_constraintTop_toTopOf="@+id/iv_play" />
+
+                <ImageView
+                    android:id="@+id/iv_play"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:src="@{voiceResultViewModel.isPlay ? @drawable/icon_voice_playing: @drawable/icon_voice_suspend}"
+                    app:layout_constraintBottom_toBottomOf="@+id/space_play"
+                    app:layout_constraintDimensionRatio="1:1"
+                    app:layout_constraintEnd_toEndOf="@+id/space_play"
+                    app:layout_constraintStart_toStartOf="@+id/space_play"
+                    app:layout_constraintTop_toTopOf="@+id/space_play"
+                    app:layout_constraintWidth_percent="0.1333333333333333"
+                    tools:src="@drawable/icon_voice_playing" />
+
+                <ImageView
+                    android:id="@+id/iv_voice_speed"
+                    android:src="@drawable/icon_voice_speed_up"
+                    app:layout_constraintDimensionRatio="1:1"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    app:layout_constraintBottom_toBottomOf="@+id/iv_play"
+                    app:layout_constraintEnd_toEndOf="@+id/space_play"
+                    app:layout_constraintTop_toTopOf="@+id/iv_play" />
+
+                <Space
+                    android:id="@+id/sapce5"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintDimensionRatio="360:36"
+                    app:layout_constraintTop_toBottomOf="@+id/space_play" />
+
+                <View
+                    android:id="@+id/view_voice_recommend_container"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:background="@drawable/bg_voice_recommend_container"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintDimensionRatio="360:289"
+                    app:layout_constraintTop_toBottomOf="@+id/sapce5" />
+
+                <View
+                    android:id="@+id/view_voice_recommend_label"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:layout_marginTop="12dp"
+                    android:background="@drawable/bg_voice_recommend_label"
+                    app:layout_constraintDimensionRatio="42:6"
+                    app:layout_constraintEnd_toEndOf="@+id/view_voice_recommend_container"
+                    app:layout_constraintStart_toStartOf="@+id/view_voice_recommend_container"
+                    app:layout_constraintTop_toTopOf="@+id/view_voice_recommend_container"
+                    app:layout_constraintWidth_percent="0.1166666666666667" />
+
+                <TextView
+                    android:id="@+id/tv_recommend"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="16dp"
+                    android:layout_marginTop="22dp"
+                    android:text="@string/voice_result_recommend"
+                    android:textColor="@color/white"
+                    android:textSize="16sp"
+                    app:layout_constraintStart_toStartOf="@+id/view_voice_recommend_container"
+                    app:layout_constraintTop_toBottomOf="@+id/view_voice_recommend_label" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/rv_recommend"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="20dp"
+                    android:fadingEdge="horizontal"
+                    android:fadingEdgeLength="10dp"
+                    android:paddingStart="16dp"
+                    android:requiresFadingEdge="horizontal"
+                    app:layout_constraintEnd_toEndOf="@+id/view_voice_recommend_container"
+                    app:layout_constraintStart_toStartOf="@+id/view_voice_recommend_container"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_recommend"
+                    tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+                    tools:listitem="@layout/item_voice_ai_result_recommend"
+                    tools:orientation="horizontal" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </ScrollView>
+
+        <Space
+            android:id="@+id/sapce1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:10"
+            app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+
+        <ImageView
+            android:id="@+id/iv_back"
+            expandTouchSize="@{5}"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+            android:onClick="@{()-> voiceResultViewModel.onBackClick()}"
+            android:src="@drawable/icon_result_back"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/sapce1"
+            app:layout_constraintWidth_percent="0.0666666666666667" />
+
+
+        <TextView
+            android:id="@+id/tv_save"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="16dp"
+            android:layout_marginBottom="12dp"
+            android:background="@drawable/bg_voice_result_save"
+            android:gravity="center"
+            android:text="@string/voice_result_save"
+            android:textColor="@color/white"
+            android:textSize="17sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="158:48"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintRight_toLeftOf="@+id/tv_share"
+            app:layout_constraintWidth_percent="0.4388888888888889" />
+
+        <TextView
+            android:id="@+id/tv_share"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="12dp"
+            android:background="@drawable/bg_voice_ai_btn"
+            android:gravity="center"
+            android:text="@string/voice_result_share"
+            android:textColor="@color/colorPrimary"
+            android:textSize="17sp"
+            android:textStyle="bold"
+            app:layout_constraintDimensionRatio="158:48"
+            app:layout_constraintLeft_toRightOf="@+id/tv_save"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toTopOf="@id/tv_save"
+            app:layout_constraintWidth_percent="0.4388888888888889" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 1 - 0
app/src/main/res/layout/fragment_voice_ai.xml

@@ -97,6 +97,7 @@
                 app:layout_constraintTop_toTopOf="@+id/v_tool_bar" />
 
             <ImageView
+                android:onClick="@{()-> voiceAIViewModel.onSettingClick()}"
                 android:layout_width="0dp"
                 android:layout_height="0dp"
                 android:layout_marginEnd="@dimen/app_common_page_horizontal_padding"

+ 0 - 1
app/src/main/res/layout/item_voice_ai_list.xml

@@ -50,7 +50,6 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:onClick="@{choiceClick}"
-            android:src="@drawable/icon_voice_ai_add"
             app:layout_constraintDimensionRatio="1:1"
             app:layout_constraintTop_toTopOf="parent" />
 

+ 54 - 0
app/src/main/res/layout/item_voice_ai_result_recommend.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+        <variable
+            name="bean"
+            type="com.atmob.voiceai.data.api.bean.VoiceListBean" />
+
+
+        <variable
+            name="choiceClick"
+            type="android.view.View.OnClickListener" />
+
+        <import type="com.atmob.common.ui.SizeUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp">
+
+        <ImageView
+            android:id="@+id/iv_avatar"
+            imageUrl="@{bean.avatarUrl}"
+            isGone="@{bean.isAddIcon}"
+            radius="@{12}"
+            android:layout_width="@{(float)SizeUtil.getScreenWidth() * 0.2f, default=wrap_content}"
+            android:layout_height="0dp"
+            android:onClick="@{choiceClick}"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:src="@drawable/icon_voice_vip_use" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="4dp"
+            android:gravity="center"
+            android:lines="1"
+            android:text="@{bean.name}"
+            android:textColor="@color/white"
+            android:textSize="12sp"
+            app:layout_constraintEnd_toEndOf="@+id/iv_avatar"
+            app:layout_constraintStart_toStartOf="@+id/iv_avatar"
+            app:layout_constraintTop_toBottomOf="@+id/iv_avatar"
+            tools:text="Add Voice" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -22,4 +22,7 @@
     <string name="generating_time">Generating ~ %d sec</string>
     <string name="generating_time_countdown_over">It\'ll be ready soon</string>
     <string name="generate_error">Generation timed out, please try again</string>
+    <string name="voice_result_recommend">Try other voices with this text</string>
+    <string name="voice_result_save">Save</string>
+    <string name="voice_result_share">Share</string>
 </resources>