Kaynağa Gözat

首页增加应用类型显示

zk 2 yıl önce
ebeveyn
işleme
bd01b8341b
35 değiştirilmiş dosya ile 1595 ekleme ve 172 silme
  1. 47 0
      app/src/main/java/com/atmob/voiceai/data/api/AtmobApi.java
  2. 86 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/CloneHistoryBean.java
  3. 66 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/CloneVoiceBean.java
  4. 84 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/CloneVoiceListBean.java
  5. 26 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/TypeListBean.java
  6. 86 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/UserVoiceBean.java
  7. 86 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/VoiceListBean.java
  8. 17 0
      app/src/main/java/com/atmob/voiceai/data/api/request/CloneDeleteRequest.java
  9. 25 0
      app/src/main/java/com/atmob/voiceai/data/api/request/TextTosSpeechRequest.java
  10. 29 0
      app/src/main/java/com/atmob/voiceai/data/api/request/VoiceCloneRequest.java
  11. 25 0
      app/src/main/java/com/atmob/voiceai/data/api/request/VoiceHistoryRequest.java
  12. 17 0
      app/src/main/java/com/atmob/voiceai/data/api/request/VoiceListRequest.java
  13. 20 0
      app/src/main/java/com/atmob/voiceai/data/api/response/TextToSpeechResponse.java
  14. 22 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceCloneListResponse.java
  15. 20 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceCloneResponse.java
  16. 32 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceHistoryResponse.java
  17. 52 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceInfoResponse.java
  18. 22 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceListResponse.java
  19. 22 0
      app/src/main/java/com/atmob/voiceai/data/api/response/VoiceTypeResponse.java
  20. 1 4
      app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java
  21. 54 0
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  22. 32 0
      app/src/main/java/com/atmob/voiceai/helper/ErrorHelper.java
  23. 1 0
      app/src/main/java/com/atmob/voiceai/module/splash/SplashActivity.java
  24. 108 1
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIFragment.java
  25. 96 0
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIListAdapter.java
  26. 158 4
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java
  27. 74 0
      app/src/main/java/com/atmob/voiceai/utils/GridLayoutItemDecoration.java
  28. BIN
      app/src/main/res/drawable-xxhdpi/icon_voice_ai_add.webp
  29. 5 0
      app/src/main/res/drawable/bg_voice_ai_tab_normal.xml
  30. 5 0
      app/src/main/res/drawable/bg_voice_ai_tab_selected.xml
  31. 200 162
      app/src/main/res/layout/fragment_voice_ai.xml
  32. 57 0
      app/src/main/res/layout/item_voice_ai_list.xml
  33. 17 0
      app/src/main/res/layout/item_voice_ai_tab.xml
  34. 2 0
      app/src/main/res/values/strings.xml
  35. 1 1
      app/src/main/res/values/themes.xml

+ 47 - 0
app/src/main/java/com/atmob/voiceai/data/api/AtmobApi.java

@@ -1,6 +1,53 @@
 package com.atmob.voiceai.data.api;
 
 
+import com.atmob.app.lib.base.BaseResponse;
+import com.atmob.voiceai.data.api.request.BaseRequest;
+import com.atmob.voiceai.data.api.request.CloneDeleteRequest;
+import com.atmob.voiceai.data.api.request.TextTosSpeechRequest;
+import com.atmob.voiceai.data.api.request.VoiceCloneRequest;
+import com.atmob.voiceai.data.api.request.VoiceHistoryRequest;
+import com.atmob.voiceai.data.api.request.VoiceListRequest;
+import com.atmob.voiceai.data.api.response.TextToSpeechResponse;
+import com.atmob.voiceai.data.api.response.VoiceCloneListResponse;
+import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
+import com.atmob.voiceai.data.api.response.VoiceHistoryResponse;
+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 atmob.reactivex.rxjava3.core.Single;
+import atmob.retrofit2.http.Body;
+import atmob.retrofit2.http.POST;
+
 public interface AtmobApi {
 
+
+    @POST("/project/voice/v1/voice/info")
+    Single<BaseResponse<VoiceInfoResponse>> getVoiceInfo(@Body BaseRequest request);
+
+    @POST("/project/voice/v1/voice/type")
+    Single<BaseResponse<VoiceTypeResponse>> getVoiceType(@Body BaseRequest request);
+
+    @POST("/project/voice/v1/voice/list")
+    Single<BaseResponse<VoiceListResponse>> getVoiceList(@Body VoiceListRequest request);
+
+    @POST("/project/voice/v1/voice/textToSpeech")
+    Single<BaseResponse<TextToSpeechResponse>> textToSpeech(@Body TextTosSpeechRequest request);
+
+    @POST("/project/voice/v1/clone/list")
+    Single<BaseResponse<VoiceCloneListResponse>> getCloneList(@Body BaseRequest request);
+
+    @POST("/project/voice/v1/voice/clone")
+    Single<BaseResponse<VoiceCloneResponse>> voiceClone(@Body VoiceCloneRequest request);
+
+    @POST("/project/voice/v1/clone/delete")
+    Single<BaseResponse<Object>> cloneDelete(@Body CloneDeleteRequest request);
+
+    @POST("/project/voice/v1/voice/history")
+    Single<BaseResponse<VoiceHistoryResponse>> voiceHistory(@Body VoiceHistoryRequest request);
+
+
+
+
 }

+ 86 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/CloneHistoryBean.java

@@ -0,0 +1,86 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class CloneHistoryBean {
+    @SerializedName("id")
+    private int id;
+    @SerializedName("ssid")
+    private String ssid;
+    @SerializedName("content")
+    private String content;
+    @SerializedName("voiceId")
+    private int voiceId;
+    @SerializedName("voiceName")
+    private String voiceName;
+    @SerializedName("voiceAvatar")
+    private String voiceAvatar;
+    @SerializedName("voiceUrl")
+    private String voiceUrl;
+    @SerializedName("createTime")
+    private long createTime;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getSsid() {
+        return ssid;
+    }
+
+    public void setSsid(String ssid) {
+        this.ssid = ssid;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public int getVoiceId() {
+        return voiceId;
+    }
+
+    public void setVoiceId(int voiceId) {
+        this.voiceId = voiceId;
+    }
+
+    public String getVoiceName() {
+        return voiceName;
+    }
+
+    public void setVoiceName(String voiceName) {
+        this.voiceName = voiceName;
+    }
+
+    public String getVoiceAvatar() {
+        return voiceAvatar;
+    }
+
+    public void setVoiceAvatar(String voiceAvatar) {
+        this.voiceAvatar = voiceAvatar;
+    }
+
+    public String getVoiceUrl() {
+        return voiceUrl;
+    }
+
+    public void setVoiceUrl(String voiceUrl) {
+        this.voiceUrl = voiceUrl;
+    }
+
+    public long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(long createTime) {
+        this.createTime = createTime;
+    }
+}

+ 66 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/CloneVoiceBean.java

@@ -0,0 +1,66 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class CloneVoiceBean {
+    @SerializedName("id")
+    private int id;
+    @SerializedName("ssid")
+    private String ssid;
+    @SerializedName("type")
+    private int type;
+    @SerializedName("voiceName")
+    private String voiceName;
+    @SerializedName("createTime")
+    private long createTime;
+    @SerializedName("voiceUrl")
+    private String voiceUrl;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getSsid() {
+        return ssid;
+    }
+
+    public void setSsid(String ssid) {
+        this.ssid = ssid;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public void setType(int type) {
+        this.type = type;
+    }
+
+    public String getVoiceName() {
+        return voiceName;
+    }
+
+    public void setVoiceName(String voiceName) {
+        this.voiceName = voiceName;
+    }
+
+    public long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(long createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getVoiceUrl() {
+        return voiceUrl;
+    }
+
+    public void setVoiceUrl(String voiceUrl) {
+        this.voiceUrl = voiceUrl;
+    }
+}

+ 84 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/CloneVoiceListBean.java

@@ -0,0 +1,84 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class CloneVoiceListBean {
+    @SerializedName("id")
+    private int id;
+    @SerializedName("ssid")
+    private String ssid;
+    @SerializedName("type")
+    private int type;
+    @SerializedName("voiceName")
+    private String voiceName;
+    @SerializedName("voiceUrl")
+    private String voiceUrl;
+    @SerializedName("createTime")
+    private long createTime;
+    private String cloneVoiceAvatar;
+    private String cloneVoiceName;
+
+    public String getCloneVoiceAvatar() {
+        return cloneVoiceAvatar;
+    }
+
+    public void setCloneVoiceAvatar(String cloneVoiceAvatar) {
+        this.cloneVoiceAvatar = cloneVoiceAvatar;
+    }
+
+    public String getCloneVoiceName() {
+        return cloneVoiceName;
+    }
+
+    public void setCloneVoiceName(String cloneVoiceName) {
+        this.cloneVoiceName = cloneVoiceName;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getSsid() {
+        return ssid;
+    }
+
+    public void setSsid(String ssid) {
+        this.ssid = ssid;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public void setType(int type) {
+        this.type = type;
+    }
+
+    public String getVoiceName() {
+        return voiceName;
+    }
+
+    public void setVoiceName(String voiceName) {
+        this.voiceName = voiceName;
+    }
+
+    public String getVoiceUrl() {
+        return voiceUrl;
+    }
+
+    public void setVoiceUrl(String voiceUrl) {
+        this.voiceUrl = voiceUrl;
+    }
+
+    public long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(long createTime) {
+        this.createTime = createTime;
+    }
+}

+ 26 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/TypeListBean.java

@@ -0,0 +1,26 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class TypeListBean {
+    @SerializedName("id")
+    private int id;
+    @SerializedName("name")
+    private String name;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}

+ 86 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/UserVoiceBean.java

@@ -0,0 +1,86 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class UserVoiceBean {
+    @SerializedName("id")
+    private int id;
+    @SerializedName("ssid")
+    private String ssid;
+    @SerializedName("content")
+    private String content;
+    @SerializedName("voiceId")
+    private int voiceId;
+    @SerializedName("voiceName")
+    private String voiceName;
+    @SerializedName("voiceAvatar")
+    private String voiceAvatar;
+    @SerializedName("voiceUrl")
+    private String voiceUrl;
+    @SerializedName("createTime")
+    private long createTime;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getSsid() {
+        return ssid;
+    }
+
+    public void setSsid(String ssid) {
+        this.ssid = ssid;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public int getVoiceId() {
+        return voiceId;
+    }
+
+    public void setVoiceId(int voiceId) {
+        this.voiceId = voiceId;
+    }
+
+    public String getVoiceName() {
+        return voiceName;
+    }
+
+    public void setVoiceName(String voiceName) {
+        this.voiceName = voiceName;
+    }
+
+    public String getVoiceAvatar() {
+        return voiceAvatar;
+    }
+
+    public void setVoiceAvatar(String voiceAvatar) {
+        this.voiceAvatar = voiceAvatar;
+    }
+
+    public String getVoiceUrl() {
+        return voiceUrl;
+    }
+
+    public void setVoiceUrl(String voiceUrl) {
+        this.voiceUrl = voiceUrl;
+    }
+
+    public long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(long createTime) {
+        this.createTime = createTime;
+    }
+}

+ 86 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/VoiceListBean.java

@@ -0,0 +1,86 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class VoiceListBean {
+    @SerializedName("id")
+    private int id;
+    @SerializedName("typeId")
+    private int typeId;
+    @SerializedName("name")
+    private String name;
+    @SerializedName("avatarUrl")
+    private String avatarUrl;
+    @SerializedName("voiceUrl")
+    private String voiceUrl;
+    @SerializedName("hasNew")
+    private boolean hasNew;
+    @SerializedName("hasPro")
+    private boolean hasPro;
+
+    private boolean isAddIcon;
+
+    public void setAddIcon(boolean addIcon) {
+        isAddIcon = addIcon;
+    }
+
+    public boolean isAddIcon() {
+        return isAddIcon;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public int getTypeId() {
+        return typeId;
+    }
+
+    public void setTypeId(int typeId) {
+        this.typeId = typeId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getAvatarUrl() {
+        return avatarUrl;
+    }
+
+    public void setAvatarUrl(String avatarUrl) {
+        this.avatarUrl = avatarUrl;
+    }
+
+    public String getVoiceUrl() {
+        return voiceUrl;
+    }
+
+    public void setVoiceUrl(String voiceUrl) {
+        this.voiceUrl = voiceUrl;
+    }
+
+    public boolean isHasNew() {
+        return hasNew;
+    }
+
+    public void setHasNew(boolean hasNew) {
+        this.hasNew = hasNew;
+    }
+
+    public boolean isHasPro() {
+        return hasPro;
+    }
+
+    public void setHasPro(boolean hasPro) {
+        this.hasPro = hasPro;
+    }
+}

+ 17 - 0
app/src/main/java/com/atmob/voiceai/data/api/request/CloneDeleteRequest.java

@@ -0,0 +1,17 @@
+package com.atmob.voiceai.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+public class CloneDeleteRequest extends BaseRequest {
+
+    @SerializedName("id")
+    private int id;
+
+    public CloneDeleteRequest(int id) {
+        this.id = id;
+    }
+
+    public int getId() {
+        return id;
+    }
+}

+ 25 - 0
app/src/main/java/com/atmob/voiceai/data/api/request/TextTosSpeechRequest.java

@@ -0,0 +1,25 @@
+package com.atmob.voiceai.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+public class TextTosSpeechRequest extends BaseRequest {
+
+    @SerializedName("id")
+    private int id;
+
+    @SerializedName("content")
+    private String content;
+
+    public TextTosSpeechRequest(int id, String content) {
+        this.id = id;
+        this.content = content;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getContent() {
+        return content;
+    }
+}

+ 29 - 0
app/src/main/java/com/atmob/voiceai/data/api/request/VoiceCloneRequest.java

@@ -0,0 +1,29 @@
+package com.atmob.voiceai.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.io.File;
+
+public class VoiceCloneRequest extends BaseRequest {
+
+
+    @SerializedName("type")
+    private int type;
+
+    @SerializedName("file")
+    private File file;
+
+
+    public VoiceCloneRequest(int type, File file) {
+        this.type = type;
+        this.file = file;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public File getFile() {
+        return file;
+    }
+}

+ 25 - 0
app/src/main/java/com/atmob/voiceai/data/api/request/VoiceHistoryRequest.java

@@ -0,0 +1,25 @@
+package com.atmob.voiceai.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+public class VoiceHistoryRequest extends BaseRequest {
+
+    @SerializedName("page")
+    private int page;
+
+    @SerializedName("pageSize")
+    private int pageSize;
+
+    public VoiceHistoryRequest(int page, int pageSize) {
+        this.page = page;
+        this.pageSize = pageSize;
+    }
+
+    public int getPage() {
+        return page;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+}

+ 17 - 0
app/src/main/java/com/atmob/voiceai/data/api/request/VoiceListRequest.java

@@ -0,0 +1,17 @@
+package com.atmob.voiceai.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+public class VoiceListRequest extends BaseRequest {
+
+    @SerializedName("id")
+    private int id;
+
+    public VoiceListRequest(int id) {
+        this.id = id;
+    }
+
+    public int getId() {
+        return id;
+    }
+}

+ 20 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/TextToSpeechResponse.java

@@ -0,0 +1,20 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.UserVoiceBean;
+import com.google.gson.annotations.SerializedName;
+
+public class TextToSpeechResponse {
+
+
+    @SerializedName("userVoice")
+    private UserVoiceBean userVoice;
+
+    public UserVoiceBean getUserVoice() {
+        return userVoice;
+    }
+
+    public void setUserVoice(UserVoiceBean userVoice) {
+        this.userVoice = userVoice;
+    }
+
+}

+ 22 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceCloneListResponse.java

@@ -0,0 +1,22 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class VoiceCloneListResponse {
+
+
+    @SerializedName("cloneVoiceList")
+    private List<CloneVoiceListBean> cloneVoiceList;
+
+    public List<CloneVoiceListBean> getCloneVoiceList() {
+        return cloneVoiceList;
+    }
+
+    public void setCloneVoiceList(List<CloneVoiceListBean> cloneVoiceList) {
+        this.cloneVoiceList = cloneVoiceList;
+    }
+
+}

+ 20 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceCloneResponse.java

@@ -0,0 +1,20 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.CloneVoiceBean;
+import com.google.gson.annotations.SerializedName;
+
+public class VoiceCloneResponse {
+
+
+    @SerializedName("cloneVoice")
+    private CloneVoiceBean cloneVoice;
+
+    public CloneVoiceBean getCloneVoice() {
+        return cloneVoice;
+    }
+
+    public void setCloneVoice(CloneVoiceBean cloneVoice) {
+        this.cloneVoice = cloneVoice;
+    }
+
+}

+ 32 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceHistoryResponse.java

@@ -0,0 +1,32 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.CloneHistoryBean;
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class VoiceHistoryResponse {
+
+
+    @SerializedName("count")
+    private int count;
+    @SerializedName("list")
+    private List<CloneHistoryBean> list;
+
+    public int getCount() {
+        return count;
+    }
+
+    public void setCount(int count) {
+        this.count = count;
+    }
+
+    public List<CloneHistoryBean> getList() {
+        return list;
+    }
+
+    public void setList(List<CloneHistoryBean> list) {
+        this.list = list;
+    }
+
+}

+ 52 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceInfoResponse.java

@@ -0,0 +1,52 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class VoiceInfoResponse {
+
+
+    @SerializedName("voiceTimes")
+    private int voiceTimes;
+    @SerializedName("cloneVoiceAvatar")
+    private String cloneVoiceAvatar;
+    @SerializedName("cloneVoiceName")
+    private String cloneVoiceName;
+    @SerializedName("cloneVoiceList")
+    private List<CloneVoiceListBean> cloneVoiceList;
+
+    public int getVoiceTimes() {
+        return voiceTimes;
+    }
+
+    public void setVoiceTimes(int voiceTimes) {
+        this.voiceTimes = voiceTimes;
+    }
+
+    public String getCloneVoiceAvatar() {
+        return cloneVoiceAvatar;
+    }
+
+    public void setCloneVoiceAvatar(String cloneVoiceAvatar) {
+        this.cloneVoiceAvatar = cloneVoiceAvatar;
+    }
+
+    public String getCloneVoiceName() {
+        return cloneVoiceName;
+    }
+
+    public void setCloneVoiceName(String cloneVoiceName) {
+        this.cloneVoiceName = cloneVoiceName;
+    }
+
+    public List<CloneVoiceListBean> getCloneVoiceList() {
+        return cloneVoiceList;
+    }
+
+    public void setCloneVoiceList(List<CloneVoiceListBean> cloneVoiceList) {
+        this.cloneVoiceList = cloneVoiceList;
+    }
+
+}

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

@@ -0,0 +1,22 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.VoiceListBean;
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class VoiceListResponse {
+
+
+    @SerializedName("voiceList")
+    private List<VoiceListBean> voiceList;
+
+    public List<VoiceListBean> getVoiceList() {
+        return voiceList;
+    }
+
+    public void setVoiceList(List<VoiceListBean> voiceList) {
+        this.voiceList = voiceList;
+    }
+
+}

+ 22 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/VoiceTypeResponse.java

@@ -0,0 +1,22 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.TypeListBean;
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class VoiceTypeResponse {
+
+
+    @SerializedName("typeList")
+    private List<TypeListBean> typeList;
+
+    public List<TypeListBean> getTypeList() {
+        return typeList;
+    }
+
+    public void setTypeList(List<TypeListBean> typeList) {
+        this.typeList = typeList;
+    }
+
+}

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

@@ -12,7 +12,7 @@ public class MemberRepository {
 
 
     private final MutableLiveData<Boolean> isMember = new MutableLiveData<>();
-    private final MutableLiveData<Integer> adFreeGenerateNumber = new MutableLiveData<>(3);
+
 
     @Inject
     public MemberRepository() {
@@ -23,7 +23,4 @@ public class MemberRepository {
         return isMember;
     }
 
-    public LiveData<Integer> getAdFreeGenerateNumber() {
-        return adFreeGenerateNumber;
-    }
 }

+ 54 - 0
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -0,0 +1,54 @@
+package com.atmob.voiceai.data.repositories;
+
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.voiceai.data.api.AtmobApi;
+import com.atmob.voiceai.data.api.request.BaseRequest;
+import com.atmob.voiceai.data.api.request.VoiceListRequest;
+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 java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.rxjava.utils.RxJavaUtil;
+
+@Singleton
+public class VoiceAIRepository {
+
+
+    private final AtmobApi atmobApi;
+
+
+    @Inject
+    public VoiceAIRepository(AtmobApi atmobApi) {
+        this.atmobApi = atmobApi;
+    }
+
+
+    public Single<VoiceInfoResponse> requestVoiceInfo() {
+        return atmobApi.getVoiceInfo(new BaseRequest())
+                .compose(RxHttpHandler.handle(true))
+                .compose(RxJavaUtil.SingleSchedule.io2Main());
+    }
+
+    public Single<VoiceTypeResponse> requestVoiceType() {
+        return atmobApi.getVoiceType(new BaseRequest())
+                .compose(RxHttpHandler.handle(false))
+                .compose(RxJavaUtil.SingleSchedule.io2Main());
+    }
+
+    public Single<VoiceListResponse> requestVoiceList(int id) {
+        return atmobApi.getVoiceList(new VoiceListRequest(id))
+                .compose(RxHttpHandler.handle(true))
+                .compose(RxJavaUtil.SingleSchedule.io2Main());
+    }
+}

+ 32 - 0
app/src/main/java/com/atmob/voiceai/helper/ErrorHelper.java

@@ -0,0 +1,32 @@
+package com.atmob.voiceai.helper;
+
+import androidx.annotation.StringRes;
+
+import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.utils.ToastUtil;
+
+public class ErrorHelper {
+
+    public static void errorThrowableToast(Throwable throwable, @ToastUtil.Duration int duration) {
+        if (throwable instanceof RxHttpHandler.ServerErrorException) {
+            RxHttpHandler.ServerErrorException serverErrorException = (RxHttpHandler.ServerErrorException) throwable;
+            errorToast(serverErrorException.getCode(), duration);
+        } else {
+            ToastUtil.show(R.string.net_error_message, duration);
+        }
+    }
+
+    public static void errorToast(int errorCode, @ToastUtil.Duration int duration) {
+        ToastUtil.show(getErrorResId(errorCode), duration);
+    }
+
+    @StringRes
+    public static int getErrorResId(int errorCode) {
+        int errorResId = R.string.net_error_message;
+        switch (errorCode) {
+
+        }
+        return errorResId;
+    }
+}

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

@@ -21,6 +21,7 @@ public class SplashActivity extends BaseActivity<ActivitySplashBinding> {
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         MainActivity.start(this);
+        finish();
     }
 
 

+ 108 - 1
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIFragment.java

@@ -1,15 +1,26 @@
 package com.atmob.voiceai.module.voiceai;
 
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.atmob.app.lib.base.BaseFragment;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.data.api.bean.TypeListBean;
 import com.atmob.voiceai.databinding.FragmentVoiceAiBinding;
+import com.atmob.voiceai.databinding.ItemVoiceAiTabBinding;
+import com.atmob.voiceai.utils.GridLayoutItemDecoration;
+import com.google.android.material.tabs.TabLayout;
 import com.gyf.immersionbar.ImmersionBar;
 
+import java.lang.reflect.Method;
+
 import dagger.hilt.android.AndroidEntryPoint;
 
 
@@ -18,6 +29,7 @@ public class VoiceAIFragment extends BaseFragment<FragmentVoiceAiBinding> {
 
 
     private VoiceAIViewModel voiceViewModel;
+    private VoiceAIListAdapter voiceAIListAdapter;
 
     @Override
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
@@ -26,16 +38,111 @@ public class VoiceAIFragment extends BaseFragment<FragmentVoiceAiBinding> {
         initObserver();
     }
 
+    private void initObserver() {
+        voiceViewModel.getVoiceDetailList().observe(getViewLifecycleOwner(), list -> voiceAIListAdapter.submit(list));
+        voiceViewModel.getVoiceTypeList().observe(getViewLifecycleOwner(), list -> {
+            if (list == null || list.isEmpty()) {
+                return;
+            }
+            binding.voiceTab.removeAllTabs();
+            for (int i = 0; i < list.size(); i++) {
+                TypeListBean typeListBean = list.get(i);
+                TabLayout.Tab tab = binding.voiceTab.newTab();
+                ItemVoiceAiTabBinding itemBinding = ItemVoiceAiTabBinding.inflate(getLayoutInflater());
+                itemBinding.tabTitle.setText(typeListBean.getName());
+                tab.setCustomView(itemBinding.getRoot());
+                tab.setTag(itemBinding);
+                binding.voiceTab.addTab(tab);
+                itemBinding.getRoot().setOnClickListener(v -> {
+                    tab.select();
+                    voiceViewModel.refreshVoiceList(typeListBean.getId());
+                });
+                if (i == 0) {
+                    itemBinding.getRoot().performClick();
+                }
+            }
+        });
+    }
 
     private void initView() {
+        initTabLayout();
+        initVoiceList();
+    }
 
+    private void initVoiceList() {
+        voiceAIListAdapter = new VoiceAIListAdapter(getViewLifecycleOwner());
+        binding.ryVoiceModel.setAdapter(voiceAIListAdapter);
+        GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 4);
+        binding.ryVoiceModel.setLayoutManager(gridLayoutManager);
+        GridLayoutItemDecoration gridLayoutItemDecoration = new GridLayoutItemDecoration(4, 0.0222222222222222f, 0.0222222222222222f, true);
+        binding.vGenerate.post(() -> gridLayoutItemDecoration.setLastBottomGap(binding.vGenerate.getMeasuredHeight()));
+        binding.ryVoiceModel.addItemDecoration(gridLayoutItemDecoration);
+
+//        binding.ryVoiceModel.addItemDecoration(new RecyclerView.ItemDecoration() {
+//            @Override
+//            public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+//                super.getItemOffsets(outRect, view, parent, state);
+//                int childAdapterPosition = parent.getChildAdapterPosition(view);
+//                if (childAdapterPosition == -1) {
+//                    outRect.bottom = 0;
+//                    return;
+//                }
+//                if (childAdapterPosition == voiceAIListAdapter.getItemCount() - 1) {
+//                    outRect.bottom = 300;
+//                } else {
+//                    outRect.bottom = 0;
+//                }
+//            }
+//        });
     }
 
-    private void initObserver() {
+    private void initTabLayout() {
+        binding.voiceTab.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+            @SuppressLint("UseCompatLoadingForDrawables")
+            @Override
+            public void onTabSelected(TabLayout.Tab tab) {
+                Object tag = tab.getTag();
+                ItemVoiceAiTabBinding itemBinding = tag instanceof ItemVoiceAiTabBinding ? ((ItemVoiceAiTabBinding) tag) : null;
+                if (itemBinding == null) {
+                    return;
+                }
+                itemBinding.tabTitle.setTextColor(getResources().getColor(R.color.colorPrimary));
+                itemBinding.tabTitle.setBackground(getResources().getDrawable(R.drawable.bg_voice_ai_tab_selected));
+            }
+
+            @SuppressLint("UseCompatLoadingForDrawables")
+            @Override
+            public void onTabUnselected(TabLayout.Tab tab) {
+                Object tag = tab.getTag();
+                ItemVoiceAiTabBinding itemBinding = tag instanceof ItemVoiceAiTabBinding ? ((ItemVoiceAiTabBinding) tag) : null;
+                if (itemBinding == null) {
+                    return;
+                }
+                itemBinding.tabTitle.setTextColor(getResources().getColor(com.atmob.app.base.R.color.white50));
+                itemBinding.tabTitle.setBackground(getResources().getDrawable(R.drawable.bg_voice_ai_tab_normal));
+            }
+
+            @Override
+            public void onTabReselected(TabLayout.Tab tab) {
+
+            }
+        });
+    }
 
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        voiceViewModel.setLastVoiceGenerateTxt(binding.etVoicePrint.getText() == null ? "" : binding.etVoicePrint.getText().toString());
     }
 
     @Override
+    public void onStop() {
+        super.onStop();
+        voiceViewModel.setLastVoiceGenerateTxt(binding.etVoicePrint.getText() == null ? "" : binding.etVoicePrint.getText().toString());
+    }
+
+
+    @Override
     protected void initViewModel() {
         super.initViewModel();
         voiceViewModel = getViewModelProvider().get(VoiceAIViewModel.class);

+ 96 - 0
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIListAdapter.java

@@ -0,0 +1,96 @@
+package com.atmob.voiceai.module.voiceai;
+
+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.common.logging.AtmobLog;
+import com.atmob.voiceai.data.api.bean.VoiceListBean;
+import com.atmob.voiceai.databinding.ItemVoiceAiListBinding;
+
+import java.util.List;
+import java.util.Objects;
+
+public class VoiceAIListAdapter extends RecyclerView.Adapter<VoiceAIListAdapter.ViewHolder> {
+
+    private final AsyncListDiffer<VoiceListBean> listDiffer;
+
+    private final LifecycleOwner lifecycleOwner;
+
+    public VoiceAIListAdapter(@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) {
+                if (oldItem.isAddIcon() == newItem.isAddIcon()) {
+                    return true;
+                }
+                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 (oldItem.isHasNew() != newItem.isHasNew()) {
+                    return false;
+                }
+                if (oldItem.isHasPro() != newItem.isHasPro()) {
+                    return false;
+                }
+                if (!Objects.equals(oldItem.getAvatarUrl(), newItem.getAvatarUrl())) {
+                    return false;
+                }
+                return true;
+            }
+        });
+    }
+
+    @NonNull
+    @Override
+    public VoiceAIListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        Context context = parent.getContext();
+        LayoutInflater layoutInflater = LayoutInflater.from(context);
+        return new ViewHolder(ItemVoiceAiListBinding.inflate(layoutInflater));
+    }
+
+
+    @Override
+    public void onBindViewHolder(@NonNull VoiceAIListAdapter.ViewHolder holder, int position) {
+        holder.bind(listDiffer.getCurrentList().get(position));
+    }
+
+    public void submit(List<VoiceListBean> itemBeanList) {
+        listDiffer.submitList(itemBeanList);
+    }
+
+
+    @Override
+    public int getItemCount() {
+        return listDiffer.getCurrentList().size();
+    }
+
+    public class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final ItemVoiceAiListBinding binding;
+
+        public ViewHolder(@NonNull ItemVoiceAiListBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+            binding.setLifecycleOwner(lifecycleOwner);
+        }
+
+
+        public void bind(VoiceListBean itemBean) {
+            binding.setBean(itemBean);
+        }
+    }
+
+}

+ 158 - 4
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java

@@ -1,6 +1,5 @@
 package com.atmob.voiceai.module.voiceai;
 
-import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
 
 import androidx.annotation.ColorInt;
@@ -9,31 +8,58 @@ import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Transformations;
 
 import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.common.data.KVUtils;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
+import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
+import com.atmob.voiceai.data.api.bean.TypeListBean;
+import com.atmob.voiceai.data.api.bean.VoiceListBean;
+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.data.repositories.MemberRepository;
+import com.atmob.voiceai.data.repositories.VoiceAIRepository;
+import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.utils.BoxingUtil;
 import com.atmob.voiceai.utils.SpannableUtil;
+import com.atmob.voiceai.utils.ToastUtil;
+
+import java.util.ArrayList;
+import java.util.List;
 
 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 VoiceAIViewModel extends BaseViewModel {
 
+    private final String LAST_GENERATE_TEXT = "last_generate_text";
 
     private final LiveData<CharSequence> voiceProbationTxt;
     private final MutableLiveData<String> voicePrintTxt = new MutableLiveData<>();
     private final LiveData<String> voicePrintLimitTxt;
     private final MemberRepository memberRepository;
-
+    private final MutableLiveData<Integer> adFreeGenerateNumber = new MutableLiveData<>();
     @ColorInt
     private final int freeTxtColor;
+    private final VoiceAIRepository voiceAIRepository;
+
+    private final MutableLiveData<List<CloneVoiceListBean>> userCloneList = new MutableLiveData<>();
+
+    private final MutableLiveData<List<TypeListBean>> voiceTypeList = new MutableLiveData<>();
+    private final MutableLiveData<List<VoiceListBean>> voiceDetailList = new MutableLiveData<>();
+
+    private int currentTypeId = -1;
+    private Disposable voiceListDisposable;
 
     @Inject
-    public VoiceAIViewModel(MemberRepository memberRepository) {
+    public VoiceAIViewModel(MemberRepository memberRepository, VoiceAIRepository voiceAIRepository) {
         this.memberRepository = memberRepository;
+        this.voiceAIRepository = voiceAIRepository;
         voicePrintLimitTxt = Transformations.map(voicePrintTxt, txt -> {
             int length = 0;
             if (!TextUtils.isEmpty(txt)) {
@@ -42,13 +68,29 @@ public class VoiceAIViewModel extends BaseViewModel {
             return length + "/200";
         });
         freeTxtColor = ContextUtil.getApplication().getResources().getColor(R.color.voice_free_crux_txt_color);
-        voiceProbationTxt = Transformations.map(memberRepository.getAdFreeGenerateNumber(), number -> {
+        voiceProbationTxt = Transformations.map(adFreeGenerateNumber, number -> {
             if (BoxingUtil.boxing(number) <= 0) {
                 return null;
             }
             String allTxt = ContextUtil.getContext().getString(R.string.voice_ai_get_free_number, number);
             return SpannableUtil.getSpannableStringBuilder(allTxt, String.valueOf(number), freeTxtColor, false);
         });
+        voicePrintTxt.setValue(KVUtils.getDefault().getString(LAST_GENERATE_TEXT, ""));
+
+        iniVoiceAI();
+    }
+
+    public LiveData<List<VoiceListBean>> getVoiceDetailList() {
+        return voiceDetailList;
+    }
+
+    public LiveData<List<TypeListBean>> getVoiceTypeList() {
+        return voiceTypeList;
+    }
+
+
+    public LiveData<Integer> getAdFreeGenerateNumber() {
+        return adFreeGenerateNumber;
     }
 
     public LiveData<Boolean> isMember() {
@@ -74,4 +116,116 @@ public class VoiceAIViewModel extends BaseViewModel {
     public void onGenerateClick() {
 
     }
+
+
+    private void iniVoiceAI() {
+        refreshVoiceInfo();
+        requestVoiceTypeList();
+    }
+
+    private void refreshVoiceInfo() {
+        voiceAIRepository.requestVoiceInfo().subscribe(new SingleObserver<VoiceInfoResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                addDisposable(d);
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceInfoResponse voiceInfoResponse) {
+                adFreeGenerateNumber.setValue(voiceInfoResponse.getVoiceTimes());
+                List<CloneVoiceListBean> cloneVoiceList = voiceInfoResponse.getCloneVoiceList();
+                if (cloneVoiceList != null && !cloneVoiceList.isEmpty()) {
+                    for (CloneVoiceListBean cloneVoiceListBean : cloneVoiceList) {
+                        cloneVoiceListBean.setCloneVoiceAvatar(voiceInfoResponse.getCloneVoiceAvatar());
+                        cloneVoiceListBean.setCloneVoiceName(voiceInfoResponse.getCloneVoiceName());
+                    }
+                }
+                userCloneList.setValue(cloneVoiceList);
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                e.printStackTrace();
+            }
+        });
+    }
+
+
+    private void requestVoiceTypeList() {
+        voiceAIRepository.requestVoiceType().subscribe(new SingleObserver<VoiceTypeResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                addDisposable(d);
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceTypeResponse voiceTypeResponse) {
+                voiceTypeList.setValue(voiceTypeResponse.getTypeList());
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+            }
+        });
+    }
+
+    public void setLastVoiceGenerateTxt(String generateTxt) {
+        KVUtils.getDefault().putString(LAST_GENERATE_TEXT, generateTxt);
+    }
+
+    public void refreshVoiceList(int id) {
+        if (id == this.currentTypeId) {
+            return;
+        }
+        this.currentTypeId = id;
+        if (voiceListDisposable != null && !voiceListDisposable.isDisposed()) {
+            voiceListDisposable.dispose();
+        }
+        voiceAIRepository.requestVoiceList(id).subscribe(new SingleObserver<VoiceListResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                voiceListDisposable = d;
+                addDisposable(d);
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceListResponse voiceListResponse) {
+                List<VoiceListBean> voiceList = voiceListResponse.getVoiceList();
+                if (voiceList == null) {
+                    voiceList = new ArrayList<>();
+                }
+                List<CloneVoiceListBean> value = userCloneList.getValue();
+                if (value == null || value.isEmpty()) {
+                    VoiceListBean cloneVoiceListBean = new VoiceListBean();
+                    cloneVoiceListBean.setAddIcon(true);
+                    cloneVoiceListBean.setId(-1);
+                    cloneVoiceListBean.setName(ContextUtil.getContext().getString(R.string.add_voice));
+                    voiceList.add(0, cloneVoiceListBean);
+                } else {
+                    List<VoiceListBean> list = getVoiceListBeans(value);
+                    voiceList.addAll(0, list);
+                }
+                voiceDetailList.setValue(voiceList);
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+            }
+        });
+    }
+
+    @androidx.annotation.NonNull
+    private List<VoiceListBean> getVoiceListBeans(List<CloneVoiceListBean> value) {
+        List<VoiceListBean> list = new ArrayList<>();
+        for (CloneVoiceListBean cloneVoiceListBean : value) {
+            VoiceListBean voiceListBean = new VoiceListBean();
+            voiceListBean.setName(cloneVoiceListBean.getCloneVoiceName());
+            voiceListBean.setAvatarUrl(cloneVoiceListBean.getCloneVoiceAvatar());
+            voiceListBean.setId(cloneVoiceListBean.getId());
+            list.add(voiceListBean);
+        }
+        return list;
+    }
 }

+ 74 - 0
app/src/main/java/com/atmob/voiceai/utils/GridLayoutItemDecoration.java

@@ -0,0 +1,74 @@
+package com.atmob.voiceai.utils;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.atmob.common.logging.AtmobLog;
+
+public class GridLayoutItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final int spanCount;
+    private final float horizontalGapPercent;
+    private final float verticalGapPercent;
+
+    private boolean showLastVertical = true;
+
+    private int lastBottomGap;
+
+
+    public GridLayoutItemDecoration(int spanCount, float horizontalGapPercent, float verticalGapPercent) {
+        this(spanCount, horizontalGapPercent, verticalGapPercent, true);
+    }
+
+    public GridLayoutItemDecoration(int spanCount, float horizontalGapPercent, float verticalGapPercent, boolean showLastVertical) {
+        this(spanCount, horizontalGapPercent, verticalGapPercent, showLastVertical, 0);
+    }
+
+    public GridLayoutItemDecoration(int spanCount, float horizontalGapPercent, float verticalGapPercent, boolean showLastVertical, int lastBottomGap) {
+        this.spanCount = spanCount;
+        this.horizontalGapPercent = horizontalGapPercent;
+        this.verticalGapPercent = verticalGapPercent;
+        this.showLastVertical = showLastVertical;
+        this.lastBottomGap = lastBottomGap;
+    }
+
+    @Override
+    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+        int horizontalGap = (int) (parent.getMeasuredWidth() * horizontalGapPercent);
+        int verticalGap = (int) (parent.getMeasuredWidth() * verticalGapPercent);
+        int spanSize = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanSize();
+        int singleGap = horizontalGap * (spanCount - 1) / spanCount;
+        int childAdapterPosition = parent.getChildAdapterPosition(view);
+        int i = childAdapterPosition % spanSize;
+        outRect.top = 0;
+        outRect.bottom = 0;
+        boolean isScope = parent.getAdapter() != null && parent.getAdapter().getItemCount() - spanSize <= childAdapterPosition;
+        if (!showLastVertical && isScope) {
+            outRect.bottom = 0;
+        } else if (lastBottomGap > 0 && isScope) {
+            outRect.bottom = lastBottomGap;
+        } else {
+            outRect.bottom = verticalGap;
+        }
+        outRect.left = 0;
+        outRect.right = 0;
+        if (i == 0) {
+            outRect.right = singleGap;
+        } else if (i == spanCount - 1) {
+            outRect.left = singleGap;
+        } else {
+            outRect.left = singleGap / 2;
+            outRect.right = singleGap / 2;
+        }
+        AtmobLog.d("TAG", view.toString() + " getItemOffsets: " + outRect.bottom + " childAdapterPosition:" + childAdapterPosition);
+    }
+
+    public void setLastBottomGap(int lastBottomGap) {
+        this.lastBottomGap = lastBottomGap;
+    }
+}

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


+ 5 - 0
app/src/main/res/drawable/bg_voice_ai_tab_normal.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="100dp" />
+    <solid android:color="#32333C" />
+</shape>

+ 5 - 0
app/src/main/res/drawable/bg_voice_ai_tab_selected.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="100dp" />
+    <solid android:color="@color/colorPrimaryVariant" />
+</shape>

+ 200 - 162
app/src/main/res/layout/fragment_voice_ai.xml

@@ -12,105 +12,106 @@
         <import type="com.atmob.common.ui.SizeUtil" />
     </data>
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:background="@color/colorPrimary">
+        android:orientation="vertical">
 
-        <ImageView
-            android:id="@+id/iv_header_bg"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:src="@drawable/bg_voice_ai_header_top_bg"
-            app:layout_constraintDimensionRatio="836:556"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintWidth_percent="0.5083333333333333" />
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/colorPrimary">
 
+            <ImageView
+                android:id="@+id/iv_header_bg"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:src="@drawable/bg_voice_ai_header_top_bg"
+                app:layout_constraintDimensionRatio="836:556"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintWidth_percent="0.5083333333333333" />
 
-        <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" />
 
-        <View
-            android:id="@+id/v_tool_bar"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            app:layout_constraintDimensionRatio="360:44"
-            app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+            <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" />
 
-        <View
-            android:id="@+id/v_vip_bg"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
-            android:background="@drawable/bg_voice_ai_vip"
-            app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
-            app:layout_constraintDimensionRatio="58:28"
-            app:layout_constraintStart_toStartOf="@+id/v_tool_bar"
-            app:layout_constraintTop_toTopOf="@+id/v_tool_bar"
-            app:layout_constraintWidth_percent="0.1611111111111111" />
-
-        <ImageView
-            android:id="@+id/iv_vip"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:src="@drawable/icon_vip"
-            app:layout_constraintBottom_toBottomOf="@+id/v_vip_bg"
-            app:layout_constraintDimensionRatio="1:1"
-            app:layout_constraintHorizontal_chainStyle="packed"
-            app:layout_constraintLeft_toLeftOf="@+id/v_vip_bg"
-            app:layout_constraintRight_toLeftOf="@+id/tv_vip_title"
-            app:layout_constraintTop_toTopOf="@+id/v_vip_bg"
-            app:layout_constraintWidth_percent="0.0444444444444444" />
-
-        <TextView
-            android:id="@+id/tv_vip_title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="4dp"
-            android:text="@string/voice_pro"
-            android:textColor="@color/colorPrimaryVariant"
-            android:textSize="14dp"
-            app:layout_constraintBottom_toBottomOf="@+id/v_vip_bg"
-            app:layout_constraintLeft_toRightOf="@+id/iv_vip"
-            app:layout_constraintRight_toRightOf="@+id/v_vip_bg"
-            app:layout_constraintTop_toTopOf="@+id/v_vip_bg" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/voice_ai"
-            android:textColor="@color/white"
-            android:textSize="18sp"
-            android:textStyle="bold"
-            app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
-            app:layout_constraintEnd_toEndOf="@+id/v_tool_bar"
-            app:layout_constraintStart_toStartOf="@+id/v_tool_bar"
-            app:layout_constraintTop_toTopOf="@+id/v_tool_bar" />
-
-        <ImageView
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_marginEnd="@dimen/app_common_page_horizontal_padding"
-            android:src="@drawable/icon_voice_ai_menu"
-            app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
-            app:layout_constraintDimensionRatio="1:1"
-            app:layout_constraintEnd_toEndOf="@+id/v_tool_bar"
-            app:layout_constraintTop_toTopOf="@+id/v_tool_bar"
-            app:layout_constraintWidth_percent="0.0666666666666667" />
-
-        <androidx.core.widget.NestedScrollView
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/v_tool_bar">
+            <View
+                android:id="@+id/v_tool_bar"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                app:layout_constraintDimensionRatio="360:44"
+                app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+
+            <View
+                android:id="@+id/v_vip_bg"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+                android:background="@drawable/bg_voice_ai_vip"
+                app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
+                app:layout_constraintDimensionRatio="58:28"
+                app:layout_constraintStart_toStartOf="@+id/v_tool_bar"
+                app:layout_constraintTop_toTopOf="@+id/v_tool_bar"
+                app:layout_constraintWidth_percent="0.1611111111111111" />
+
+            <ImageView
+                android:id="@+id/iv_vip"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:src="@drawable/icon_vip"
+                app:layout_constraintBottom_toBottomOf="@+id/v_vip_bg"
+                app:layout_constraintDimensionRatio="1:1"
+                app:layout_constraintHorizontal_chainStyle="packed"
+                app:layout_constraintLeft_toLeftOf="@+id/v_vip_bg"
+                app:layout_constraintRight_toLeftOf="@+id/tv_vip_title"
+                app:layout_constraintTop_toTopOf="@+id/v_vip_bg"
+                app:layout_constraintWidth_percent="0.0444444444444444" />
+
+            <TextView
+                android:id="@+id/tv_vip_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="4dp"
+                android:text="@string/voice_pro"
+                android:textColor="@color/colorPrimaryVariant"
+                android:textSize="14dp"
+                app:layout_constraintBottom_toBottomOf="@+id/v_vip_bg"
+                app:layout_constraintLeft_toRightOf="@+id/iv_vip"
+                app:layout_constraintRight_toRightOf="@+id/v_vip_bg"
+                app:layout_constraintTop_toTopOf="@+id/v_vip_bg" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/voice_ai"
+                android:textColor="@color/white"
+                android:textSize="18sp"
+                android:textStyle="bold"
+                app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
+                app:layout_constraintEnd_toEndOf="@+id/v_tool_bar"
+                app:layout_constraintStart_toStartOf="@+id/v_tool_bar"
+                app:layout_constraintTop_toTopOf="@+id/v_tool_bar" />
+
+            <ImageView
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_marginEnd="@dimen/app_common_page_horizontal_padding"
+                android:src="@drawable/icon_voice_ai_menu"
+                app:layout_constraintBottom_toBottomOf="@+id/v_tool_bar"
+                app:layout_constraintDimensionRatio="1:1"
+                app:layout_constraintEnd_toEndOf="@+id/v_tool_bar"
+                app:layout_constraintTop_toTopOf="@+id/v_tool_bar"
+                app:layout_constraintWidth_percent="0.0666666666666667" />
 
             <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/container"
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content">
+                android:layout_height="wrap_content"
+                app:layout_constraintTop_toBottomOf="@+id/v_tool_bar">
 
                 <Space
                     android:id="@+id/space1"
@@ -173,43 +174,33 @@
                     app:layout_constraintDimensionRatio="360:8"
                     app:layout_constraintTop_toBottomOf="@+id/v_free_get_bg" />
 
-                <Space
-                    android:id="@+id/sapce3"
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    app:layout_constraintDimensionRatio="360:12"
-                    app:layout_constraintTop_toBottomOf="@+id/sapce2" />
 
                 <TextView
                     android:id="@+id/tv_your_txt_title"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+                    android:layout_marginTop="12dp"
                     android:text="@string/voice_your_text"
                     android:textColor="@color/white"
                     android:textSize="16sp"
                     android:textStyle="bold"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/sapce3" />
+                    app:layout_constraintTop_toBottomOf="@+id/sapce2" />
 
 
-                <Space
-                    android:id="@+id/sapce4"
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    app:layout_constraintDimensionRatio="360:12"
-                    app:layout_constraintTop_toBottomOf="@+id/tv_your_txt_title" />
-
                 <View
                     android:id="@+id/v_voice_print_bg"
                     android:layout_width="match_parent"
                     android:layout_height="0dp"
                     android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
+                    android:layout_marginTop="12dp"
                     android:background="@drawable/bg_voice_ai"
                     app:layout_constraintDimensionRatio="336:148"
-                    app:layout_constraintTop_toBottomOf="@+id/sapce4" />
+                    app:layout_constraintTop_toBottomOf="@+id/tv_your_txt_title" />
 
                 <com.atmob.voiceai.widget.IgnoreNoEnterActionEditText
+                    android:id="@+id/et_voice_print"
                     android:layout_width="0dp"
                     android:layout_height="0dp"
                     android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
@@ -253,24 +244,44 @@
                     app:layout_constraintTop_toTopOf="@+id/tv_print_clear"
                     tools:text="0/200" />
 
-                <Space
-                    android:id="@+id/space5"
-                    android:layout_width="match_parent"
-                    android:layout_height="0dp"
-                    app:layout_constraintDimensionRatio="360:30"
-                    app:layout_constraintTop_toBottomOf="@+id/v_voice_print_bg" />
 
                 <TextView
                     android:id="@+id/tv_all_voice"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+                    android:layout_marginTop="30dp"
                     android:text="@string/voice_all"
                     android:textColor="@color/white"
                     android:textSize="16sp"
                     android:textStyle="bold"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/space5" />
+                    app:layout_constraintTop_toBottomOf="@+id/v_voice_print_bg" />
+
+                <Space
+                    android:id="@+id/space6"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    app:layout_constraintTop_toBottomOf="@+id/tv_all_voice" />
+
+                <com.google.android.material.tabs.TabLayout
+                    android:id="@+id/voice_tab"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+                    android:layout_marginTop="@dimen/app_common_page_horizontal_padding"
+                    android:background="@color/colorPrimary"
+                    android:fadingEdgeLength="30dp"
+                    android:requiresFadingEdge="horizontal"
+                    app:layout_constraintDimensionRatio="334:28"
+                    app:layout_constraintTop_toBottomOf="@+id/space6"
+                    app:layout_scrollFlags="noScroll"
+                    app:tabIndicator="@null"
+                    app:tabMinWidth="0dp"
+                    app:tabMode="scrollable"
+                    app:tabPaddingEnd="@dimen/app_common_page_horizontal_padding"
+                    app:tabPaddingStart="0dp"
+                    app:tabRippleColor="#00000000" />
 
 
                 <androidx.constraintlayout.widget.Group
@@ -282,60 +293,87 @@
             </androidx.constraintlayout.widget.ConstraintLayout>
 
 
-        </androidx.core.widget.NestedScrollView>
+            <androidx.constraintlayout.widget.Group
+                isGone="@{voiceAIViewModel.isMember}"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:constraint_referenced_ids="v_vip_bg,iv_vip,tv_vip_title" />
 
-        <View
-            android:id="@+id/v_generate"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:background="@drawable/bg_voice_generate"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintDimensionRatio="360:76" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
 
-        <View
-            android:id="@+id/v_generate_btn"
+        <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="0dp"
-            android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
-            android:background="@drawable/bg_voice_ai_btn"
-            android:onClick="@{()-> voiceAIViewModel.onGenerateClick()}"
-            app:layout_constraintBottom_toBottomOf="@+id/v_generate"
-            app:layout_constraintDimensionRatio="336:48"
-            app:layout_constraintTop_toTopOf="@+id/v_generate" />
-
-        <ImageView
-            android:id="@+id/iv_ad"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:src="@drawable/icon_ad_film"
-            app:layout_constraintBottom_toBottomOf="@+id/v_generate_btn"
-            app:layout_constraintDimensionRatio="1:1"
-            app:layout_constraintHorizontal_chainStyle="packed"
-            app:layout_constraintLeft_toLeftOf="@+id/v_generate_btn"
-            app:layout_constraintRight_toLeftOf="@+id/tv_generate_txt"
-            app:layout_constraintTop_toTopOf="@+id/v_generate_btn"
-            app:layout_constraintWidth_percent="0.0388888888888889" />
-
-        <TextView
-            android:id="@+id/tv_generate_txt"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="6dp"
-            android:text="@string/voice_ai_generate"
-            android:textColor="@color/colorPrimary"
-            android:textSize="17sp"
-            android:textStyle="bold"
-            app:layout_constraintBottom_toBottomOf="@+id/v_generate_btn"
-            app:layout_constraintLeft_toRightOf="@+id/iv_ad"
-            app:layout_constraintRight_toRightOf="@+id/v_generate_btn"
-            app:layout_constraintTop_toTopOf="@+id/v_generate_btn" />
-
-
-        <androidx.constraintlayout.widget.Group
-            isGone="@{voiceAIViewModel.isMember}"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            app:constraint_referenced_ids="v_vip_bg,iv_vip,tv_vip_title" />
+            android:layout_weight="1">
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/ry_voice_model"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_marginTop="16dp"
+                android:fadingEdge="vertical"
+                android:fadingEdgeLength="10dp"
+                android:requiresFadingEdge="vertical"
+                android:paddingHorizontal="@dimen/app_common_page_horizontal_padding"
+                tools:itemCount="10"
+                tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+                tools:listitem="@layout/item_voice_ai_list"
+                tools:spanCount="4" />
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true">
+
+                <View
+                    android:id="@+id/v_generate"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:background="@drawable/bg_voice_generate"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintDimensionRatio="360:76" />
+
+                <View
+                    android:id="@+id/v_generate_btn"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
+                    android:background="@drawable/bg_voice_ai_btn"
+                    android:onClick="@{()-> voiceAIViewModel.onGenerateClick()}"
+                    app:layout_constraintBottom_toBottomOf="@+id/v_generate"
+                    app:layout_constraintDimensionRatio="336:48"
+                    app:layout_constraintTop_toTopOf="@+id/v_generate" />
+
+                <ImageView
+                    android:id="@+id/iv_ad"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:src="@drawable/icon_ad_film"
+                    app:layout_constraintBottom_toBottomOf="@+id/v_generate_btn"
+                    app:layout_constraintDimensionRatio="1:1"
+                    app:layout_constraintHorizontal_chainStyle="packed"
+                    app:layout_constraintLeft_toLeftOf="@+id/v_generate_btn"
+                    app:layout_constraintRight_toLeftOf="@+id/tv_generate_txt"
+                    app:layout_constraintTop_toTopOf="@+id/v_generate_btn"
+                    app:layout_constraintWidth_percent="0.0388888888888889" />
+
+                <TextView
+                    android:id="@+id/tv_generate_txt"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="6dp"
+                    android:text="@string/voice_ai_generate"
+                    android:textColor="@color/colorPrimary"
+                    android:textSize="17sp"
+                    android:textStyle="bold"
+                    app:layout_constraintBottom_toBottomOf="@+id/v_generate_btn"
+                    app:layout_constraintLeft_toRightOf="@+id/iv_ad"
+                    app:layout_constraintRight_toRightOf="@+id/v_generate_btn"
+                    app:layout_constraintTop_toTopOf="@+id/v_generate_btn" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </RelativeLayout>
+
 
-    </androidx.constraintlayout.widget.ConstraintLayout>
+    </LinearLayout>
 </layout>

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

@@ -0,0 +1,57 @@
+<?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" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <Space
+            android:id="@+id/space_content"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintTop_toTopOf="parent" />
+
+
+        <ImageView
+            isGone="@{!bean.isAddIcon}"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/icon_voice_ai_add"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <ImageView
+            imageUrl="@{bean.avatarUrl}"
+            isGone="@{bean.isAddIcon}"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/icon_voice_ai_add"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <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/space_content"
+            app:layout_constraintStart_toStartOf="@+id/space_content"
+            app:layout_constraintTop_toBottomOf="@+id/space_content"
+            tools:text="Add Voice" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 17 - 0
app/src/main/res/layout/item_voice_ai_tab.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/tab_title"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:minWidth="72dp"
+    android:paddingHorizontal="8dp"
+    android:paddingVertical="6dp"
+    android:gravity="center"
+    tools:text="AII"
+    android:textSize="14sp"
+    android:textColor="@color/white50"
+    android:background="@drawable/bg_voice_ai_tab_normal">
+
+
+</TextView>

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

@@ -12,4 +12,6 @@
     <string name="voice_ai_generate">Generate</string>
     <string name="voice_ai_get_free">Get more</string>
     <string name="voice_ai_get_free_number">You have %d chances today.</string>
+    <string name="net_error_message">network timeout</string>
+    <string name="add_voice">Add Voice</string>
 </resources>

+ 1 - 1
app/src/main/res/values/themes.xml

@@ -5,7 +5,7 @@
         <item name="colorPrimary">@color/colorPrimary</item>
         <item name="colorPrimaryVariant">@color/colorPrimaryVariant</item>
         <item name="colorPrimaryDark">@color/colorPrimary</item>
-        <item name="colorAccent">@color/colorPrimary</item>
+        <item name="colorAccent">@color/white</item>
         <item name="colorOnPrimary">@color/white</item>
         <!-- Status bar color. -->
         <item name="android:statusBarColor">@android:color/transparent</item>