Android: orb as always-visible PTT button
- Orb is now always visible (not just when talk mode is on) - Tap to toggle talk mode on/off - When inactive: static, dimmed, shows 'Tap to talk' - When active: pulsing animation with status text - Haptic feedback on tap
This commit is contained in:
parent
32cf8c3290
commit
69ba01468c
@ -264,16 +264,16 @@ fun RootScreen(viewModel: MainViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (talkEnabled) {
|
// Always show the orb as a PTT button - pulses when active, static when inactive
|
||||||
Popup(alignment = Alignment.Center, properties = PopupProperties(focusable = false)) {
|
Popup(alignment = Alignment.Center, properties = PopupProperties(focusable = false)) {
|
||||||
TalkOrbOverlay(
|
TalkOrbOverlay(
|
||||||
seamColor = seamColor,
|
seamColor = seamColor,
|
||||||
statusText = talkStatusText,
|
statusText = talkStatusText,
|
||||||
isListening = talkIsListening,
|
isListening = talkIsListening,
|
||||||
isSpeaking = talkIsSpeaking,
|
isSpeaking = talkIsSpeaking,
|
||||||
onDismiss = { viewModel.setTalkEnabled(false) },
|
isActive = talkEnabled,
|
||||||
)
|
onTap = { viewModel.setTalkEnabled(!talkEnabled) },
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentSheet = sheet
|
val currentSheet = sheet
|
||||||
|
|||||||
@ -37,12 +37,13 @@ fun TalkOrbOverlay(
|
|||||||
statusText: String,
|
statusText: String,
|
||||||
isListening: Boolean,
|
isListening: Boolean,
|
||||||
isSpeaking: Boolean,
|
isSpeaking: Boolean,
|
||||||
|
isActive: Boolean = true,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDismiss: (() -> Unit)? = null,
|
onTap: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val transition = rememberInfiniteTransition(label = "talk-orb")
|
val transition = rememberInfiniteTransition(label = "talk-orb")
|
||||||
val t by
|
val animatedT by
|
||||||
transition.animateFloat(
|
transition.animateFloat(
|
||||||
initialValue = 0f,
|
initialValue = 0f,
|
||||||
targetValue = 1f,
|
targetValue = 1f,
|
||||||
@ -53,15 +54,22 @@ fun TalkOrbOverlay(
|
|||||||
),
|
),
|
||||||
label = "pulse",
|
label = "pulse",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Only pulse when active, otherwise static
|
||||||
|
val t = if (isActive) animatedT else 0f
|
||||||
|
|
||||||
val trimmed = statusText.trim()
|
val trimmed = statusText.trim()
|
||||||
val showStatus = trimmed.isNotEmpty() && trimmed != "Off"
|
val showStatus = isActive && trimmed.isNotEmpty() && trimmed != "Off"
|
||||||
val phase =
|
val phase =
|
||||||
when {
|
when {
|
||||||
|
!isActive -> "Tap to talk"
|
||||||
isSpeaking -> "Speaking"
|
isSpeaking -> "Speaking"
|
||||||
isListening -> "Listening"
|
isListening -> "Listening"
|
||||||
else -> "Thinking"
|
else -> "Thinking"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dim the orb when inactive
|
||||||
|
val orbAlpha = if (isActive) 1f else 0.5f
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.padding(24.dp),
|
modifier = modifier.padding(24.dp),
|
||||||
@ -75,10 +83,10 @@ fun TalkOrbOverlay(
|
|||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null, // No ripple - the orb itself is the feedback
|
indication = null, // No ripple - the orb itself is the feedback
|
||||||
enabled = onDismiss != null,
|
enabled = onTap != null,
|
||||||
onClick = {
|
onClick = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
onDismiss?.invoke()
|
onTap?.invoke()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -87,30 +95,33 @@ fun TalkOrbOverlay(
|
|||||||
|
|
||||||
val ring1 = 1.05f + (t * 0.25f)
|
val ring1 = 1.05f + (t * 0.25f)
|
||||||
val ring2 = 1.20f + (t * 0.55f)
|
val ring2 = 1.20f + (t * 0.55f)
|
||||||
val ringAlpha1 = (1f - t) * 0.34f
|
val ringAlpha1 = (1f - t) * 0.34f * orbAlpha
|
||||||
val ringAlpha2 = (1f - t) * 0.22f
|
val ringAlpha2 = (1f - t) * 0.22f * orbAlpha
|
||||||
|
|
||||||
drawCircle(
|
// Only draw pulsing rings when active
|
||||||
color = seamColor.copy(alpha = ringAlpha1),
|
if (isActive) {
|
||||||
radius = baseRadius * ring1,
|
drawCircle(
|
||||||
center = center,
|
color = seamColor.copy(alpha = ringAlpha1),
|
||||||
style = Stroke(width = 3.dp.toPx()),
|
radius = baseRadius * ring1,
|
||||||
)
|
center = center,
|
||||||
drawCircle(
|
style = Stroke(width = 3.dp.toPx()),
|
||||||
color = seamColor.copy(alpha = ringAlpha2),
|
)
|
||||||
radius = baseRadius * ring2,
|
drawCircle(
|
||||||
center = center,
|
color = seamColor.copy(alpha = ringAlpha2),
|
||||||
style = Stroke(width = 3.dp.toPx()),
|
radius = baseRadius * ring2,
|
||||||
)
|
center = center,
|
||||||
|
style = Stroke(width = 3.dp.toPx()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
drawCircle(
|
drawCircle(
|
||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors =
|
colors =
|
||||||
listOf(
|
listOf(
|
||||||
seamColor.copy(alpha = 0.92f),
|
seamColor.copy(alpha = 0.92f * orbAlpha),
|
||||||
seamColor.copy(alpha = 0.40f),
|
seamColor.copy(alpha = 0.40f * orbAlpha),
|
||||||
Color.Black.copy(alpha = 0.56f),
|
Color.Black.copy(alpha = 0.56f * orbAlpha),
|
||||||
),
|
),
|
||||||
center = center,
|
center = center,
|
||||||
radius = baseRadius * 1.35f,
|
radius = baseRadius * 1.35f,
|
||||||
@ -120,7 +131,7 @@ fun TalkOrbOverlay(
|
|||||||
)
|
)
|
||||||
|
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = seamColor.copy(alpha = 0.34f),
|
color = seamColor.copy(alpha = 0.34f * orbAlpha),
|
||||||
radius = baseRadius,
|
radius = baseRadius,
|
||||||
center = center,
|
center = center,
|
||||||
style = Stroke(width = 1.dp.toPx()),
|
style = Stroke(width = 1.dp.toPx()),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user