From 5e955168d25294a8f27a376cb1a0b3248cc7143c Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 19 Sep 2026 21:17:38 +0300 Subject: [PATCH] =?UTF-8?q?chatgui-android:=20PTT-=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=E2=80=94=20talking=20flow,=20=D1=86=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=20idle/talking,=20drag-lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RadioAudioEngine: StateFlow talking для реактивной подсветки кнопки - RadioOverlayService: градиент idle/talking, drag-lock, сброс isShown при destroy - ChatScreen: скрывать PTT-кнопку при активном оверлее, подсветка по talking --- .../com/utun/chat/data/RadioAudioEngine.kt | 8 ++++ .../com/utun/chat/data/RadioOverlayService.kt | 42 +++++++++++++++++-- .../com/utun/chat/ui/screens/ChatScreen.kt | 11 ++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt index 1099154b..bedf2560 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioAudioEngine.kt @@ -8,6 +8,8 @@ import android.media.AudioRecord import android.media.AudioTrack import android.media.MediaRecorder import com.utun.chat.ChatApplication +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** * Полудуплексный аудио-движок рации (аналог CallAudioEngine, но с PTT). @@ -44,6 +46,9 @@ class RadioAudioEngine { @Volatile private var groupId = 0L @Volatile private var talking = false + private val _talkingFlow = MutableStateFlow(false) + val talkingFlow: StateFlow = _talkingFlow + fun isActive(): Boolean = active fun isTalking(): Boolean = talking @@ -128,6 +133,7 @@ class RadioAudioEngine { if (!active) return active = false talking = false + _talkingFlow.value = false captureThread?.join(1000); captureThread = null playThread?.join(1000); playThread = null @@ -148,6 +154,7 @@ class RadioAudioEngine { fun talkBegin() { if (!active || talking) return talking = true + _talkingFlow.value = true NativeLib.radioTalkBegin(groupId) } @@ -155,6 +162,7 @@ class RadioAudioEngine { fun talkEnd() { if (!active || !talking) return talking = false + _talkingFlow.value = false NativeLib.radioTalkEnd(groupId) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt index 1d8e4145..c0f31193 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/RadioOverlayService.kt @@ -6,6 +6,7 @@ import android.app.Service import android.content.Context import android.content.Intent import android.graphics.PixelFormat +import android.graphics.drawable.GradientDrawable import android.os.Build import android.os.IBinder import android.provider.Settings @@ -13,10 +14,13 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View +import android.view.ViewConfiguration import android.view.WindowManager import android.widget.TextView import androidx.core.app.NotificationCompat import kotlin.math.abs +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** * Оверлей PTT поверх других приложений: плавающая круглая кнопка, drag-and-drop. @@ -29,6 +33,12 @@ class RadioOverlayService : Service() { companion object { const val CHANNEL_ID = "radio_overlay" const val NOTIF_ID = 1002 + const val COLOR_IDLE = 0xFF81C784.toInt() + const val COLOR_TALKING = 0xFFE53935.toInt() + const val DRAG_LOCK_MS = 300L + + private val _isShown = MutableStateFlow(false) + val isShown: StateFlow = _isShown fun canDrawOverlays(ctx: Context): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(ctx) @@ -89,8 +99,12 @@ class RadioOverlayService : Service() { text = "\uD83C\uDF99" textSize = 30f gravity = android.view.Gravity.CENTER - setBackgroundColor(0xFFE74C3C.toInt()) + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(COLOR_IDLE) + } } + val touchSlop = (ViewConfiguration.get(this).scaledTouchSlop * 2.5f).toInt() val lp = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, @@ -109,6 +123,7 @@ class RadioOverlayService : Service() { private var startX = 0; private var startY = 0 private var touchX = 0f; private var touchY = 0f private var dragging = false + private var dragAllowed = true override fun onTouch(view: View, event: MotionEvent): Boolean { when (event.action) { @@ -118,16 +133,25 @@ class RadioOverlayService : Service() { touchX = event.rawX touchY = event.rawY dragging = false + dragAllowed = true beginTalk() return true } MotionEvent.ACTION_MOVE -> { - if (abs(event.rawX - touchX) > 8 || abs(event.rawY - touchY) > 8) { - dragging = true - endTalk() + if (dragging) { lp.x = startX + (event.rawX - touchX).toInt() lp.y = startY + (event.rawY - touchY).toInt() wm.updateViewLayout(v, lp) + } else if (dragAllowed) { + if (event.eventTime - event.downTime > DRAG_LOCK_MS) { + dragAllowed = false + } else if (abs(event.rawX - touchX) > touchSlop || abs(event.rawY - touchY) > touchSlop) { + dragging = true + endTalk() + lp.x = startX + (event.rawX - touchX).toInt() + lp.y = startY + (event.rawY - touchY).toInt() + wm.updateViewLayout(v, lp) + } } return true } @@ -142,27 +166,37 @@ class RadioOverlayService : Service() { wm.addView(v, lp) overlayView = v params = lp + _isShown.value = true LogManager.addLog("INFO", "RadioOverlay", "overlay shown") } private fun beginTalk() { if (talking) return talking = true + updateButtonColor() RadioAudioEngine.instance().talkBegin() } private fun endTalk() { if (!talking) return talking = false + updateButtonColor() RadioAudioEngine.instance().talkEnd() } + private fun updateButtonColor() { + (overlayView?.background as? GradientDrawable)?.setColor( + if (talking) COLOR_TALKING else COLOR_IDLE + ) + } + private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt() override fun onDestroy() { endTalk() overlayView?.let { windowManager?.removeView(it) } overlayView = null + _isShown.value = false LogManager.addLog("INFO", "RadioOverlay", "overlay destroyed") super.onDestroy() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index bbb677cc..beebc114 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -21,12 +21,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.utun.chat.data.Channel import com.utun.chat.data.Message import com.utun.chat.data.NativeLib +import com.utun.chat.data.RadioAudioEngine +import com.utun.chat.data.RadioOverlayService import com.utun.chat.ui.components.InputBar import com.utun.chat.ui.components.MessageBubble import com.utun.chat.viewmodel.ChatViewModel @@ -64,6 +67,8 @@ fun ChatScreen( /* рация */ val radioActive by viewModel.radioActive.collectAsState() val radioTalking by viewModel.radioTalking.collectAsState() + val overlayShown by RadioOverlayService.isShown.collectAsState() + val localTalking by RadioAudioEngine.instance().talkingFlow.collectAsState() /* attach menu */ var showAttachMenu by remember { mutableStateOf(false) } @@ -173,9 +178,10 @@ fun ChatScreen( onPlayVideo = { msg -> onPlayVideo(msg) }) } } - if (radioActive) { + if (radioActive && !overlayShown) { RadioPttButton( modifier = Modifier.align(Alignment.TopCenter).padding(top = 12.dp), + isTalking = localTalking, onTalkBegin = { viewModel.radioTalkBegin() }, onTalkEnd = { viewModel.radioTalkEnd() } ) @@ -260,6 +266,7 @@ fun ChatScreen( @Composable private fun RadioPttButton( modifier: Modifier = Modifier, + isTalking: Boolean = false, onTalkBegin: () -> Unit, onTalkEnd: () -> Unit ) { @@ -276,7 +283,7 @@ private fun RadioPttButton( ) }, shape = CircleShape, - color = MaterialTheme.colorScheme.error, + color = if (isTalking) Color(0xFFE53935) else Color(0xFF81C784), shadowElevation = 6.dp ) { Box(contentAlignment = Alignment.Center) {