mirror of
https://github.com/souravanand001/ai-assistant-android.git
synced 2026-10-03 04:36:08 +00:00
Improved animation for HandsFreeBar and renamed layout
This commit is contained in:
parent
906659ca4e
commit
04216343f1
4 changed files with 239 additions and 199 deletions
|
|
@ -53,7 +53,7 @@ import androidx.compose.animation.core.tween
|
|||
|
||||
@Composable
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
fun MyLayout(
|
||||
fun ChatLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
onShowCopyIconChange: (Boolean) -> Unit,
|
||||
selectedItemIndex: Int?,
|
||||
|
|
@ -286,9 +286,9 @@ fun MyLayout(
|
|||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MyLayoutPreview() {
|
||||
fun ChatLayoutPreview() {
|
||||
AssistantTheme {
|
||||
MyLayout(
|
||||
ChatLayout(
|
||||
onShowCopyIconChange = {},
|
||||
selectedItemIndex = null,
|
||||
onSelectedItemChange = {},
|
||||
|
|
@ -348,7 +348,7 @@ fun ChatScreenContent(
|
|||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
MyLayout(
|
||||
ChatLayout(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onShowCopyIconChange = { newValue ->
|
||||
showCopyIcon = newValue
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.app.assistant.ui.screen
|
|||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
|
|
@ -33,10 +34,15 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -72,28 +78,41 @@ fun HandsFreeBar(
|
|||
}
|
||||
}
|
||||
|
||||
val orbBackgroundColor by animateColorAsState(
|
||||
targetValue = when (currentState) {
|
||||
HandsFreeState.STARTING -> MaterialTheme.colorScheme.surfaceVariant
|
||||
HandsFreeState.LISTENING -> MaterialTheme.colorScheme.primaryContainer
|
||||
HandsFreeState.THINKING -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
HandsFreeState.SPEAKING -> MaterialTheme.colorScheme.secondaryContainer
|
||||
},
|
||||
// Dynamic state transition parameters
|
||||
val startingAlpha by animateFloatAsState(
|
||||
targetValue = if (currentState == HandsFreeState.STARTING) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 350),
|
||||
label = "orbBackground"
|
||||
label = "startingAlpha"
|
||||
)
|
||||
|
||||
val orbContentColor by animateColorAsState(
|
||||
targetValue = when (currentState) {
|
||||
HandsFreeState.STARTING -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
HandsFreeState.LISTENING -> MaterialTheme.colorScheme.primary
|
||||
HandsFreeState.THINKING -> MaterialTheme.colorScheme.tertiary
|
||||
HandsFreeState.SPEAKING -> MaterialTheme.colorScheme.secondary
|
||||
},
|
||||
val listeningProgress by animateFloatAsState(
|
||||
targetValue = if (currentState == HandsFreeState.LISTENING) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 350),
|
||||
label = "orbContent"
|
||||
label = "listeningProgress"
|
||||
)
|
||||
|
||||
val thinkingProgress by animateFloatAsState(
|
||||
targetValue = if (currentState == HandsFreeState.THINKING) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 350),
|
||||
label = "thinkingProgress"
|
||||
)
|
||||
|
||||
val speakingProgress by animateFloatAsState(
|
||||
targetValue = if (currentState == HandsFreeState.SPEAKING) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 350),
|
||||
label = "speakingProgress"
|
||||
)
|
||||
|
||||
val dotsSpreadProgress by animateFloatAsState(
|
||||
targetValue = if (currentState == HandsFreeState.THINKING || currentState == HandsFreeState.SPEAKING) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 350),
|
||||
label = "dotsSpreadProgress"
|
||||
)
|
||||
|
||||
// Keep the orb background color and content color consistent across all states
|
||||
val orbBackgroundColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
||||
val orbContentColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
val orbWidth by animateDpAsState(
|
||||
targetValue = when (currentState) {
|
||||
HandsFreeState.STARTING -> 48.dp
|
||||
|
|
@ -134,12 +153,81 @@ fun HandsFreeBar(
|
|||
initialValue = 0f,
|
||||
targetValue = (2 * Math.PI).toFloat(),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1200, easing = LinearEasing),
|
||||
animation = tween(2200, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "wavePhase"
|
||||
)
|
||||
|
||||
// Starting state animations
|
||||
val startingRotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1500, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "startingRotation"
|
||||
)
|
||||
|
||||
val coreScale by infiniteTransition.animateFloat(
|
||||
initialValue = 0.7f,
|
||||
targetValue = 1.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(800, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "startingCore"
|
||||
)
|
||||
|
||||
// Speaking state voice equalizer animations
|
||||
val speakTransition = rememberInfiniteTransition(label = "speakingEqualizer")
|
||||
val s1 by speakTransition.animateFloat(
|
||||
initialValue = 0.15f,
|
||||
targetValue = 0.9f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(350, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar1"
|
||||
)
|
||||
val s2 by speakTransition.animateFloat(
|
||||
initialValue = 0.25f,
|
||||
targetValue = 0.75f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(280, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar2"
|
||||
)
|
||||
val s3 by speakTransition.animateFloat(
|
||||
initialValue = 0.1f,
|
||||
targetValue = 1.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(420, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar3"
|
||||
)
|
||||
val s4 by speakTransition.animateFloat(
|
||||
initialValue = 0.2f,
|
||||
targetValue = 0.8f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(310, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar4"
|
||||
)
|
||||
val s5 by speakTransition.animateFloat(
|
||||
initialValue = 0.15f,
|
||||
targetValue = 0.6f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(460, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar5"
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -182,15 +270,15 @@ fun HandsFreeBar(
|
|||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (currentState == HandsFreeState.THINKING) {
|
||||
// Outer pulsing ring (drawn behind using graphicsLayer scale so it does not affect parent height)
|
||||
// Outer pulsing ring (drawn behind using graphicsLayer scale so it does not affect parent height)
|
||||
if (thinkingProgress > 0f) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(orbWidth, orbHeight)
|
||||
.graphicsLayer {
|
||||
scaleX = thinkingPulseScale
|
||||
scaleY = thinkingPulseScale
|
||||
alpha = thinkingPulseAlpha
|
||||
alpha = thinkingPulseAlpha * thinkingProgress
|
||||
}
|
||||
.background(
|
||||
color = orbBackgroundColor,
|
||||
|
|
@ -205,193 +293,148 @@ fun HandsFreeBar(
|
|||
.background(color = orbBackgroundColor, shape = RoundedCornerShape(24.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Creative animations depending on state
|
||||
when (currentState) {
|
||||
HandsFreeState.STARTING -> {
|
||||
// Circular rotating arc segment with a pulsing inner core dot
|
||||
val rotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1500, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "startingRotation"
|
||||
)
|
||||
|
||||
val coreScale by infiniteTransition.animateFloat(
|
||||
initialValue = 0.7f,
|
||||
targetValue = 1.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(800, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "startingCore"
|
||||
)
|
||||
val colorPrimary = MaterialTheme.colorScheme.primary
|
||||
val colorSecondary = MaterialTheme.colorScheme.secondary
|
||||
val colorTertiary = MaterialTheme.colorScheme.tertiary
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Outer spinning arc segment
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { rotationZ = rotation }
|
||||
) {
|
||||
drawArc(
|
||||
color = orbContentColor,
|
||||
startAngle = 0f,
|
||||
sweepAngle = 270f,
|
||||
useCenter = false,
|
||||
style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
// Inner breathing core
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = coreScale
|
||||
scaleY = coreScale
|
||||
}
|
||||
.background(orbContentColor, CircleShape)
|
||||
)
|
||||
}
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
val centerX = width / 2
|
||||
val centerY = height / 2
|
||||
|
||||
// Helper to lerp floats
|
||||
fun lerp(start: Float, stop: Float, fraction: Float): Float {
|
||||
return start + fraction * (stop - start)
|
||||
}
|
||||
HandsFreeState.LISTENING -> {
|
||||
// Out of the box: Flowing double sine wave
|
||||
Canvas(modifier = Modifier.size(width = 52.dp, height = 24.dp)) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
val centerY = height / 2
|
||||
val amplitude = height / 2 - 2.dp.toPx()
|
||||
|
||||
// Draw primary wave
|
||||
val wavePath = Path()
|
||||
wavePath.moveTo(0f, centerY)
|
||||
for (x in 0..width.toInt()) {
|
||||
val frequency = (2 * Math.PI.toFloat() / width) * 1.5f
|
||||
val y = kotlin.math.sin(x * frequency + wavePhase) * amplitude + centerY
|
||||
wavePath.lineTo(x.toFloat(), y)
|
||||
}
|
||||
drawPath(
|
||||
path = wavePath,
|
||||
// A. STARTING outer ring rotating arc segment
|
||||
if (startingAlpha > 0f) {
|
||||
val arcSize = 32.dp.toPx()
|
||||
rotate(degrees = startingRotation, pivot = Offset(centerX, centerY)) {
|
||||
drawArc(
|
||||
color = orbContentColor,
|
||||
style = Stroke(width = 2.5.dp.toPx(), cap = StrokeCap.Round)
|
||||
)
|
||||
|
||||
// Draw secondary wave
|
||||
val wavePath2 = Path()
|
||||
wavePath2.moveTo(0f, centerY)
|
||||
for (x in 0..width.toInt()) {
|
||||
val frequency = (2 * Math.PI.toFloat() / width) * 1.2f
|
||||
val y = kotlin.math.sin(x * frequency - wavePhase + 1.2f) * (amplitude * 0.6f) + centerY
|
||||
wavePath2.lineTo(x.toFloat(), y)
|
||||
}
|
||||
drawPath(
|
||||
path = wavePath2,
|
||||
color = orbContentColor.copy(alpha = 0.5f),
|
||||
style = Stroke(width = 1.5.dp.toPx(), cap = StrokeCap.Round)
|
||||
startAngle = 0f,
|
||||
sweepAngle = 270f,
|
||||
useCenter = false,
|
||||
topLeft = Offset(centerX - arcSize / 2, centerY - arcSize / 2),
|
||||
size = Size(arcSize, arcSize),
|
||||
style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round),
|
||||
alpha = startingAlpha
|
||||
)
|
||||
}
|
||||
}
|
||||
HandsFreeState.THINKING -> {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
val centerX = width / 2
|
||||
val centerY = height / 2
|
||||
|
||||
val baseRadius = 2.5.dp.toPx()
|
||||
val spacing = 7.5.dp.toPx()
|
||||
val horizontalAmplitude = 5.dp.toPx()
|
||||
val verticalAmplitude = 1.5.dp.toPx()
|
||||
// B. STARTING core dot (solid color)
|
||||
if (startingAlpha > 0f) {
|
||||
val startDotRadius = 6.dp.toPx() * coreScale
|
||||
drawCircle(
|
||||
color = orbContentColor,
|
||||
radius = startDotRadius,
|
||||
center = Offset(centerX, centerY),
|
||||
alpha = startingAlpha
|
||||
)
|
||||
}
|
||||
|
||||
for (i in 0 until 5) {
|
||||
// Staggered phase for each dot
|
||||
val dotPhase = wavePhase - i * 0.6f
|
||||
// C. LISTENING state morphing blob (gradient with white outline)
|
||||
if (listeningProgress > 0f) {
|
||||
// 1. Soft glowing background aura inside the capsule
|
||||
val auraRadius = (height / 2) * 0.85f
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
colorTertiary.copy(alpha = 0.3f * listeningProgress),
|
||||
colorPrimary.copy(alpha = 0.1f * listeningProgress),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(centerX, centerY),
|
||||
radius = auraRadius
|
||||
),
|
||||
radius = auraRadius,
|
||||
center = Offset(centerX, centerY)
|
||||
)
|
||||
|
||||
// 3D-like orbital offsets
|
||||
val dx = horizontalAmplitude * kotlin.math.cos(dotPhase)
|
||||
val dy = verticalAmplitude * kotlin.math.sin(dotPhase)
|
||||
// 2. Morphing Blob Path calculation
|
||||
val baseRadius = lerp(6.dp.toPx(), height * 0.32f, listeningProgress)
|
||||
val amp1 = lerp(0f, 2.dp.toPx(), listeningProgress)
|
||||
val amp2 = lerp(0f, 1.5.dp.toPx(), listeningProgress)
|
||||
|
||||
val homeX = centerX + (i - 2) * spacing
|
||||
val x = homeX + dx
|
||||
val y = centerY + dy
|
||||
|
||||
// Scale and alpha oscillate to create depth/3D effect
|
||||
val scale = 0.8f + 0.4f * ((kotlin.math.sin(dotPhase) + 1f) / 2f)
|
||||
val alpha = 0.4f + 0.6f * ((kotlin.math.sin(dotPhase) + 1f) / 2f)
|
||||
|
||||
drawCircle(
|
||||
color = orbContentColor,
|
||||
radius = baseRadius * scale,
|
||||
center = Offset(x, y),
|
||||
alpha = alpha
|
||||
)
|
||||
val blobPath = Path()
|
||||
val steps = 72
|
||||
for (i in 0..steps) {
|
||||
val angle = (i * 2 * Math.PI / steps).toFloat()
|
||||
val r = baseRadius +
|
||||
amp1 * kotlin.math.sin(2 * angle + wavePhase) +
|
||||
amp2 * kotlin.math.cos(3 * angle - wavePhase)
|
||||
val x = centerX + r * kotlin.math.cos(angle)
|
||||
val y = centerY + r * kotlin.math.sin(angle)
|
||||
if (i == 0) {
|
||||
blobPath.moveTo(x, y)
|
||||
} else {
|
||||
blobPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
HandsFreeState.SPEAKING -> {
|
||||
// Out of the box: Active voice equalizer with varied baseline heights
|
||||
val speakTransition = rememberInfiniteTransition(label = "speakingEqualizer")
|
||||
val s1 by speakTransition.animateFloat(
|
||||
initialValue = 0.15f,
|
||||
targetValue = 0.9f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(350, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar1"
|
||||
)
|
||||
val s2 by speakTransition.animateFloat(
|
||||
initialValue = 0.25f,
|
||||
targetValue = 0.75f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(280, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar2"
|
||||
)
|
||||
val s3 by speakTransition.animateFloat(
|
||||
initialValue = 0.1f,
|
||||
targetValue = 1.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(420, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar3"
|
||||
)
|
||||
val s4 by speakTransition.animateFloat(
|
||||
initialValue = 0.2f,
|
||||
targetValue = 0.8f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(310, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar4"
|
||||
)
|
||||
val s5 by speakTransition.animateFloat(
|
||||
initialValue = 0.15f,
|
||||
targetValue = 0.6f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(460, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "speakBar5"
|
||||
blobPath.close()
|
||||
|
||||
val blobGradient = Brush.linearGradient(
|
||||
colors = listOf(colorPrimary, colorTertiary, colorSecondary),
|
||||
start = Offset(centerX - baseRadius, centerY - baseRadius),
|
||||
end = Offset(centerX + baseRadius, centerY + baseRadius)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Box(Modifier.width(3.dp).height(8.dp + 16.dp * s1).background(orbContentColor, CircleShape))
|
||||
Box(Modifier.width(3.dp).height(12.dp + 14.dp * s2).background(orbContentColor, CircleShape))
|
||||
Box(Modifier.width(3.dp).height(6.dp + 22.dp * s3).background(orbContentColor, CircleShape))
|
||||
Box(Modifier.width(3.dp).height(10.dp + 18.dp * s4).background(orbContentColor, CircleShape))
|
||||
Box(Modifier.width(3.dp).height(8.dp + 12.dp * s5).background(orbContentColor, CircleShape))
|
||||
// Fill the blob
|
||||
drawPath(path = blobPath, brush = blobGradient, alpha = listeningProgress)
|
||||
|
||||
// Thin white outline
|
||||
drawPath(
|
||||
path = blobPath,
|
||||
color = Color.White.copy(alpha = 0.4f * listeningProgress),
|
||||
style = Stroke(width = 1.dp.toPx())
|
||||
)
|
||||
}
|
||||
|
||||
// D. THINKING and SPEAKING states: 5 orbiting dots / equalizer bars
|
||||
if (dotsSpreadProgress > 0f) {
|
||||
val spacing = 8.dp.toPx()
|
||||
val dotWidth = 5.dp.toPx()
|
||||
val dotHeight = 5.dp.toPx()
|
||||
|
||||
val horizontalAmplitude = 5.dp.toPx()
|
||||
val verticalAmplitude = 1.5.dp.toPx()
|
||||
|
||||
for (i in 0 until 5) {
|
||||
// Equalizer height for this bar
|
||||
val eqHeight = when (i) {
|
||||
0 -> 8.dp.toPx() + 16.dp.toPx() * s1
|
||||
1 -> 12.dp.toPx() + 14.dp.toPx() * s2
|
||||
2 -> 6.dp.toPx() + 22.dp.toPx() * s3
|
||||
3 -> 10.dp.toPx() + 18.dp.toPx() * s4
|
||||
else -> 8.dp.toPx() + 12.dp.toPx() * s5
|
||||
}
|
||||
|
||||
// Interpolate height between circle (dotHeight) and equalizer bar height
|
||||
val currentHeight = lerp(dotHeight, eqHeight, speakingProgress)
|
||||
|
||||
// 3D-like orbital offsets (only active in THINKING)
|
||||
val dotPhase = wavePhase - i * 0.6f
|
||||
val dx = horizontalAmplitude * kotlin.math.cos(dotPhase.toDouble()).toFloat() * thinkingProgress
|
||||
val dy = verticalAmplitude * kotlin.math.sin(dotPhase.toDouble()).toFloat() * thinkingProgress
|
||||
|
||||
// Interpolate home horizontal position based on spread progress
|
||||
val homeX = centerX + (i - 2) * spacing * dotsSpreadProgress
|
||||
val x = homeX + dx
|
||||
val y = centerY + dy
|
||||
|
||||
val topLeftX = x - dotWidth / 2
|
||||
val topLeftY = y - currentHeight / 2
|
||||
|
||||
drawRoundRect(
|
||||
color = orbContentColor,
|
||||
topLeft = Offset(topLeftX, topLeftY),
|
||||
size = Size(dotWidth, currentHeight),
|
||||
cornerRadius = CornerRadius(dotWidth / 2, dotWidth / 2),
|
||||
alpha = dotsSpreadProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,9 +296,6 @@ class MainViewModel(
|
|||
if (newMuteState) {
|
||||
_isListening.value = false
|
||||
_isMicReady.value = false
|
||||
triggerToast("Microphone muted")
|
||||
} else {
|
||||
triggerToast("Microphone unmuted")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue