chatgui-android: PTT-оверлей — talking flow, цвет idle/talking, drag-lock
- RadioAudioEngine: StateFlow talking для реактивной подсветки кнопки - RadioOverlayService: градиент idle/talking, drag-lock, сброс isShown при destroy - ChatScreen: скрывать PTT-кнопку при активном оверлее, подсветка по talking
This commit is contained in:
1 parent
841b3042cf
commit
5e955168d2
3 files changed
+55
-6
No files matched your search
@@ -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<Boolean> = _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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Boolean> = _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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user