Compare commits

...

2 Commits

Author SHA1 Message Date
2015286595 加入频谱图 2026-03-20 19:37:10 +08:00
a44ddb6dff 优化UI,加入模型/音色选择 2026-03-19 07:47:20 +08:00
27 changed files with 538 additions and 223 deletions

View File

@ -28,7 +28,8 @@
<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

@ -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 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(Callback callback) { public OmniRealtimeClient(String voice, 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)
@ -98,7 +100,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", "Ethan"); session.put("voice", voice);
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,6 +13,7 @@ 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
@ -25,7 +26,6 @@ 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,6 +39,7 @@ 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 = ""
@ -82,6 +83,7 @@ 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)
@ -162,11 +164,17 @@ 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 -> {
@ -311,20 +319,24 @@ class CallActivity : BaseActivity() {
applyMessage("AI对话: 连接中...") applyMessage("AI对话: 连接中...")
binding.tvSubtitle.visibility = View.VISIBLE binding.tvSubtitle.visibility = View.VISIBLE
binding.tvSubtitle.text = "正在连接..." binding.tvSubtitle.text = "正在连接..."
binding.btnAIConversation.text = getString(R.string.ai_fab_stop) binding.audioSpectrum.visibility = View.VISIBLE
binding.audioSpectrum.clear()
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() { 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.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) 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 {
@ -425,10 +437,13 @@ class CallActivity : BaseActivity() {
mainHandler.post { mainHandler.post {
if (!isFinishing) { 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.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对话: 已结束")
} }
} }
@ -451,11 +466,11 @@ class CallActivity : BaseActivity() {
} }
}, binding.cameraPreview) }, binding.cameraPreview)
cameraFrameCapture?.start() 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) binding.btnCameraToggle.contentDescription = getString(R.string.camera_on)
} else { } else {
stopCamera() 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) binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
} }
} }

View File

@ -1,16 +1,15 @@
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
@ -20,54 +19,36 @@ class MainActivity : BaseActivity() {
private var mLoadingDialog: LoadingDialog? = null private var mLoadingDialog: LoadingDialog? = null
private var mLastProgress = 0 private var mLastProgress = 0
val models = arrayListOf( private val baseConfigUrl = "https://dl.xn--876a.net/models/gj_dh_res.zip"
"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", private val avatarModels = listOf(
"https://github.com/duixcom/Duix-Mobile/releases/download/v1.0.0/675429759852613_7f8d9388a4213080b1820b83dd057cfb_optim_m80.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/674402003804229_f6e86fb375c4f1f1b82b24f7ee4e7cb4_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/674400178376773_3925e756433c5a9caa9b9d54147ae4ab_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/674397294927941_6e297e18a4bdbe35c07a6ae48a1f021f_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/674393494597701_f49fcf68f5afdb241d516db8a7d88a7b_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/651705983152197_ccf3256b2449c76e77f94276dffcb293_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/627306542239813_1871244b5e6912efc636ba31ea4c5c6d_optim_m80.zip", AvatarModel("索菲亚", "Sonrisa", "https://dl.xn--876a.net/models/Sofia.zip", R.mipmap.sofia),
) )
private var mBaseConfigUrl = "" private var mSelectedModel: AvatarModel? = null
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.tvSdkVersion.text = "SDK Version: ${BuildConfig.VERSION_NAME}" binding.rvAvatars.layoutManager = GridLayoutManager(this, 2)
binding.rvAvatars.adapter = AvatarAdapter(avatarModels, object : AvatarAdapter.Callback {
override fun onSelect(model: AvatarModel) {
binding.btnMoreModel.setOnClickListener { mSelectedModel = model
val modelSelectorDialog = ModelSelectorDialog(mContext, models, object : ModelSelectorDialog.Listener{ play(model)
override fun onSelect(url: String) {
binding.etUrl.setText(url)
} }
}) })
modelSelectorDialog.show()
}
binding.btnPlay.setOnClickListener {
play()
}
} }
private fun play(){ private fun play(model: AvatarModel) {
mBaseConfigUrl = binding.etBaseConfig.text.toString() mSelectedModel = model
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()
} }
@ -80,7 +61,8 @@ class MainActivity : BaseActivity() {
} }
private fun checkModel() { private fun checkModel() {
if (VirtualModelUtil.checkModel(mContext, mModelUrl)){ val model = mSelectedModel ?: return
if (VirtualModelUtil.checkModel(mContext, model.modelUrl)) {
jumpPlayPage() jumpPlayPage()
} else { } else {
modelDownload() modelDownload()
@ -88,18 +70,19 @@ class MainActivity : BaseActivity() {
} }
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", mModelUrl) intent.putExtra("modelUrl", model.modelUrl)
val debug = binding.switchDebug.isChecked intent.putExtra("voice", model.voice)
intent.putExtra("debug", debug) intent.putExtra("debug", false)
startActivity(intent) startActivity(intent)
} }
private fun baseConfigDownload() { private fun baseConfigDownload() {
mLoadingDialog?.dismiss() mLoadingDialog?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading") mLoadingDialog = LoadingDialog(mContext, "正在准备资源...")
mLoadingDialog?.show() mLoadingDialog?.show()
VirtualModelUtil.baseConfigDownload(mContext, mBaseConfigUrl, object : VirtualModelUtil.baseConfigDownload(mContext, baseConfigUrl, 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()
@ -107,7 +90,7 @@ class MainActivity : BaseActivity() {
mLastProgress = progress mLastProgress = progress
runOnUiThread { runOnUiThread {
if (mLoadingDialog?.isShowing == true) { if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Config download(${progress}%)") mLoadingDialog?.setContent("基础配置下载中(${progress}%)")
} }
} }
} }
@ -119,7 +102,7 @@ class MainActivity : BaseActivity() {
mLastProgress = progress mLastProgress = progress
runOnUiThread { runOnUiThread {
if (mLoadingDialog?.isShowing == true) { if (mLoadingDialog?.isShowing == true) {
mLoadingDialog?.setContent("Config unzip(${progress}%)") mLoadingDialog?.setContent("基础配置解压中(${progress}%)")
} }
} }
} }
@ -135,45 +118,37 @@ 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, "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?.dismiss()
mLoadingDialog = LoadingDialog(mContext, "Start downloading") mLoadingDialog = LoadingDialog(mContext, "正在下载 ${model.name} ...")
mLoadingDialog?.show() mLoadingDialog?.show()
VirtualModelUtil.modelDownload(mContext, mModelUrl, object : VirtualModelUtil.ModelDownloadCallback{ VirtualModelUtil.modelDownload(mContext, model.modelUrl, object : VirtualModelUtil.ModelDownloadCallback {
override fun onDownloadProgress( override fun onDownloadProgress(url: String?, current: Long, total: Long) {
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 download(${progress}%)") mLoadingDialog?.setContent("${model.name} 下载中(${progress}%)")
} }
} }
} }
} }
override fun onUnzipProgress( override fun onUnzipProgress(url: String?, current: Long, total: Long) {
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 unzip(${progress}%)") mLoadingDialog?.setContent("${model.name} 解压中(${progress}%)")
} }
} }
} }
@ -186,18 +161,12 @@ class MainActivity : BaseActivity() {
} }
} }
override fun onDownloadFail( override fun onDownloadFail(url: String?, code: Int, msg: String?) {
url: String?,
code: Int,
msg: String?,
) {
runOnUiThread { runOnUiThread {
mLoadingDialog?.dismiss() 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,215 @@
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

@ -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" <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
@ -84,44 +83,53 @@
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/btnAIConversation" app:layout_constraintBottom_toTopOf="@+id/audioSpectrum"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginHorizontal="24dp" 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_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginHorizontal="32dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
/> />
<Button <ImageButton
android:id="@+id/btnAIConversation" android:id="@+id/btnAIConversation"
android:text="@string/ai_fab_start" android:src="@drawable/ic_mic"
android:textSize="14sp" android:background="@drawable/shape_fab_ai"
android:textColor="@color/white"
android:background="@drawable/shape_circle_fab"
android:elevation="8dp" android:elevation="8dp"
android:layout_width="56dp" android:scaleType="center"
android:layout_height="56dp" android:layout_width="64dp"
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="32dp" android:layout_marginBottom="40dp"
android:contentDescription="@string/ai_conversation" android:contentDescription="@string/ai_conversation"
/> />
<Button <ImageButton
android:id="@+id/btnCameraToggle" android:id="@+id/btnCameraToggle"
android:text="📷" android:src="@drawable/ic_camera"
android:textSize="18sp" android:background="@drawable/shape_fab_camera_off"
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,119 +3,56 @@
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:padding="12dp"> android:fitsSystemWindows="true"
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="120dp" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:text="@string/app_name" android:paddingTop="48dp"
android:textColor="@color/black" android:paddingBottom="4dp"
android:textSize="18dp" android:text="@string/app_title"
android:textStyle="italic|bold" android:textColor="@color/warm_text_primary"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/tv_sdk_version" android:id="@+id/tv_subtitle"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:gravity="center"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="16dp"
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"
app:layout_constraintBottom_toBottomOf="parent" android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@+id/tv_sdk_version"> android:paddingStart="10dp"
android:paddingEnd="10dp"
<androidx.constraintlayout.widget.ConstraintLayout android:paddingBottom="16dp"
android:layout_width="match_parent" app:layout_constraintBottom_toTopOf="@+id/tvBottomHint"
android:layout_height="wrap_content"> app:layout_constraintTop_toBottomOf="@+id/tv_subtitle" />
<TextView <TextView
android:id="@+id/tvDownloadTips" android:id="@+id/tvBottomHint"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/main_download_tips"
android:textSize="13sp"
android:textColor="@color/black"
android:textStyle="bold"
android:layout_width="match_parent" 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_height="wrap_content"
android:layout_marginTop="12dp" android:gravity="center"
android:text="@string/base_config_url" android:paddingTop="8dp"
android:textSize="13sp" android:paddingBottom="16dp"
app:layout_constraintTop_toBottomOf="@+id/tvDownloadTips" android:text="@string/choose_companion_hint"
app:layout_constraintStart_toStartOf="parent"/> android:textColor="@color/warm_text_hint"
android:textSize="12sp"
<EditText app:layout_constraintBottom_toBottomOf="parent" />
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

@ -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="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,5 +1,8 @@
<resources> <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="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,15 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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: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: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">#fff</item> <item name="colorPrimary">@color/warm_bg</item>
<item name="colorPrimaryDark">#4349a9</item> <item name="colorPrimaryDark">@color/warm_accent</item>
<item name="colorAccent">#4349a9</item> <item name="colorAccent">@color/warm_accent</item>
<item name="android:windowTranslucentStatus">true</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> </style>
</resources> </resources>