优化UI,加入模型/音色选择
@ -28,7 +28,8 @@
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.CallActivity"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.DUIX.Call">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
15
test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package ai.guiji.duix.test.model
|
||||
|
||||
/**
|
||||
* 数字人形象模型
|
||||
* @param name 中文显示名
|
||||
* @param voice 语音音色名(传给 Omni Realtime)
|
||||
* @param modelUrl 模型资源下载地址
|
||||
* @param avatarRes 头像资源 ID(mipmap)
|
||||
*/
|
||||
data class AvatarModel(
|
||||
val name: String,
|
||||
val voice: String,
|
||||
val modelUrl: String,
|
||||
val avatarRes: Int
|
||||
)
|
||||
@ -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" +
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
10
test/src/main/res/drawable/ic_camera.xml
Normal 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>
|
||||
10
test/src/main/res/drawable/ic_mic.xml
Normal 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>
|
||||
14
test/src/main/res/drawable/ic_mic_off.xml
Normal 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>
|
||||
5
test/src/main/res/drawable/shape_fab_ai.xml
Normal 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>
|
||||
5
test/src/main/res/drawable/shape_fab_ai_active.xml
Normal 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>
|
||||
5
test/src/main/res/drawable/shape_fab_camera_off.xml
Normal 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>
|
||||
5
test/src/main/res/drawable/shape_fab_camera_on.xml
Normal 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>
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@ -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>
|
||||
42
test/src/main/res/layout/item_avatar_card.xml
Normal 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>
|
||||
BIN
test/src/main/res/mipmap-hdpi/ethan.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
test/src/main/res/mipmap-hdpi/jennifer.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
test/src/main/res/mipmap-hdpi/leo.webp
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
test/src/main/res/mipmap-hdpi/lily.webp
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
test/src/main/res/mipmap-hdpi/moon.webp
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
test/src/main/res/mipmap-hdpi/oliver.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
test/src/main/res/mipmap-hdpi/sofia.webp
Normal file
|
After Width: | Height: | Size: 76 KiB |
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||