fix: переделать layout записи — layered архитектура вместо cramming в 40dp panel

- Panel bar (timer + slide-to-cancel/waveform) как Layer 1
- Mic/Send circle (48dp) как overlay Layer 2 поверх панели
- LockIcon как Layer 3 над кругом через graphicsLayer (без clip)
- Убран padding(end=94dp), заменён на padding(end=44dp)
- Убран offset(x=8dp) который толкал круг за экран
- Controls увеличены 28dp→36dp для лучшей тач-зоны
- Blob scale 2.05→1.8 пропорционально новому 48dp размеру

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 21:41:29 +05:00
parent b6055c98a5
commit b13cdb7ea1

View File

@@ -670,7 +670,7 @@ private fun RecordLockedControls(
// Delete button
Box(
modifier = Modifier
.size(28.dp)
.size(36.dp)
.clip(CircleShape)
.background(deleteBgColor)
.clickable(
@@ -683,14 +683,14 @@ private fun RecordLockedControls(
imageVector = Icons.Default.Close,
contentDescription = "Delete recording",
tint = deleteIconColor,
modifier = Modifier.size(16.dp)
modifier = Modifier.size(18.dp)
)
}
// Pause/Resume button
Box(
modifier = Modifier
.size(28.dp)
.size(36.dp)
.clip(CircleShape)
.background(pauseBgColor)
.clickable(
@@ -1921,7 +1921,10 @@ fun MessageInputBar(
}
}
// Fix #8: AnimatedVisibility for smooth exit
// ── Recording panel (layered architecture) ──
// Layer 1: panel bar (timer + center content)
// Layer 2: mic/send circle OVERLAY at right edge (extends beyond panel)
// Layer 3: lock icon ABOVE circle (extends above panel)
androidx.compose.animation.AnimatedVisibility(
visible = isVoiceRecording,
enter = fadeIn(tween(180)) + expandVertically(tween(180)),
@@ -1953,26 +1956,28 @@ fun MessageInputBar(
label = "record_ui_shift"
)
// Outer Box — no clip, allows children to overflow
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 48.dp)
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(horizontal = 8.dp, vertical = 8.dp)
.onGloballyPositioned { coordinates ->
recordingInputRowHeightPx = coordinates.size.height
recordingInputRowY = coordinates.positionInWindow().y
},
contentAlignment = Alignment.CenterEnd
}
) {
// ── Layer 1: Panel bar ──
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.padding(end = 44.dp) // space for circle overlap
.clip(RoundedCornerShape(20.dp))
.background(recordingPanelColor)
.padding(start = 13.dp, end = 94.dp)
.padding(horizontal = 13.dp)
) {
// Left: blink dot + timer (all states)
// Left: blink dot + timer
Row(
modifier = Modifier
.align(Alignment.CenterStart)
@@ -1983,7 +1988,6 @@ fun MessageInputBar(
verticalAlignment = Alignment.CenterVertically
) {
if (recordUiState == RecordUiState.PAUSED) {
// Static dot (no blink) when paused
Box(
modifier = Modifier.size(28.dp),
contentAlignment = Alignment.Center
@@ -2009,7 +2013,7 @@ fun MessageInputBar(
)
}
// Center: SlideToCancel or Waveform+Controls
// Center content: SlideToCancel or Waveform+Controls
AnimatedContent(
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
modifier = Modifier
@@ -2062,22 +2066,22 @@ fun MessageInputBar(
}
}
// Mic button area with LockIcon overlay
// ── Layer 2: Mic/Send circle overlay ──
// Positioned at right edge, overlapping the panel
Box(
modifier = Modifier
.size(40.dp)
.offset(x = 8.dp),
.size(48.dp)
.align(Alignment.CenterEnd)
.zIndex(5f),
contentAlignment = Alignment.Center
) {
// LockIcon positioned above the mic button
// ── Layer 3: LockIcon above circle ──
if (recordUiState == RecordUiState.RECORDING ||
recordUiState == RecordUiState.LOCKED ||
recordUiState == RecordUiState.PAUSED
) {
// Fix #1: Lock grows 50dp→36dp as lockProgress 0→1
// Fix #2: Y-position animates closer to mic
val lockSizeDp = (50.dp - 14.dp * lockProgress)
val lockYOffset = ((-60).dp + 14.dp * lockProgress)
val lockYOffset = ((-56).dp + 14.dp * lockProgress)
LockIcon(
lockProgress = lockProgress,
isLocked = recordUiState == RecordUiState.LOCKED,
@@ -2085,35 +2089,42 @@ fun MessageInputBar(
isDarkTheme = isDarkTheme,
modifier = Modifier
.size(lockSizeDp)
.offset(y = lockYOffset)
.graphicsLayer {
translationY = with(density) { lockYOffset.toPx() }
clip = false
}
.zIndex(10f)
)
// Tooltip
if (showLockTooltip && recordUiState == RecordUiState.RECORDING) {
LockTooltip(
visible = showLockTooltip,
isDarkTheme = isDarkTheme,
modifier = Modifier
.offset(x = (-80).dp, y = (-60).dp)
.graphicsLayer {
translationX = with(density) { (-80).dp.toPx() }
translationY = with(density) { (-56).dp.toPx() }
clip = false
}
.zIndex(11f)
)
}
}
// Blob animation (visual-only enlargement)
VoiceButtonBlob(
voiceLevel = voiceLevel,
isDarkTheme = isDarkTheme,
modifier = Modifier
.fillMaxSize()
.size(48.dp)
.graphicsLayer {
scaleX = 2.05f
scaleY = 2.05f
scaleX = 1.8f
scaleY = 1.8f
clip = false
}
)
// Fix #7: Send button with scale animation
// Send or Mic button
val sendScale by animateFloatAsState(
targetValue = if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) 1f else 0f,
animationSpec = tween(durationMillis = 150, easing = FastOutSlowInEasing),
@@ -2122,13 +2133,13 @@ fun MessageInputBar(
if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) {
Box(
modifier = Modifier
.requiredSize(82.dp)
.size(48.dp)
.graphicsLayer {
scaleX = sendScale
scaleY = sendScale
}
.shadow(
elevation = 10.dp,
elevation = 6.dp,
shape = CircleShape,
clip = false
)
@@ -2150,22 +2161,22 @@ fun MessageInputBar(
imageVector = TelegramSendIcon,
contentDescription = "Send voice message",
tint = Color.White,
modifier = Modifier.size(30.dp)
modifier = Modifier.size(22.dp)
)
}
} else {
Box(
modifier = Modifier
.requiredSize(82.dp)
.size(48.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.92f)),
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = if (recordMode == RecordMode.VOICE) Icons.Default.Mic else Icons.Default.Videocam,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(30.dp)
modifier = Modifier.size(22.dp)
)
}
}