diff --git a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt new file mode 100644 index 00000000..f5d2b696 --- /dev/null +++ b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/model/AvatarModel.kt @@ -0,0 +1,81 @@ +package ai.guiji.duix.test.model + +import ai.guiji.duix.test.R + +/** + * 数字人形象数据模型 + */ +data class AvatarModel( + val name: String, + val description: String, + val url: String, + val avatarResId: Int = 0 // 头像资源 ID,0 表示无头像 +) { + companion object { + /** + * 官方公开的 4 个数字人形象 (v2.0.1) - 推荐使用 + */ + val OFFICIAL_AVATARS = arrayListOf( + AvatarModel( + name = "Leo", + description = "年轻男性 · 休闲风格", + url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Leo.zip", + avatarResId = R.drawable.avatar_leo + ), + AvatarModel( + name = "Oliver", + description = "成熟男性 · 商务正装", + url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Oliver.zip", + avatarResId = R.drawable.avatar_oliver + ), + AvatarModel( + name = "Sofia", + description = "知性女性 · 白衬衫", + url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Sofia.zip", + avatarResId = R.drawable.avatar_sofia + ), + AvatarModel( + name = "Lily", + description = "年轻女性 · 亚洲面孔", + url = "https://github.com/duixcom/Duix.mobile/releases/download/v2.0.1/Lily.zip", + avatarResId = R.drawable.avatar_lily + ) + ) + + /** + * 更多可用模型 (v1.0.0) + */ + val MORE_MODELS = arrayListOf( + AvatarModel( + name = "bendi3", + description = "通用形象", + url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/bendi3_20240518.zip" + ), + AvatarModel( + name = "airuike", + description = "通用形象", + url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/airuike_20240409.zip" + ), + AvatarModel( + name = "ddzh", + description = "数字人形象", + url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/ddzh.zip" + ), + AvatarModel( + name = "shuziren_v2", + description = "数字人 v2", + url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/shuziren_v2.zip" + ), + AvatarModel( + name = "St_yxm", + description = "加密优化版", + url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/St_yxm_encrypted_optim.zip" + ), + AvatarModel( + name = "Zyynv", + description = "1080p 高清", + url = "https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/Zyynv_0515_1080p.zip" + ) + ) + } +} diff --git a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt index 15b5ef7b..a63dd166 100644 --- a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt +++ b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/activity/MainActivity.kt @@ -4,6 +4,7 @@ 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.dialog.LoadingDialog import ai.guiji.duix.test.ui.dialog.ModelSelectorDialog import android.annotation.SuppressLint @@ -17,21 +18,9 @@ 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 var mBaseConfigUrl = "" private var mModelUrl = "" @@ -43,13 +32,23 @@ class MainActivity : BaseActivity() { binding.tvSdkVersion.text = "SDK Version: ${BuildConfig.VERSION_NAME}" + // 默认选中第一个官方形象 + val defaultAvatar = AvatarModel.OFFICIAL_AVATARS[0] + binding.etUrl.setText(defaultAvatar.url) + binding.tvSelectedAvatar.text = "当前形象: ${defaultAvatar.name} (${defaultAvatar.description})" binding.btnMoreModel.setOnClickListener { - val modelSelectorDialog = ModelSelectorDialog(mContext, models, object : ModelSelectorDialog.Listener{ - override fun onSelect(url: String) { - binding.etUrl.setText(url) + val modelSelectorDialog = ModelSelectorDialog( + mContext, + AvatarModel.OFFICIAL_AVATARS, + AvatarModel.MORE_MODELS, + object : ModelSelectorDialog.Listener { + override fun onSelect(avatar: AvatarModel) { + binding.etUrl.setText(avatar.url) + binding.tvSelectedAvatar.text = "当前形象: ${avatar.name} (${avatar.description})" + } } - }) + ) modelSelectorDialog.show() } binding.btnPlay.setOnClickListener { @@ -57,37 +56,37 @@ class MainActivity : BaseActivity() { } } - private fun play(){ + private fun play() { mBaseConfigUrl = binding.etBaseConfig.text.toString() mModelUrl = binding.etUrl.text.toString() - if (TextUtils.isEmpty(mBaseConfigUrl)){ + if (TextUtils.isEmpty(mBaseConfigUrl)) { Toast.makeText(mContext, R.string.base_config_cannot_be_empty, Toast.LENGTH_SHORT).show() return } - if (TextUtils.isEmpty(mModelUrl)){ + if (TextUtils.isEmpty(mModelUrl)) { Toast.makeText(mContext, R.string.model_url_cannot_be_empty, Toast.LENGTH_SHORT).show() return } 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() { + if (VirtualModelUtil.checkModel(mContext, mModelUrl)) { jumpPlayPage() } else { modelDownload() } } - private fun jumpPlayPage(){ + private fun jumpPlayPage() { val intent = Intent(mContext, CallActivity::class.java) intent.putExtra("modelUrl", mModelUrl) val debug = binding.switchDebug.isChecked @@ -95,7 +94,7 @@ class MainActivity : BaseActivity() { startActivity(intent) } - private fun baseConfigDownload(){ + private fun baseConfigDownload() { mLoadingDialog?.dismiss() mLoadingDialog = LoadingDialog(mContext, "Start downloading") mLoadingDialog?.show() @@ -103,10 +102,10 @@ class MainActivity : BaseActivity() { 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){ + if (mLoadingDialog?.isShowing == true) { mLoadingDialog?.setContent("Config download(${progress}%)") } } @@ -115,10 +114,10 @@ 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){ + if (mLoadingDialog?.isShowing == true) { mLoadingDialog?.setContent("Config unzip(${progress}%)") } } @@ -142,21 +141,21 @@ class MainActivity : BaseActivity() { }) } - private fun modelDownload(){ + private fun modelDownload() { mLoadingDialog?.dismiss() mLoadingDialog = LoadingDialog(mContext, "Start downloading") mLoadingDialog?.show() - VirtualModelUtil.modelDownload(mContext, mModelUrl, object : VirtualModelUtil.ModelDownloadCallback{ + VirtualModelUtil.modelDownload(mContext, mModelUrl, 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){ + if (mLoadingDialog?.isShowing == true) { mLoadingDialog?.setContent("Model download(${progress}%)") } } @@ -169,10 +168,10 @@ class MainActivity : BaseActivity() { total: Long, ) { val progress = (current * 100 / total).toInt() - if (progress != mLastProgress){ + if (progress != mLastProgress) { mLastProgress = progress runOnUiThread { - if (mLoadingDialog?.isShowing == true){ + if (mLoadingDialog?.isShowing == true) { mLoadingDialog?.setContent("Model unzip(${progress}%)") } } diff --git a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/adapter/ModelSelectorAdapter.kt b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/adapter/ModelSelectorAdapter.kt index 288d7d31..cf3776be 100644 --- a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/adapter/ModelSelectorAdapter.kt +++ b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/adapter/ModelSelectorAdapter.kt @@ -1,17 +1,21 @@ package ai.guiji.duix.test.ui.adapter import ai.guiji.duix.test.databinding.ItemModelSelectorBinding +import ai.guiji.duix.test.model.AvatarModel import android.annotation.SuppressLint import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView class ModelSelectorAdapter( - private val mList: ArrayList, + private val mList: ArrayList, private val callback: Callback ) : RecyclerView.Adapter() { + private var selectedPosition = -1 + class ItemHolder(val itemBinding: ItemModelSelectorBinding) : RecyclerView.ViewHolder(itemBinding.root) @@ -23,9 +27,28 @@ class ModelSelectorAdapter( @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: ItemHolder, position: Int) { - holder.itemBinding.tvModelUrl.text = mList[position] - holder.itemBinding.tvModelUrl.setOnClickListener { - callback.onClick(mList[position]) + val avatar = mList[position] + holder.itemBinding.tvModelName.text = avatar.name + holder.itemBinding.tvModelDesc.text = avatar.description + + // 显示头像(如果有) + if (avatar.avatarResId != 0) { + holder.itemBinding.ivAvatar.visibility = View.VISIBLE + holder.itemBinding.ivAvatar.setImageResource(avatar.avatarResId) + } else { + holder.itemBinding.ivAvatar.visibility = View.GONE + } + + // 高亮选中项 + val isSelected = position == selectedPosition + holder.itemBinding.root.alpha = if (isSelected) 1.0f else 0.85f + + holder.itemBinding.root.setOnClickListener { + val oldPos = selectedPosition + selectedPosition = holder.adapterPosition + if (oldPos >= 0) notifyItemChanged(oldPos) + notifyItemChanged(selectedPosition) + callback.onClick(avatar) } } @@ -34,6 +57,6 @@ class ModelSelectorAdapter( } interface Callback { - fun onClick(url: String) + fun onClick(avatar: AvatarModel) } -} \ No newline at end of file +} diff --git a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/dialog/ModelSelectorDialog.kt b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/dialog/ModelSelectorDialog.kt index 42a306b2..243eefb1 100644 --- a/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/dialog/ModelSelectorDialog.kt +++ b/duix-android/dh_aigc_android/test/src/main/java/ai/guiji/duix/test/ui/dialog/ModelSelectorDialog.kt @@ -2,18 +2,24 @@ package ai.guiji.duix.test.ui.dialog import ai.guiji.duix.test.R import ai.guiji.duix.test.databinding.DialogModelSelectorBinding +import ai.guiji.duix.test.model.AvatarModel import ai.guiji.duix.test.ui.adapter.ModelSelectorAdapter import android.app.Dialog import android.content.Context import android.os.Bundle +import android.view.ViewGroup import android.view.Window +import android.view.WindowManager -class ModelSelectorDialog(mContext: Context, val models: ArrayList, private val listener: Listener) : - Dialog(mContext, R.style.dialog_center) { +class ModelSelectorDialog( + mContext: Context, + private val officialModels: ArrayList, + private val moreModels: ArrayList, + private val listener: Listener +) : Dialog(mContext, R.style.dialog_center) { private lateinit var binding: DialogModelSelectorBinding - private var mAdapter: ModelSelectorAdapter?=null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,19 +27,35 @@ class ModelSelectorDialog(mContext: Context, val models: ArrayList, priv binding = DialogModelSelectorBinding.inflate(layoutInflater) super.setContentView(binding.root) - mAdapter = ModelSelectorAdapter(models, object : ModelSelectorAdapter.Callback{ - override fun onClick(url: String) { + // 设置 Dialog 宽度为屏幕 90% + window?.setLayout( + (context.resources.displayMetrics.widthPixels * 0.9).toInt(), + WindowManager.LayoutParams.WRAP_CONTENT + ) + + // 推荐形象 - 网格布局 + val officialAdapter = ModelSelectorAdapter(officialModels, object : ModelSelectorAdapter.Callback { + override fun onClick(avatar: AvatarModel) { + dismiss() + listener.onSelect(avatar) + } + }) + binding.rvOfficialModels.adapter = officialAdapter + + // 更多模型 - 列表布局 + val moreAdapter = ModelSelectorAdapter(moreModels, object : ModelSelectorAdapter.Callback { + override fun onClick(avatar: AvatarModel) { dismiss() - listener.onSelect(url) + listener.onSelect(avatar) } }) - binding.rvModels.adapter = mAdapter + binding.rvLegacyModels.adapter = moreAdapter setCancelable(true) setCanceledOnTouchOutside(true) } interface Listener { - fun onSelect(url: String) + fun onSelect(avatar: AvatarModel) } -} \ No newline at end of file +} diff --git a/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_leo.jpg b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_leo.jpg new file mode 100644 index 00000000..9b354596 Binary files /dev/null and b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_leo.jpg differ diff --git a/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_lily.jpg b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_lily.jpg new file mode 100644 index 00000000..abccef1a Binary files /dev/null and b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_lily.jpg differ diff --git a/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_oliver.jpg b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_oliver.jpg new file mode 100644 index 00000000..e5f7ea93 Binary files /dev/null and b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_oliver.jpg differ diff --git a/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_sofia.jpg b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_sofia.jpg new file mode 100644 index 00000000..00689a03 Binary files /dev/null and b/duix-android/dh_aigc_android/test/src/main/res/drawable-nodpi/avatar_sofia.jpg differ diff --git a/duix-android/dh_aigc_android/test/src/main/res/layout/activity_main.xml b/duix-android/dh_aigc_android/test/src/main/res/layout/activity_main.xml index 99044d79..1ad885a8 100644 --- a/duix-android/dh_aigc_android/test/src/main/res/layout/activity_main.xml +++ b/duix-android/dh_aigc_android/test/src/main/res/layout/activity_main.xml @@ -8,7 +8,7 @@ + + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tvAvatarLabel" />