临时群

This commit is contained in:
844143714@qq,com 2026-05-13 17:11:57 +08:00
parent e33e64df14
commit d4801dd02c
16 changed files with 702 additions and 1 deletions

View File

@ -53,6 +53,14 @@
android:label="开发者模式" android:label="开发者模式"
android:theme="@style/Theme.StandAPP" /> android:theme="@style/Theme.StandAPP" />
<activity
android:name=".TempCallActivity"
android:exported="false"
android:label="临时呼叫"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTop" />
<activity <activity
android:name=".printer.PrintTestActivity" android:name=".printer.PrintTestActivity"
android:exported="false" android:exported="false"

View File

@ -181,6 +181,24 @@
</div> </div>
</div> </div>
<!-- 临时呼叫状态区域 -->
<div id="temp-call-status" style="display: none; margin-top: 10px; padding: 10px; background: #fef3c7; border-radius: 8px; border: 1px solid #f59e0b;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">📞</span>
<div>
<div style="font-weight: bold; color: #92400e; font-size: 14px;">临时呼叫进行中</div>
<div id="temp-call-group-name" style="font-size: 12px; color: #b45309;">临时群组</div>
</div>
</div>
<button id="btn-maximize-temp-call" class="btn"
style="margin: 0; padding: 8px 16px; font-size: 12px; background: #f59e0b; min-width: auto;"
onclick="maximizeTempCall()">
最大化
</button>
</div>
</div>
<div style="margin-top: 8px; font-size: 13px;"> <div style="margin-top: 8px; font-size: 13px;">
当前群组: <span id="ptt-current-group" style="color: #6366f1; font-weight: bold;">-</span> 当前群组: <span id="ptt-current-group" style="color: #6366f1; font-weight: bold;">-</span>
<span id="ptt-current-group-id" style="color: #94a3b8; font-size: 11px; margin-left: 5px;"></span> <span id="ptt-current-group-id" style="color: #94a3b8; font-size: 11px; margin-left: 5px;"></span>
@ -625,8 +643,75 @@
} }
} catch(e) {} } catch(e) {}
} }
// 同步临时呼叫状态
syncTempCallStatus();
}, 500); }, 500);
// === 临时呼叫状态管理 ===
function syncTempCallStatus() {
if (window.android && window.android.getTempCallStatus) {
try {
var status = JSON.parse(window.android.getTempCallStatus());
updateTempCallUI(status.isOpen, status.isMinimized);
} catch(e) {}
}
}
function updateTempCallUI(isOpen, isMinimized) {
var statusDiv = document.getElementById('temp-call-status');
var maxBtn = document.getElementById('btn-maximize-temp-call');
var groupSpan = document.getElementById('temp-call-group-name');
// 只有在最小化状态时才显示最大化按钮
if (isMinimized) {
statusDiv.style.display = 'block';
maxBtn.style.display = 'inline-block';
maxBtn.disabled = false;
maxBtn.style.background = '#f59e0b';
maxBtn.style.cursor = 'pointer';
} else if (isOpen) {
// Activity 打开但未最小化,不显示状态区域
statusDiv.style.display = 'none';
} else {
// Activity 已销毁且未最小化
statusDiv.style.display = 'none';
}
}
function maximizeTempCall() {
if (window.android && window.android.maximizeTempCall) {
window.android.maximizeTempCall();
}
}
// 临时呼叫状态变化回调 (由 Android 原生调用)
window.onTempCallStateChanged = function(state) {
console.log("临时呼叫状态变化: " + state);
var statusDiv = document.getElementById('temp-call-status');
var maxBtn = document.getElementById('btn-maximize-temp-call');
switch(state) {
case 'minimized':
// 最小化:显示状态区域和最大化按钮
statusDiv.style.display = 'block';
maxBtn.style.display = 'inline-block';
maxBtn.disabled = false;
maxBtn.style.background = '#f59e0b';
showPttResult("[回调] 临时呼叫已最小化,可通过最大化按钮恢复");
break;
case 'resumed':
// 恢复:隐藏状态区域
statusDiv.style.display = 'none';
showPttResult("[回调] 临时呼叫页面已恢复");
break;
case 'destroyed':
// 销毁:隐藏所有
statusDiv.style.display = 'none';
showPttResult("[回调] 临时呼叫已结束");
break;
}
};
// 告知 Android 页面加载完成(已移至 checkApp 中,等 bridge 注入后再调用) // 告知 Android 页面加载完成(已移至 checkApp 中,等 bridge 注入后再调用)
window.onload = function() {}; window.onload = function() {};

View File

@ -238,8 +238,61 @@ public class CallBackResolution {
for (int i = 0; i < stringsTalk.size(); i++) { for (int i = 0; i < stringsTalk.size(); i++) {
tempTxt += unicode(stringsTalk.get(i)); tempTxt += unicode(stringsTalk.get(i));
} }
Log.i(TAG, " 临时讲话 信息通知的指令 " + tempTxt); int code=-1;
if(tempTxt.equals("退出临时呼叫")){
code=0;
} else if (tempTxt.startsWith("临时呼叫临时群组")) {
code=1;
} else if (tempTxt.equals("呼出失败")||tempTxt.equals("呼叫失败")) {
code=2;
} else if (tempTxt.equals("呼叫成功")||tempTxt.equals("呼出成功")) {
code=3;
}else{
code=-1;
}
Log.i(TAG, " 临时讲话 信息通知的指令 " + tempTxt + " code=" + code);
CallBackUtil.callBackTempCallNotify(tempTxt); CallBackUtil.callBackTempCallNotify(tempTxt);
// 处理临时呼叫页面逻辑
if (code == 1) {
// 临时呼叫临时群组 打开临时呼叫页面
try {
android.content.Intent intent = new android.content.Intent(
com.stand.standapp.MyApplication.getAppInstance(),
com.stand.standapp.TempCallActivity.class
);
// 从84消息中提取临时群名称格式为"临时呼叫XXX"
String tempGroupName = "临时会话";
if (tempTxt.startsWith("临时呼叫") && tempTxt.length() > "临时呼叫".length()) {
tempGroupName = tempTxt.substring("临时呼叫".length());
}
intent.putExtra("group_name", tempGroupName);
intent.addFlags(
android.content.Intent.FLAG_ACTIVITY_NEW_TASK |
android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
);
com.stand.standapp.MyApplication.getAppInstance().startActivity(intent);
} catch (Exception e) {
Log.e(TAG, "Failed to open TempCallActivity: " + e.getMessage());
}
} else if (code == 0) {
// 退出临时呼叫 关闭临时呼叫页面
if (com.stand.standapp.TempCallActivity.isOpen()) {
com.stand.standapp.TempCallActivity.closeWithMessage("临时呼叫已结束");
} else if (com.stand.standapp.TempCallActivity.isMinimizedState()) {
// 最小化状态下收到结束消息清除状态并通知前端
com.stand.standapp.TempCallActivity.clearMinimizedState();
}
} else if (code == 2) {
// 呼叫失败 关闭临时呼叫页面
if (com.stand.standapp.TempCallActivity.isOpen()) {
com.stand.standapp.TempCallActivity.closeWithMessage("呼叫失败");
} else if (com.stand.standapp.TempCallActivity.isMinimizedState()) {
// 最小化状态下收到失败消息清除状态并通知前端
com.stand.standapp.TempCallActivity.clearMinimizedState();
}
}
} }
break; break;

View File

@ -105,6 +105,11 @@ public class SendAtUtil {
send("0E000000000000\r\n"); send("0E000000000000\r\n");
} }
/** 结束多成员临时呼叫 */
public static void stopTempCall(){
send("5500000100000000\r\n");
}
/** /**
* 字符串转换成十六进制字符串 * 字符串转换成十六进制字符串

View File

@ -83,6 +83,8 @@ class AndroidInterface(private val activity: MainActivity) {
"onPageFinished" -> { onPageFinished(); "" } "onPageFinished" -> { onPageFinished(); "" }
"showRestartDialog" -> { showRestartDialog(args.optString(0, ""), args.optString(1, "提示")); "" } "showRestartDialog" -> { showRestartDialog(args.optString(0, ""), args.optString(1, "提示")); "" }
"showJsonDialog" -> { showJsonDialog(args.optString(0, "")); "" } "showJsonDialog" -> { showJsonDialog(args.optString(0, "")); "" }
"getTempCallStatus" -> getTempCallStatus()
"maximizeTempCall" -> { maximizeTempCall(); "" }
else -> { else -> {
Timber.tag("Bridge").w("Unknown method: $method") Timber.tag("Bridge").w("Unknown method: $method")
"" ""
@ -464,4 +466,25 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
} }
fun getTempCallStatus(): String {
return try {
val json = JSONObject()
json.put("isOpen", TempCallActivity.isOpen())
json.put("isMinimized", TempCallActivity.isMinimizedState())
json.toString()
} catch (e: Exception) {
"{\"isOpen\":false,\"isMinimized\":false}"
}
}
fun maximizeTempCall() {
Handler(Looper.getMainLooper()).post {
try {
TempCallActivity.maximize()
} catch (e: Exception) {
Timber.tag("TempCall").e(e, "maximizeTempCall error")
}
}
}
} }

View File

@ -0,0 +1,284 @@
package com.stand.standapp
import android.animation.ObjectAnimator
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.example.kingway.ptt.SendAtUtil
import timber.log.Timber
class TempCallActivity : AppCompatActivity() {
private var isSpeaking = false
private var lastSpeakTime = 0L
private val MIN_SPEAK_DURATION = 200L
private val handler = Handler(Looper.getMainLooper())
private var pulseAnimator: ObjectAnimator? = null
companion object {
@Volatile
var instance: TempCallActivity? = null
private set
@Volatile
var isMinimized = false
private set
// 存储当前临时群组名称,用于恢复时传递
@Volatile
var currentGroupName: String = "临时会话"
@JvmStatic
fun isOpen(): Boolean = instance != null
@JvmStatic
fun isMinimizedState(): Boolean = isMinimized
@JvmStatic
fun maximize() {
if (isMinimized) {
isMinimized = false
val intent = android.content.Intent(
com.stand.standapp.MyApplication.getAppInstance(),
TempCallActivity::class.java
)
intent.putExtra("group_name", currentGroupName)
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
com.stand.standapp.MyApplication.getAppInstance().startActivity(intent)
}
}
@JvmStatic
fun closeWithMessage(message: String) {
instance?.runOnUiThread {
Toast.makeText(instance, message, Toast.LENGTH_SHORT).show()
instance?.finish()
} ?: run {
// Activity 已销毁但状态还在,清除状态
clearMinimizedState()
}
}
@JvmStatic
fun clearMinimizedState() {
isMinimized = false
currentGroupName = "临时会话"
MainActivity.executeJs("onTempCallStateChanged('destroyed')")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 全屏覆盖 + 常亮 + 锁屏显示
window.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
)
setContentView(R.layout.activity_temp_call)
instance = this
isMinimized = false
val groupName = intent.getStringExtra("group_name") ?: "临时会话"
currentGroupName = groupName
findViewById<TextView>(R.id.tv_group_name).text = groupName
setupMinimizeButton()
setupPttButton()
setupHangupButton()
startPulseAnimation()
}
override fun onResume() {
super.onResume()
isMinimized = false
}
override fun onDestroy() {
super.onDestroy()
pulseAnimator?.cancel()
if (instance == this) {
instance = null
// 如果不是最小化状态,说明是真正结束,清除所有状态
if (!isMinimized) {
currentGroupName = "临时会话"
}
// 通知 web 页面状态变化
MainActivity.executeJs("onTempCallStateChanged('" + if (isMinimized) "minimized" else "destroyed" + "')")
}
}
// 拦截返回键
override fun onBackPressed() {
showMinimizeDialog()
}
// 不要在 recent tasks 中显示
override fun finish() {
super.finish()
overridePendingTransition(0, android.R.anim.fade_out)
}
fun updateGroupName(name: String) {
findViewById<TextView>(R.id.tv_group_name)?.text = name
}
private fun startPulseAnimation() {
val pulse = findViewById<ImageView>(R.id.iv_pulse) ?: return
pulseAnimator = ObjectAnimator.ofFloat(pulse, "alpha", 1f, 0.3f).apply {
duration = 1000
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.REVERSE
start()
}
}
private fun setupPttButton() {
val btnPtt = findViewById<Button>(R.id.btn_ptt)
val viewGlow = findViewById<View>(R.id.view_glow)
val startSpeak = {
if (!isSpeaking) {
isSpeaking = true
lastSpeakTime = System.currentTimeMillis()
SendAtUtil.speakPlay()
btnPtt.text = "正在\n说话..."
btnPtt.setBackgroundResource(R.drawable.circle_ptt_speaking)
viewGlow.setBackgroundResource(R.drawable.circle_ptt_glow_active)
findViewById<TextView>(R.id.tv_talk_info).text = "正在通话"
findViewById<TextView>(R.id.tv_talk_status).apply {
visibility = View.VISIBLE
text = "● 对讲中"
setTextColor(0xFFEF4444.toInt())
}
Timber.tag("TempCall").d("Speak Play")
}
}
val stopSpeak = {
if (isSpeaking) {
val now = System.currentTimeMillis()
val duration = now - lastSpeakTime
if (duration < MIN_SPEAK_DURATION) {
handler.postDelayed({ realRelease(btnPtt, viewGlow) }, MIN_SPEAK_DURATION - duration)
} else {
realRelease(btnPtt, viewGlow)
}
}
}
btnPtt.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startSpeak()
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
stopSpeak()
true
}
else -> false
}
}
}
private fun realRelease(btnPtt: Button, viewGlow: View) {
if (isSpeaking) {
isSpeaking = false
SendAtUtil.speakRelease()
btnPtt.text = "按住\n说话"
btnPtt.setBackgroundResource(R.drawable.circle_ptt_button)
viewGlow.setBackgroundResource(R.drawable.circle_ptt_glow)
findViewById<TextView>(R.id.tv_talk_info).text = "按下按钮讲话"
findViewById<TextView>(R.id.tv_talk_status).apply {
visibility = View.GONE
}
Timber.tag("TempCall").d("Speak Release")
}
}
private fun setupHangupButton() {
findViewById<Button>(R.id.btn_hangup).setOnClickListener {
try {
SendAtUtil.stopTempCall()
} catch (e: Exception) {
Timber.tag("TempCall").e(e, "stopTempCall error")
}
finish()
}
}
private fun setupMinimizeButton() {
findViewById<View>(R.id.ll_minimize).setOnClickListener {
minimizeAndFinish()
}
}
private fun showMinimizeDialog() {
androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("临时呼叫")
.setMessage("是否最小化临时呼叫页面?")
.setPositiveButton("最小化") { _, _ ->
minimizeAndFinish()
}
.setNegativeButton("取消", null)
.show()
}
private fun minimizeAndFinish() {
isMinimized = true
// 通知 web 页面临时呼叫页面已最小化
MainActivity.executeJs("onTempCallStateChanged('minimized')")
// finish 但保持 isMinimized = true
finish()
}
// 由 GpioManager 调用,同步 GPIO 按下状态
fun onGpioPressed() {
val btnPtt = findViewById<Button>(R.id.btn_ptt) ?: return
val viewGlow = findViewById<View>(R.id.view_glow) ?: return
if (!isSpeaking) {
isSpeaking = true
lastSpeakTime = System.currentTimeMillis()
SendAtUtil.speakPlay()
btnPtt.text = "正在\n说话..."
btnPtt.setBackgroundResource(R.drawable.circle_ptt_speaking)
viewGlow.setBackgroundResource(R.drawable.circle_ptt_glow_active)
findViewById<TextView>(R.id.tv_talk_info).text = "正在通话"
findViewById<TextView>(R.id.tv_talk_status).apply {
visibility = View.VISIBLE
text = "● 对讲中"
setTextColor(0xFFEF4444.toInt())
}
}
}
// 由 GpioManager 调用,同步 GPIO 松开状态
fun onGpioReleased() {
val btnPtt = findViewById<Button>(R.id.btn_ptt) ?: return
val viewGlow = findViewById<View>(R.id.view_glow) ?: return
if (isSpeaking) {
val now = System.currentTimeMillis()
val duration = now - lastSpeakTime
if (duration < MIN_SPEAK_DURATION) {
handler.postDelayed({ realRelease(btnPtt, viewGlow) }, MIN_SPEAK_DURATION - duration)
} else {
realRelease(btnPtt, viewGlow)
}
}
}
}

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.stand.standapp.MainActivity import com.stand.standapp.MainActivity
import com.stand.standapp.TempCallActivity
import timber.log.Timber import timber.log.Timber
/** /**
@ -75,6 +76,8 @@ object GpioManager {
Timber.tag("GPIO").d("PTT Speak Play (Hardware)") Timber.tag("GPIO").d("PTT Speak Play (Hardware)")
// 统一使用 updatePttTalkStatus 更新 UI // 统一使用 updatePttTalkStatus 更新 UI
MainActivity.executeJs("updatePttTalkStatus(true)") MainActivity.executeJs("updatePttTalkStatus(true)")
// 同步 TempCallActivity
TempCallActivity.instance?.onGpioPressed()
} }
} else { } else {
@ -104,6 +107,8 @@ object GpioManager {
Timber.tag("GPIO").d("PTT Speak Release (Hardware)") Timber.tag("GPIO").d("PTT Speak Release (Hardware)")
// 统一使用 updatePttTalkStatus 更新 UI // 统一使用 updatePttTalkStatus 更新 UI
MainActivity.executeJs("updatePttTalkStatus(false)") MainActivity.executeJs("updatePttTalkStatus(false)")
// 同步 TempCallActivity
TempCallActivity.instance?.onGpioReleased()
} }
} }
} }

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#66000000" />
<corners android:radius="16dp" />
<padding
android:left="8dp"
android:right="8dp"
android:top="4dp"
android:bottom="4dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#EF4444" />
<size android:width="64dp" android:height="64dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#66FFFFFF" />
<size
android:width="40dp"
android:height="40dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#3B82F6" />
<size android:width="150dp" android:height="150dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#223B82F6" />
<size android:width="180dp" android:height="180dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#333B82F6" />
<size android:width="180dp" android:height="180dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#2563EB" />
<size android:width="150dp" android:height="150dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#10B981" />
<size android:width="10dp" android:height="10dp" />
</shape>

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_login">
<!-- 最小化按钮 (右上角,明显的按钮样式) -->
<LinearLayout
android:id="@+id/ll_minimize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_marginTop="40dp"
android:layout_marginRight="16dp"
android:background="@drawable/btn_minimize_bg"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="#FFFFFF" />
<TextView
android:id="@+id/btn_minimize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:text="最小化"
android:textColor="#FFFFFF"
android:textSize="12sp" />
</LinearLayout>
<!-- 顶部状态栏区域 -->
<LinearLayout
android:id="@+id/ll_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="50dp"
android:paddingBottom="16dp">
<!-- 临时呼叫图标 -->
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@android:drawable/ic_menu_call"
android:tint="#FFFFFF" />
<!-- 状态文字 -->
<TextView
android:id="@+id/tv_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="临时呼叫中"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold"
android:letterSpacing="0.1" />
<!-- 脉冲动画指示器 -->
<ImageView
android:id="@+id/iv_pulse"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginTop="6dp"
android:src="@drawable/circle_pulse" />
</LinearLayout>
<!-- 中间信息卡片 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:background="@drawable/shape_login_box"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="临时群组"
android:textColor="#99FFFFFF"
android:textSize="12sp"
android:letterSpacing="0.15" />
<TextView
android:id="@+id/tv_group_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="临时群组"
android:textColor="#FFFFFF"
android:textSize="22sp"
android:textStyle="bold" />
<!-- 分隔线 -->
<View
android:layout_width="60dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="#33FFFFFF" />
<TextView
android:id="@+id/tv_talk_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按下按钮讲话"
android:textColor="#CCFFFFFF"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_talk_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=""
android:textColor="#10B981"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
<!-- 底部按钮区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="48dp">
<!-- PTT 大按钮 -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp">
<!-- 外圈光晕 -->
<View
android:id="@+id/view_glow"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center"
android:background="@drawable/circle_ptt_glow" />
<!-- PTT 按钮 -->
<Button
android:id="@+id/btn_ptt"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center"
android:background="@drawable/circle_ptt_button"
android:text="按住\n说话"
android:textColor="#FFFFFF"
android:textSize="22sp"
android:textStyle="bold" />
</FrameLayout>
<!-- 挂断按钮 -->
<Button
android:id="@+id/btn_hangup"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/circle_hangup_button"
android:text="挂断"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
</RelativeLayout>