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