优化UI,加入模型/音色选择

This commit is contained in:
feie9456 2026-03-19 07:47:20 +08:00
parent d09d6f1cc0
commit a44ddb6dff
26 changed files with 298 additions and 222 deletions

View File

@ -28,7 +28,8 @@
<activity
android:name=".ui.activity.CallActivity"
android:exported="false">
android:exported="false"
android:theme="@style/Theme.DUIX.Call">
</activity>
</application>

View File

@ -0,0 +1,15 @@
package ai.guiji.duix.test.model
/**
* 数字人形象模型
* @param name 中文显示名
* @param voice 语音音色名传给 Omni Realtime
* @param modelUrl 模型资源下载地址
* @param avatarRes 头像资源 IDmipmap
*/
data class AvatarModel(
val name: String,
val voice: String,
val modelUrl: String,
val avatarRes: Int
)

View File

@ -30,10 +30,12 @@ public class OmniRealtimeClient {
private final OkHttpClient okHttpClient;
private WebSocket webSocket;
private final Callback callback;
private final String voice;
private final AtomicBoolean isConnected = new AtomicBoolean(false);
private final AtomicBoolean userSpeaking = new AtomicBoolean(false);
public OmniRealtimeClient(Callback callback) {
public OmniRealtimeClient(String voice, Callback callback) {
this.voice = voice;
this.callback = callback;
this.okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
@ -98,7 +100,7 @@ public class OmniRealtimeClient {
try {
JSONObject session = new JSONObject();
session.put("modalities", new org.json.JSONArray().put("text").put("audio"));
session.put("voice", "Ethan");
session.put("voice", voice);
session.put("input_audio_format", "pcm");
session.put("output_audio_format", "pcm");
session.put("instructions", "你是一名面向中国老年群体的心理陪伴 AI。你的职责是为老人提供温和、耐心、尊重、稳定的情感陪伴、情绪支持、回忆唤起、生活关怀和风险识别服务。你不是医生也不是心理治疗师不能替代专业诊疗但应在必要时建议联系家属、社区、医生或急救资源。\n" +

View File

@ -25,7 +25,6 @@ import android.util.Log
import android.view.View
import android.widget.CompoundButton
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import java.io.File
import java.io.FileInputStream
@ -39,6 +38,7 @@ class CallActivity : BaseActivity() {
}
private var modelUrl = ""
private var voice = "Ethan"
private var debug = false
private var mMessage = ""
@ -82,6 +82,7 @@ class CallActivity : BaseActivity() {
setContentView(binding.root)
modelUrl = intent.getStringExtra("modelUrl") ?: ""
voice = intent.getStringExtra("voice") ?: "Ethan"
debug = intent.getBooleanExtra("debug", false)
Glide.with(mContext).load("file:///android_asset/bg/bg1.png").into(binding.ivBg)
@ -311,14 +312,15 @@ class CallActivity : BaseActivity() {
applyMessage("AI对话: 连接中...")
binding.tvSubtitle.visibility = View.VISIBLE
binding.tvSubtitle.text = "正在连接..."
binding.btnAIConversation.text = getString(R.string.ai_fab_stop)
binding.btnAIConversation.setImageResource(R.drawable.ic_mic_off)
binding.btnAIConversation.setBackgroundResource(R.drawable.shape_fab_ai_active)
omniClient = OmniRealtimeClient(object : OmniRealtimeClient.Callback {
omniClient = OmniRealtimeClient(voice, object : OmniRealtimeClient.Callback {
override fun onConnected() {
mainHandler.post {
binding.tvSubtitle.text = "已连接,请对着麦克风说话"
binding.btnCameraToggle.visibility = View.VISIBLE
binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_off)
binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_off)
binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
isCameraEnabled = false
applyMessage("AI对话: 已连接")
@ -425,7 +427,8 @@ class CallActivity : BaseActivity() {
mainHandler.post {
if (!isFinishing) {
binding.btnAIConversation.text = getString(R.string.ai_fab_start)
binding.btnAIConversation.setImageResource(R.drawable.ic_mic)
binding.btnAIConversation.setBackgroundResource(R.drawable.shape_fab_ai)
binding.btnCameraToggle.visibility = View.GONE
binding.tvSubtitle.visibility = View.GONE
binding.tvSubtitle.text = ""
@ -451,11 +454,11 @@ class CallActivity : BaseActivity() {
}
}, binding.cameraPreview)
cameraFrameCapture?.start()
binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_on)
binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_on)
binding.btnCameraToggle.contentDescription = getString(R.string.camera_on)
} else {
stopCamera()
binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_off)
binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_off)
binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
}
}

View File

@ -1,113 +1,96 @@
package ai.guiji.duix.test.ui.activity
import ai.guiji.duix.sdk.client.BuildConfig
import ai.guiji.duix.sdk.client.VirtualModelUtil
import ai.guiji.duix.test.R
import ai.guiji.duix.test.databinding.ActivityMainBinding
import ai.guiji.duix.test.model.AvatarModel
import ai.guiji.duix.test.ui.adapter.AvatarAdapter
import ai.guiji.duix.test.ui.dialog.LoadingDialog
import ai.guiji.duix.test.ui.dialog.ModelSelectorDialog
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import java.io.File
class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding
private var mLoadingDialog: LoadingDialog?=null
private var mLoadingDialog: LoadingDialog? = null
private var mLastProgress = 0
val models = arrayListOf(
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/bendi3_20240518.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/airuike_20240409.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/675429759852613_7f8d9388a4213080b1820b83dd057cfb_optim_m80.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674402003804229_f6e86fb375c4f1f1b82b24f7ee4e7cb4_optim_m80.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674400178376773_3925e756433c5a9caa9b9d54147ae4ab_optim_m80.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674397294927941_6e297e18a4bdbe35c07a6ae48a1f021f_optim_m80.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/674393494597701_f49fcf68f5afdb241d516db8a7d88a7b_optim_m80.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/651705983152197_ccf3256b2449c76e77f94276dffcb293_optim_m80.zip",
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/627306542239813_1871244b5e6912efc636ba31ea4c5c6d_optim_m80.zip",
private val baseConfigUrl = "https://dl.xn--876a.net/models/gj_dh_res.zip"
private val avatarModels = listOf(
AvatarModel("晨煦", "Ethan", "https://dl.xn--876a.net/models/bendi3_20240518.zip", R.mipmap.ethan),
AvatarModel("詹妮弗", "Jennifer", "https://dl.xn--876a.net/models/Emma.zip", R.mipmap.jennifer),
AvatarModel("月白", "Moon", "https://dl.xn--876a.net/models/Kai.zip", R.mipmap.moon),
AvatarModel("艾登", "Aiden", "https://dl.xn--876a.net/models/Leo.zip", R.mipmap.leo),
AvatarModel("四月", "Maia", "https://dl.xn--876a.net/models/Lily.zip", R.mipmap.lily),
AvatarModel("奥利弗", "Andre", "https://dl.xn--876a.net/models/Oliver.zip", R.mipmap.oliver),
AvatarModel("索菲亚", "Sonrisa", "https://dl.xn--876a.net/models/Sofia.zip", R.mipmap.sofia),
)
private var mBaseConfigUrl = ""
private var mModelUrl = ""
private var mSelectedModel: AvatarModel? = null
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvSdkVersion.text = "SDK Version: ${BuildConfig.VERSION_NAME}"
binding.btnMoreModel.setOnClickListener {
val modelSelectorDialog = ModelSelectorDialog(mContext, models, object : ModelSelectorDialog.Listener{
override fun onSelect(url: String) {
binding.etUrl.setText(url)
binding.rvAvatars.layoutManager = GridLayoutManager(this, 2)
binding.rvAvatars.adapter = AvatarAdapter(avatarModels, object : AvatarAdapter.Callback {
override fun onSelect(model: AvatarModel) {
mSelectedModel = model
play(model)
}
})
modelSelectorDialog.show()
}
binding.btnPlay.setOnClickListener {
play()
}
}
private fun play(){
mBaseConfigUrl = binding.etBaseConfig.text.toString()
mModelUrl = binding.etUrl.text.toString()
if (TextUtils.isEmpty(mBaseConfigUrl)){
Toast.makeText(mContext, R.string.base_config_cannot_be_empty, Toast.LENGTH_SHORT).show()
return
}
if (TextUtils.isEmpty(mModelUrl)){
Toast.makeText(mContext, R.string.model_url_cannot_be_empty, Toast.LENGTH_SHORT).show()
return
}
private fun play(model: AvatarModel) {
mSelectedModel = model
checkBaseConfig()
}
private fun checkBaseConfig(){
if (VirtualModelUtil.checkBaseConfig(mContext)){
private fun checkBaseConfig() {
if (VirtualModelUtil.checkBaseConfig(mContext)) {
checkModel()
} else {
baseConfigDownload()
}
}
private fun checkModel(){
if (VirtualModelUtil.checkModel(mContext, mModelUrl)){
private fun checkModel() {
val model = mSelectedModel ?: return
if (VirtualModelUtil.checkModel(mContext, model.modelUrl)) {
jumpPlayPage()
} else {
modelDownload()
}
}
private fun jumpPlayPage(){
private fun jumpPlayPage() {
val model = mSelectedModel ?: return
val intent = Intent(mContext, CallActivity::class.java)
intent.putExtra("modelUrl", mModelUrl)
val debug = binding.switchDebug.isChecked
intent.putExtra("debug", debug)
intent.putExtra("modelUrl", model.modelUrl)
intent.putExtra("voice", model.voice)
intent.putExtra("debug", false)
startActivity(intent)
}
private fun baseConfigDownload(){
private fun baseConfigDownload() {
mLoadingDialog?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading")
mLoadingDialog = LoadingDialog(mContext, "正在准备资源...")
mLoadingDialog?.show()
VirtualModelUtil.baseConfigDownload(mContext, mBaseConfigUrl, object :
VirtualModelUtil.baseConfigDownload(mContext, baseConfigUrl, object :
VirtualModelUtil.ModelDownloadCallback {
override fun onDownloadProgress(url: String?, current: Long, total: Long) {
val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){
if (progress != mLastProgress) {
mLastProgress = progress
runOnUiThread {
if (mLoadingDialog?.isShowing == true){
mLoadingDialog?.setContent("Config download(${progress}%)")
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("基础配置下载中(${progress}%)")
}
}
}
@ -115,11 +98,11 @@ class MainActivity : BaseActivity() {
override fun onUnzipProgress(url: String?, current: Long, total: Long) {
val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){
if (progress != mLastProgress) {
mLastProgress = progress
runOnUiThread {
if (mLoadingDialog?.isShowing == true){
mLoadingDialog?.setContent("Config unzip(${progress}%)")
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("基础配置解压中(${progress}%)")
}
}
}
@ -135,45 +118,37 @@ class MainActivity : BaseActivity() {
override fun onDownloadFail(url: String?, code: Int, msg: String?) {
runOnUiThread {
mLoadingDialog?.dismiss()
Toast.makeText(mContext, "BaseConfig download error: $msg", Toast.LENGTH_SHORT).show()
Toast.makeText(mContext, "资源下载失败: $msg", Toast.LENGTH_SHORT).show()
}
}
})
}
private fun modelDownload(){
private fun modelDownload() {
val model = mSelectedModel ?: return
mLoadingDialog?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading")
mLoadingDialog = LoadingDialog(mContext, "正在下载 ${model.name} ...")
mLoadingDialog?.show()
VirtualModelUtil.modelDownload(mContext, mModelUrl, object : VirtualModelUtil.ModelDownloadCallback{
override fun onDownloadProgress(
url: String?,
current: Long,
total: Long,
) {
VirtualModelUtil.modelDownload(mContext, model.modelUrl, object : VirtualModelUtil.ModelDownloadCallback {
override fun onDownloadProgress(url: String?, current: Long, total: Long) {
val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){
if (progress != mLastProgress) {
mLastProgress = progress
runOnUiThread {
if (mLoadingDialog?.isShowing == true){
mLoadingDialog?.setContent("Model download(${progress}%)")
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("${model.name} 下载中(${progress}%)")
}
}
}
}
override fun onUnzipProgress(
url: String?,
current: Long,
total: Long,
) {
override fun onUnzipProgress(url: String?, current: Long, total: Long) {
val progress = (current * 100 / total).toInt()
if (progress != mLastProgress){
if (progress != mLastProgress) {
mLastProgress = progress
runOnUiThread {
if (mLoadingDialog?.isShowing == true){
mLoadingDialog?.setContent("Model unzip(${progress}%)")
if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("${model.name} 解压中(${progress}%)")
}
}
}
@ -186,18 +161,12 @@ class MainActivity : BaseActivity() {
}
}
override fun onDownloadFail(
url: String?,
code: Int,
msg: String?,
) {
override fun onDownloadFail(url: String?, code: Int, msg: String?) {
runOnUiThread {
mLoadingDialog?.dismiss()
Toast.makeText(mContext, "Model download error: $msg", Toast.LENGTH_SHORT).show()
Toast.makeText(mContext, "下载失败: $msg", Toast.LENGTH_SHORT).show()
}
}
})
}
}

View File

@ -0,0 +1,38 @@
package ai.guiji.duix.test.ui.adapter
import ai.guiji.duix.test.databinding.ItemAvatarCardBinding
import ai.guiji.duix.test.model.AvatarModel
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class AvatarAdapter(
private val list: List<AvatarModel>,
private val callback: Callback
) : RecyclerView.Adapter<AvatarAdapter.ViewHolder>() {
class ViewHolder(val binding: ItemAvatarCardBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemAvatarCardBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val model = list[position]
holder.binding.ivAvatar.setImageResource(model.avatarRes)
holder.binding.tvName.text = model.name
holder.binding.cardAvatar.setOnClickListener {
callback.onSelect(model)
}
}
override fun getItemCount(): Int = list.size
interface Callback {
fun onSelect(model: AvatarModel)
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M12,15.2C13.77,15.2 15.2,13.77 15.2,12C15.2,10.23 13.77,8.8 12,8.8C10.23,8.8 8.8,10.23 8.8,12C8.8,13.77 10.23,15.2 12,15.2ZM9,2L7.17,4H4C2.9,4 2,4.9 2,6V18C2,19.1 2.9,20 4,20H20C21.1,20 22,19.1 22,18V6C22,4.9 21.1,4 20,4H16.83L15,2H9ZM12,17C9.24,17 7,14.76 7,12C7,9.24 9.24,7 12,7C14.76,7 17,9.24 17,12C17,14.76 14.76,17 12,17Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
<path
android:fillColor="@android:color/white"
android:pathData="M3.27,3.27L2,4.54l7,7v0.46c0,1.66 1.34,3 3,3c0.23,0 0.44,-0.03 0.65,-0.08l1.66,1.66C13.56,16.5 12.8,16.87 12,17.1V21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9l3.73,3.73 1.27,-1.27L3.27,3.27z"
android:strokeWidth="0" />
</vector>

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent">
<ImageView
@ -91,37 +90,34 @@
android:layout_marginBottom="12dp"
/>
<Button
<ImageButton
android:id="@+id/btnAIConversation"
android:text="@string/ai_fab_start"
android:textSize="14sp"
android:textColor="@color/white"
android:background="@drawable/shape_circle_fab"
android:src="@drawable/ic_mic"
android:background="@drawable/shape_fab_ai"
android:elevation="8dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="center"
android:layout_width="64dp"
android:layout_height="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="32dp"
android:layout_marginBottom="40dp"
android:contentDescription="@string/ai_conversation"
/>
<Button
<ImageButton
android:id="@+id/btnCameraToggle"
android:text="📷"
android:textSize="18sp"
android:textColor="@color/white"
android:background="@drawable/shape_circle_camera_off"
android:src="@drawable/ic_camera"
android:background="@drawable/shape_fab_camera_off"
android:elevation="8dp"
android:scaleType="center"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/btnAIConversation"
app:layout_constraintTop_toTopOf="@+id/btnAIConversation"
app:layout_constraintStart_toEndOf="@+id/btnAIConversation"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="24dp"
android:contentDescription="@string/camera_off"
/>

View File

@ -3,119 +3,56 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="12dp">
android:fitsSystemWindows="true"
android:background="@color/warm_bg">
<!-- 顶部标题区域 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="18dp"
android:textStyle="italic|bold"
android:paddingTop="48dp"
android:paddingBottom="4dp"
android:text="@string/app_title"
android:textColor="@color/warm_text_primary"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_sdk_version"
android:layout_width="wrap_content"
android:id="@+id/tv_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
android:paddingBottom="16dp"
android:text="@string/app_subtitle"
android:textColor="@color/warm_text_secondary"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@+id/tv_title" />
<ScrollView
<!-- 角色列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAvatars"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_sdk_version">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:clipToPadding="false"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/tvBottomHint"
app:layout_constraintTop_toBottomOf="@+id/tv_subtitle" />
<TextView
android:id="@+id/tvDownloadTips"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/main_download_tips"
android:textSize="13sp"
android:textColor="@color/black"
android:textStyle="bold"
android:id="@+id/tvBottomHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvBaseConfig"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/base_config_url"
android:textSize="13sp"
app:layout_constraintTop_toBottomOf="@+id/tvDownloadTips"
app:layout_constraintStart_toStartOf="parent"/>
<EditText
android:id="@+id/etBaseConfig"
android:layout_marginTop="12dp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/gj_dh_res.zip"
android:textSize="13sp"
app:layout_constraintTop_toBottomOf="@+id/tvBaseConfig"
/>
<TextView
android:id="@+id/tvModelUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/model_url"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etBaseConfig" />
<Button
android:id="@+id/btnMoreModel"
android:text="@string/more"
android:layout_marginTop="12dp"
android:layout_width="wrap_content"
android:layout_height="48dp"
app:layout_constraintTop_toBottomOf="@+id/tvModelUrl"
app:layout_constraintEnd_toEndOf="parent"
/>
<EditText
android:id="@+id/etUrl"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/bendi3_20240518.zip"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/btnMoreModel"
app:layout_constraintEnd_toStartOf="@+id/btnMoreModel"
/>
<Button
android:id="@+id/btnPlay"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="20dp"
android:text="@string/play"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etUrl" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchDebug"
android:text="@string/debug_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp"
android:layout_margin="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnPlay"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
android:gravity="center"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:text="@string/choose_companion_hint"
android:textColor="@color/warm_text_hint"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardAvatar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="12dp"
android:textColor="#333333"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@+id/ivAvatar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -9,4 +9,11 @@
<color name="white">#FFFFFFFF</color>
<color name="primary">#5a7bbe</color>
<color name="primary_99">#995a7bbe</color>
<!-- 温暖主题色 -->
<color name="warm_bg">#FFF5F0EB</color>
<color name="warm_text_primary">#FF3E2723</color>
<color name="warm_text_secondary">#FF8D6E63</color>
<color name="warm_text_hint">#FFBCAAA4</color>
<color name="warm_accent">#FFE8926F</color>
</resources>

View File

@ -1,5 +1,8 @@
<resources>
<string name="app_name">DUIX Demo</string>
<string name="app_name">AI 情感陪护</string>
<string name="app_title">AI 情感陪护</string>
<string name="app_subtitle">选一位陪伴者,开始温暖对话</string>
<string name="choose_companion_hint">点击头像即可开始对话</string>
<string name="play">Play</string>
<string name="base_config_url">BaseConfig Url:</string>
<string name="model_url">Model Url:</string>

View File

@ -1,15 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.DUIX.Test" parent="Theme.AppCompat.Light.NoActionBar">
<style name="Theme.DUIX.Test" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowBackground">@android:color/white</item>
<item name="android:windowBackground">@color/warm_bg</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowIsFloating">false</item>
<item name="colorPrimary">#fff</item>
<item name="colorPrimaryDark">#4349a9</item>
<item name="colorAccent">#4349a9</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="colorPrimary">@color/warm_bg</item>
<item name="colorPrimaryDark">@color/warm_accent</item>
<item name="colorAccent">@color/warm_accent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
<!-- CallActivity 全屏沉浸主题 -->
<style name="Theme.DUIX.Call" parent="Theme.DUIX.Test">
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>