radio: PTT double-tap latch + single-tap off (in-chat & overlay); compressor initial gain = mid-range
This commit is contained in:
1 parent
556b0bc7b7
commit
61bf235ef4
3 files changed
+101
-16
No files matched your search
@@ -12,6 +12,7 @@ import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.SystemClock
|
||||
import android.provider.Settings
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
@@ -29,9 +30,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Оверлей PTT поверх других приложений: плавающая круглая кнопка, drag-and-drop.
|
||||
* Удержание активирует передачу (RadioAudioEngine.talkBegin/talkEnd), отпускание —
|
||||
* завершает. Работает пока включена опция «PTT поверх приложений» и разрешено
|
||||
* «поверх других приложений» (SYSTEM_ALERT_WINDOW).
|
||||
* Удержание > PTT_HOLD_MS активирует передачу (RadioAudioEngine.talkBegin/talkEnd),
|
||||
* отпускание — завершает. Двойной тап фиксирует передачу (latch) без удержания,
|
||||
* повторный двойной тап или drag — отбой. Работает пока включена опция
|
||||
* «PTT поверх приложений» и разрешено «поверх других приложений» (SYSTEM_ALERT_WINDOW).
|
||||
*/
|
||||
class RadioOverlayService : Service() {
|
||||
|
||||
@@ -41,6 +43,7 @@ class RadioOverlayService : Service() {
|
||||
const val COLOR_IDLE = 0xFF81C784.toInt()
|
||||
const val COLOR_TALKING = 0xFFE53935.toInt()
|
||||
const val DRAG_LOCK_MS = 300L
|
||||
const val PTT_HOLD_MS = 250L
|
||||
|
||||
private val _isShown = MutableStateFlow(false)
|
||||
val isShown: StateFlow<Boolean> = _isShown
|
||||
@@ -135,7 +138,7 @@ class RadioOverlayService : Service() {
|
||||
val wm = windowManager ?: return
|
||||
val v = TextView(this).apply {
|
||||
text = "\uD83C\uDF99"
|
||||
textSize = 30f
|
||||
textSize = 21f
|
||||
gravity = android.view.Gravity.CENTER
|
||||
background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
@@ -143,6 +146,7 @@ class RadioOverlayService : Service() {
|
||||
}
|
||||
}
|
||||
val touchSlop = (ViewConfiguration.get(this).scaledTouchSlop * 2.5f).toInt()
|
||||
val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout().toLong()
|
||||
val lp = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
@@ -153,7 +157,7 @@ class RadioOverlayService : Service() {
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
width = dp(96); height = dp(96)
|
||||
width = dp(67); height = dp(67)
|
||||
gravity = Gravity.TOP or Gravity.START
|
||||
x = 16; y = 200
|
||||
}
|
||||
@@ -162,6 +166,9 @@ class RadioOverlayService : Service() {
|
||||
private var touchX = 0f; private var touchY = 0f
|
||||
private var dragging = false
|
||||
private var dragAllowed = true
|
||||
private var latched = false
|
||||
private var lastTapUpTime = 0L
|
||||
private val talkBeginRunnable = Runnable { beginTalk() }
|
||||
|
||||
override fun onTouch(view: View, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
@@ -172,7 +179,8 @@ class RadioOverlayService : Service() {
|
||||
touchY = event.rawY
|
||||
dragging = false
|
||||
dragAllowed = true
|
||||
beginTalk()
|
||||
view.removeCallbacks(talkBeginRunnable)
|
||||
view.postDelayed(talkBeginRunnable, PTT_HOLD_MS)
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
@@ -185,6 +193,8 @@ class RadioOverlayService : Service() {
|
||||
dragAllowed = false
|
||||
} else if (abs(event.rawX - touchX) > touchSlop || abs(event.rawY - touchY) > touchSlop) {
|
||||
dragging = true
|
||||
view.removeCallbacks(talkBeginRunnable)
|
||||
latched = false
|
||||
endTalkImmediate()
|
||||
lp.x = startX + (event.rawX - touchX).toInt()
|
||||
lp.y = startY + (event.rawY - touchY).toInt()
|
||||
@@ -193,8 +203,27 @@ class RadioOverlayService : Service() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
endTalk()
|
||||
MotionEvent.ACTION_UP -> {
|
||||
view.removeCallbacks(talkBeginRunnable)
|
||||
val now = SystemClock.uptimeMillis()
|
||||
val isDouble = lastTapUpTime != 0L && (now - lastTapUpTime) <= doubleTapTimeout
|
||||
if (latched) {
|
||||
latched = false
|
||||
lastTapUpTime = 0L
|
||||
endTalk()
|
||||
} else if (isDouble) {
|
||||
latched = true
|
||||
lastTapUpTime = 0L
|
||||
beginTalk()
|
||||
} else {
|
||||
lastTapUpTime = now
|
||||
endTalk()
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
view.removeCallbacks(talkBeginRunnable)
|
||||
if (!latched) endTalk()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.utun.chat.ui.screens
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -42,6 +44,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -276,7 +279,10 @@ fun ChatScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/* Круглая PTT-кнопка рации: держим — говорим. */
|
||||
/* Круглая PTT-кнопка рации: удержание > PTT_HOLD_MS — говорим, двойной тап —
|
||||
* фиксирует передачу (latch) без удержания, следующий тап/двойной тап — отбой. */
|
||||
private const val PTT_HOLD_MS = 250L
|
||||
|
||||
@Composable
|
||||
private fun RadioPttButton(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -284,17 +290,60 @@ private fun RadioPttButton(
|
||||
onTalkBegin: () -> Unit,
|
||||
onTalkEnd: () -> Unit
|
||||
) {
|
||||
val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout().toLong()
|
||||
var latched by remember { mutableStateOf(false) }
|
||||
var lastTapUp by remember { mutableStateOf(0L) }
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.size(96.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
val downId = down.id
|
||||
down.consume()
|
||||
|
||||
val now = SystemClock.uptimeMillis()
|
||||
val isSecondTap = lastTapUp != 0L && (now - lastTapUp) <= doubleTapTimeout
|
||||
|
||||
/* быстрое отпускание (до PTT_HOLD_MS) — это тап */
|
||||
var tapped = false
|
||||
withTimeoutOrNull(PTT_HOLD_MS) {
|
||||
while (true) {
|
||||
val ev = awaitPointerEvent()
|
||||
val c = ev.changes.firstOrNull { it.id == downId } ?: return@withTimeoutOrNull
|
||||
if (!c.pressed) { tapped = true; return@withTimeoutOrNull }
|
||||
c.consume()
|
||||
}
|
||||
}
|
||||
|
||||
if (tapped) {
|
||||
if (latched) {
|
||||
latched = false
|
||||
lastTapUp = 0L
|
||||
onTalkEnd()
|
||||
} else if (isSecondTap) {
|
||||
latched = true
|
||||
lastTapUp = 0L
|
||||
onTalkBegin()
|
||||
} else {
|
||||
lastTapUp = SystemClock.uptimeMillis()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
/* удержание — говорим, пока не отпустят */
|
||||
onTalkBegin()
|
||||
tryAwaitRelease()
|
||||
while (true) {
|
||||
val ev = awaitPointerEvent()
|
||||
val c = ev.changes.firstOrNull { it.id == downId } ?: break
|
||||
c.consume()
|
||||
if (!c.pressed) break
|
||||
}
|
||||
onTalkEnd()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = CircleShape,
|
||||
color = if (isTalking) Color(0xFFE53935) else Color(0xFF81C784),
|
||||
|
||||
Reference in new issue
Block a user