Compare commits

..

No commits in common. "2015286595b2be58ff5a4c89bdd8a6a2d00beebc" and "d09d6f1cc0cd2b144148a189b92383dd5e920896" have entirely different histories.

27 changed files with 223 additions and 538 deletions

View File

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

View File

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

View File

@ -13,7 +13,6 @@ import ai.guiji.duix.test.databinding.ActivityCallBinding
import ai.guiji.duix.test.realtime.OmniRealtimeClient import ai.guiji.duix.test.realtime.OmniRealtimeClient
import ai.guiji.duix.test.ui.adapter.MotionAdapter import ai.guiji.duix.test.ui.adapter.MotionAdapter
import ai.guiji.duix.test.ui.dialog.AudioRecordDialog import ai.guiji.duix.test.ui.dialog.AudioRecordDialog
import ai.guiji.duix.test.ui.widget.AudioSpectrumView
import ai.guiji.duix.test.util.StringUtils import ai.guiji.duix.test.util.StringUtils
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -26,6 +25,7 @@ import android.util.Log
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -39,7 +39,6 @@ class CallActivity : BaseActivity() {
} }
private var modelUrl = "" private var modelUrl = ""
private var voice = "Ethan"
private var debug = false private var debug = false
private var mMessage = "" private var mMessage = ""
@ -83,7 +82,6 @@ class CallActivity : BaseActivity() {
setContentView(binding.root) setContentView(binding.root)
modelUrl = intent.getStringExtra("modelUrl") ?: "" modelUrl = intent.getStringExtra("modelUrl") ?: ""
voice = intent.getStringExtra("voice") ?: "Ethan"
debug = intent.getBooleanExtra("debug", false) debug = intent.getBooleanExtra("debug", false)
Glide.with(mContext).load("file:///android_asset/bg/bg1.png").into(binding.ivBg) Glide.with(mContext).load("file:///android_asset/bg/bg1.png").into(binding.ivBg)
@ -164,17 +162,11 @@ class CallActivity : BaseActivity() {
Constant.CALLBACK_EVENT_AUDIO_PLAY_START -> { Constant.CALLBACK_EVENT_AUDIO_PLAY_START -> {
applyMessage("callback audio play start") applyMessage("callback audio play start")
Log.i(TAG, "CALLBACK_EVENT_AUDIO_PLAY_START") Log.i(TAG, "CALLBACK_EVENT_AUDIO_PLAY_START")
if (isAIConversationActive) {
runOnUiThread { binding.audioSpectrum.setAiSpeaking(true) }
}
} }
Constant.CALLBACK_EVENT_AUDIO_PLAY_END -> { Constant.CALLBACK_EVENT_AUDIO_PLAY_END -> {
applyMessage("callback audio play end") applyMessage("callback audio play end")
Log.i(TAG, "CALLBACK_EVENT_PLAY_END") Log.i(TAG, "CALLBACK_EVENT_PLAY_END")
if (isAIConversationActive) {
runOnUiThread { binding.audioSpectrum.setAiSpeaking(false) }
}
} }
Constant.CALLBACK_EVENT_AUDIO_PLAY_ERROR -> { Constant.CALLBACK_EVENT_AUDIO_PLAY_ERROR -> {
@ -319,24 +311,20 @@ class CallActivity : BaseActivity() {
applyMessage("AI对话: 连接中...") applyMessage("AI对话: 连接中...")
binding.tvSubtitle.visibility = View.VISIBLE binding.tvSubtitle.visibility = View.VISIBLE
binding.tvSubtitle.text = "正在连接..." binding.tvSubtitle.text = "正在连接..."
binding.audioSpectrum.visibility = View.VISIBLE binding.btnAIConversation.text = getString(R.string.ai_fab_stop)
binding.audioSpectrum.clear()
binding.btnAIConversation.setImageResource(R.drawable.ic_mic_off)
binding.btnAIConversation.setBackgroundResource(R.drawable.shape_fab_ai_active)
omniClient = OmniRealtimeClient(voice, object : OmniRealtimeClient.Callback { omniClient = OmniRealtimeClient(object : OmniRealtimeClient.Callback {
override fun onConnected() { override fun onConnected() {
mainHandler.post { mainHandler.post {
binding.tvSubtitle.text = "已连接,请对着麦克风说话" binding.tvSubtitle.text = "已连接,请对着麦克风说话"
binding.btnCameraToggle.visibility = View.VISIBLE binding.btnCameraToggle.visibility = View.VISIBLE
binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_off) binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_off)
binding.btnCameraToggle.contentDescription = getString(R.string.camera_off) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
isCameraEnabled = false isCameraEnabled = false
applyMessage("AI对话: 已连接") applyMessage("AI对话: 已连接")
streamingRecorder = StreamingAudioRecorder(mContext, object : StreamingAudioRecorder.StreamingCallback { streamingRecorder = StreamingAudioRecorder(mContext, object : StreamingAudioRecorder.StreamingCallback {
override fun onAudioData(pcmData: ByteArray) { override fun onAudioData(pcmData: ByteArray) {
omniClient?.appendAudio(pcmData) omniClient?.appendAudio(pcmData)
mainHandler.post { binding.audioSpectrum.updateAudio(pcmData) }
} }
override fun onError(message: String) { override fun onError(message: String) {
mainHandler.post { mainHandler.post {
@ -437,13 +425,10 @@ class CallActivity : BaseActivity() {
mainHandler.post { mainHandler.post {
if (!isFinishing) { if (!isFinishing) {
binding.btnAIConversation.setImageResource(R.drawable.ic_mic) binding.btnAIConversation.text = getString(R.string.ai_fab_start)
binding.btnAIConversation.setBackgroundResource(R.drawable.shape_fab_ai)
binding.btnCameraToggle.visibility = View.GONE binding.btnCameraToggle.visibility = View.GONE
binding.tvSubtitle.visibility = View.GONE binding.tvSubtitle.visibility = View.GONE
binding.tvSubtitle.text = "" binding.tvSubtitle.text = ""
binding.audioSpectrum.clear()
binding.audioSpectrum.visibility = View.GONE
applyMessage("AI对话: 已结束") applyMessage("AI对话: 已结束")
} }
} }
@ -466,11 +451,11 @@ class CallActivity : BaseActivity() {
} }
}, binding.cameraPreview) }, binding.cameraPreview)
cameraFrameCapture?.start() cameraFrameCapture?.start()
binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_on) binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_on)
binding.btnCameraToggle.contentDescription = getString(R.string.camera_on) binding.btnCameraToggle.contentDescription = getString(R.string.camera_on)
} else { } else {
stopCamera() stopCamera()
binding.btnCameraToggle.setBackgroundResource(R.drawable.shape_fab_camera_off) binding.btnCameraToggle.background = ContextCompat.getDrawable(mContext, R.drawable.shape_circle_camera_off)
binding.btnCameraToggle.contentDescription = getString(R.string.camera_off) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
} }
} }

View File

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

View File

@ -1,38 +0,0 @@
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

@ -1,215 +0,0 @@
package ai.guiji.duix.test.ui.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
/**
* 淡雅音频频谱可视化 View
* 支持两种模式
* 1. 麦克风模式 用真实 PCM 数据驱动频谱柱
* 2. AI 说话模式 柔和呼吸脉动动画与数字人口型同步 SDK 回调驱动
*/
public class AudioSpectrumView extends View {
private static final int BAR_COUNT = 32;
private static final float SMOOTHING = 0.35f;
private static final float DECAY = 0.92f;
private final Paint barPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint glowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final float[] magnitudes = new float[BAR_COUNT];
private final float[] displayMagnitudes = new float[BAR_COUNT];
private LinearGradient micGradient;
private LinearGradient aiGradient;
private int lastWidth = 0;
private int lastHeight = 0;
/** AI 说话脉动状态 */
private boolean aiSpeaking = false;
private long aiSpeakStartTime = 0;
public AudioSpectrumView(Context context) {
super(context);
init();
}
public AudioSpectrumView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AudioSpectrumView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
barPaint.setStyle(Paint.Style.FILL);
glowPaint.setStyle(Paint.Style.FILL);
glowPaint.setAlpha(40);
}
/**
* 接收 PCM 16bit mono 音频数据麦克风提取频谱能量
*/
public void updateAudio(byte[] pcmData) {
if (pcmData == null || pcmData.length < 4) return;
if (aiSpeaking) return; // AI 说话时忽略麦克风数据
int sampleCount = pcmData.length / 2;
int samplesPerBar = Math.max(1, sampleCount / BAR_COUNT);
for (int i = 0; i < BAR_COUNT; i++) {
long sum = 0;
int start = i * samplesPerBar;
int end = Math.min(start + samplesPerBar, sampleCount);
for (int j = start; j < end; j++) {
int idx = j * 2;
if (idx + 1 >= pcmData.length) break;
short sample = (short) ((pcmData[idx] & 0xFF) | (pcmData[idx + 1] << 8));
sum += Math.abs(sample);
}
float avg = (float) sum / Math.max(1, end - start);
float normalized = Math.min(1f, avg / 8000f);
float boost = 1f + (float) i / BAR_COUNT * 1.0f;
magnitudes[i] = Math.min(1f, normalized * boost);
}
postInvalidateOnAnimation();
}
/** 通知 AI 数字人开始说话(由 DUIX SDK AUDIO_PLAY_START 回调触发) */
public void setAiSpeaking(boolean speaking) {
this.aiSpeaking = speaking;
if (speaking) {
aiSpeakStartTime = System.currentTimeMillis();
// 清除麦克风残留数据
for (int i = 0; i < BAR_COUNT; i++) {
magnitudes[i] = 0f;
}
}
postInvalidateOnAnimation();
}
public boolean isAiSpeaking() {
return aiSpeaking;
}
/** 清除频谱 */
public void clear() {
aiSpeaking = false;
for (int i = 0; i < BAR_COUNT; i++) {
magnitudes[i] = 0f;
}
postInvalidateOnAnimation();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth();
int h = getHeight();
if (w == 0 || h == 0) return;
if (micGradient == null || lastWidth != w || lastHeight != h) {
lastWidth = w;
lastHeight = h;
// 麦克风暖橙渐变
micGradient = new LinearGradient(
0, h, 0, 0,
new int[]{0x00E8926F, 0x66E8926F, 0x99F0B89A},
new float[]{0f, 0.5f, 1f},
Shader.TileMode.CLAMP
);
// AI 说话柔和蓝紫渐变
aiGradient = new LinearGradient(
0, h, 0, 0,
new int[]{0x009BB5E8, 0x669BB5E8, 0x99C4B5F0},
new float[]{0f, 0.5f, 1f},
Shader.TileMode.CLAMP
);
}
// 选择渐变色
LinearGradient activeGradient = aiSpeaking ? aiGradient : micGradient;
barPaint.setShader(activeGradient);
glowPaint.setShader(activeGradient);
float totalBarWidth = w * 0.85f;
float sideMargin = (w - totalBarWidth) / 2f;
float gap = totalBarWidth * 0.02f;
float barWidth = (totalBarWidth - gap * (BAR_COUNT - 1)) / BAR_COUNT;
float cornerRadius = barWidth / 2f;
float maxBarHeight = h * 0.96f;
float minBarHeight = h * 0.05f;
if (aiSpeaking) {
drawAiPulse(canvas, w, h, sideMargin, gap, barWidth, cornerRadius, maxBarHeight, minBarHeight);
} else {
drawMicSpectrum(canvas, h, sideMargin, gap, barWidth, cornerRadius, maxBarHeight, minBarHeight);
}
}
private void drawMicSpectrum(Canvas canvas, int h, float sideMargin, float gap,
float barWidth, float cornerRadius, float maxBarHeight, float minBarHeight) {
boolean hasActivity = false;
for (int i = 0; i < BAR_COUNT; i++) {
float target = magnitudes[i];
if (target > displayMagnitudes[i]) {
displayMagnitudes[i] += (target - displayMagnitudes[i]) * (1f - SMOOTHING);
} else {
displayMagnitudes[i] *= DECAY;
}
float barH = minBarHeight + displayMagnitudes[i] * maxBarHeight;
float left = sideMargin + i * (barWidth + gap);
float top = h - barH;
float right = left + barWidth;
canvas.drawRoundRect(left - 1, top - 2, right + 1, h, cornerRadius, cornerRadius, glowPaint);
canvas.drawRoundRect(left, top, right, h, cornerRadius, cornerRadius, barPaint);
if (displayMagnitudes[i] > 0.001f) hasActivity = true;
}
if (hasActivity) {
postInvalidateOnAnimation();
}
}
private void drawAiPulse(Canvas canvas, int w, int h, float sideMargin, float gap,
float barWidth, float cornerRadius, float maxBarHeight, float minBarHeight) {
float elapsed = (System.currentTimeMillis() - aiSpeakStartTime) / 1000f;
float center = BAR_COUNT / 2f;
for (int i = 0; i < BAR_COUNT; i++) {
// 多重正弦波叠加模拟自然呼吸感
float dist = Math.abs(i - center) / center; // 0~1, 离中心的距离
float wave1 = (float) Math.sin(elapsed * 2.5 - dist * 3.0) * 0.5f + 0.5f;
float wave2 = (float) Math.sin(elapsed * 1.7 + i * 0.4) * 0.3f + 0.5f;
float wave3 = (float) Math.sin(elapsed * 4.0 - i * 0.2) * 0.15f + 0.5f;
// 中间高两边低的包络
float envelope = 1f - dist * 0.6f;
float target = (wave1 * 0.5f + wave2 * 0.3f + wave3 * 0.2f) * envelope;
target = Math.max(0.05f, Math.min(1f, target));
// 平滑过渡
displayMagnitudes[i] += (target - displayMagnitudes[i]) * 0.15f;
float barH = minBarHeight + displayMagnitudes[i] * maxBarHeight * 0.9f;
float left = sideMargin + i * (barWidth + gap);
float top = h - barH;
float right = left + barWidth;
canvas.drawRoundRect(left - 1, top - 2, right + 1, h, cornerRadius, cornerRadius, glowPaint);
canvas.drawRoundRect(left, top, right, h, cornerRadius, cornerRadius, barPaint);
}
postInvalidateOnAnimation();
}
}

View File

@ -1,10 +0,0 @@
<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

@ -1,10 +0,0 @@
<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

@ -1,14 +0,0 @@
<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

@ -1,5 +0,0 @@
<?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

@ -1,5 +0,0 @@
<?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

@ -1,5 +0,0 @@
<?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

@ -1,5 +0,0 @@
<?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,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ImageView <ImageView
@ -83,53 +84,44 @@
android:padding="12dp" android:padding="12dp"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/audioSpectrum"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="8dp"
/>
<ai.guiji.duix.test.ui.widget.AudioSpectrumView
android:id="@+id/audioSpectrum"
android:layout_width="0dp"
android:layout_height="48dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/btnAIConversation" app:layout_constraintBottom_toTopOf="@+id/btnAIConversation"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginHorizontal="32dp" android:layout_marginHorizontal="24dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
/> />
<ImageButton <Button
android:id="@+id/btnAIConversation" android:id="@+id/btnAIConversation"
android:src="@drawable/ic_mic" android:text="@string/ai_fab_start"
android:background="@drawable/shape_fab_ai" android:textSize="14sp"
android:textColor="@color/white"
android:background="@drawable/shape_circle_fab"
android:elevation="8dp" android:elevation="8dp"
android:scaleType="center" android:layout_width="56dp"
android:layout_width="64dp" android:layout_height="56dp"
android:layout_height="64dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="40dp" android:layout_marginBottom="32dp"
android:contentDescription="@string/ai_conversation" android:contentDescription="@string/ai_conversation"
/> />
<ImageButton <Button
android:id="@+id/btnCameraToggle" android:id="@+id/btnCameraToggle"
android:src="@drawable/ic_camera" android:text="📷"
android:background="@drawable/shape_fab_camera_off" android:textSize="18sp"
android:textColor="@color/white"
android:background="@drawable/shape_circle_camera_off"
android:elevation="8dp" android:elevation="8dp"
android:scaleType="center"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/btnAIConversation" app:layout_constraintBottom_toBottomOf="@+id/btnAIConversation"
app:layout_constraintTop_toTopOf="@+id/btnAIConversation"
app:layout_constraintStart_toEndOf="@+id/btnAIConversation" app:layout_constraintStart_toEndOf="@+id/btnAIConversation"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="24dp"
android:contentDescription="@string/camera_off" android:contentDescription="@string/camera_off"
/> />

View File

@ -3,56 +3,119 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:padding="12dp">
android:background="@color/warm_bg">
<!-- 顶部标题区域 -->
<TextView <TextView
android:id="@+id/tv_title" android:id="@+id/tv_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="120dp"
android:gravity="center" android:gravity="center"
android:paddingTop="48dp" android:text="@string/app_name"
android:paddingBottom="4dp" android:textColor="@color/black"
android:text="@string/app_title" android:textSize="18dp"
android:textColor="@color/warm_text_primary" android:textStyle="italic|bold"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/tv_subtitle" android:id="@+id/tv_sdk_version"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:textSize="12sp"
android:paddingBottom="16dp" app:layout_constraintEnd_toEndOf="parent"
android:text="@string/app_subtitle"
android:textColor="@color/warm_text_secondary"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@+id/tv_title" /> app:layout_constraintTop_toBottomOf="@+id/tv_title" />
<!-- 角色列表 --> <ScrollView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAvatars"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false" app:layout_constraintBottom_toBottomOf="parent"
android:paddingStart="10dp" app:layout_constraintTop_toBottomOf="@+id/tv_sdk_version">
android:paddingEnd="10dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/tvBottomHint"
app:layout_constraintTop_toBottomOf="@+id/tv_subtitle" />
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/tvBottomHint" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:gravity="center" <TextView
android:paddingTop="8dp" android:id="@+id/tvDownloadTips"
android:paddingBottom="16dp" app:layout_constraintTop_toTopOf="parent"
android:text="@string/choose_companion_hint" android:text="@string/main_download_tips"
android:textColor="@color/warm_text_hint" android:textSize="13sp"
android:textSize="12sp" android:textColor="@color/black"
app:layout_constraintBottom_toBottomOf="parent" /> android:textStyle="bold"
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>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,42 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -9,11 +9,4 @@
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="primary">#5a7bbe</color> <color name="primary">#5a7bbe</color>
<color name="primary_99">#995a7bbe</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> </resources>

View File

@ -1,8 +1,5 @@
<resources> <resources>
<string name="app_name">AI 情感陪护</string> <string name="app_name">DUIX Demo</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="play">Play</string>
<string name="base_config_url">BaseConfig Url:</string> <string name="base_config_url">BaseConfig Url:</string>
<string name="model_url">Model Url:</string> <string name="model_url">Model Url:</string>

View File

@ -1,24 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.DUIX.Test" parent="Theme.MaterialComponents.Light.NoActionBar"> <style name="Theme.DUIX.Test" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<item name="android:windowBackground">@color/warm_bg</item> <item name="android:windowBackground">@android:color/white</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowIsTranslucent">false</item> <item name="android:windowIsTranslucent">false</item>
<item name="android:windowIsFloating">false</item> <item name="android:windowIsFloating">false</item>
<item name="colorPrimary">@color/warm_bg</item> <item name="colorPrimary">#fff</item>
<item name="colorPrimaryDark">@color/warm_accent</item> <item name="colorPrimaryDark">#4349a9</item>
<item name="colorAccent">@color/warm_accent</item> <item name="colorAccent">#4349a9</item>
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:windowTranslucentStatus">true</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> </style>
</resources> </resources>