fix: LOCKED panel 1:1 с Telegram — полностью другой layout при lock
Telegram при LOCKED: таймер и dot СКРЫТЫ, вместо них: - [Delete 44dp] — красная иконка удаления слева - [Waveform] — заполняет оставшееся место - Lock→Pause кнопка наверху (отдельный overlay) - Circle = Send (без blob) При RECORDING (без изменений): - [dot][timer] [◀ Slide to cancel] [Circle+Blob] Реализация: AnimatedContent crossfade между двумя полностью разными panel layouts. RecordLockedControls больше не используется в панели — delete в самой панели, pause в LockIcon overlay. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2095,9 +2095,11 @@ fun MessageInputBar(
|
||||
label = "record_ui_shift"
|
||||
)
|
||||
|
||||
// ── Telegram-style recording layout ──
|
||||
// Telegram uses separate overlay layers (RecordCircle 194dp, ControlsView 250dp)
|
||||
// We replicate with Box layers + graphicsLayer for overflow
|
||||
// ── Telegram-exact recording layout ──
|
||||
// RECORDING: [dot][timer] [◀ Slide to cancel] ... [Circle+Blob]
|
||||
// LOCKED: [Delete] [Waveform 32dp] ... [Circle=Send] + Lock→Pause above
|
||||
// PAUSED: [Delete] [Waveform 32dp] ... [Circle=Send] + Lock→Play above
|
||||
// Timer and dot are HIDDEN in LOCKED/PAUSED (Telegram exact)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -2108,93 +2110,103 @@ fun MessageInputBar(
|
||||
recordingInputRowY = coordinates.positionInWindow().y
|
||||
}
|
||||
) {
|
||||
// ── Layer 1: Panel bar (timer + center) ──
|
||||
// Telegram: full-width bar, circle overlaps right edge
|
||||
Row(
|
||||
val isLockedOrPaused = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED
|
||||
|
||||
// Crossfade between RECORDING panel and LOCKED panel
|
||||
AnimatedContent(
|
||||
targetState = isLockedOrPaused,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(recordingPanelColor)
|
||||
.padding(start = 13.dp, end = 52.dp), // 52dp = half-circle overlap
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Blink dot + timer
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (recordUiState == RecordUiState.PAUSED) {
|
||||
Box(modifier = Modifier.size(28.dp), contentAlignment = Alignment.Center) {
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape)
|
||||
.background(if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D)))
|
||||
}
|
||||
} else {
|
||||
RecordBlinkDot(isDarkTheme = isDarkTheme)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = formatVoiceRecordTimer(voiceElapsedMs),
|
||||
color = recordingTextColor,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Center: SlideToCancel or Waveform+Controls
|
||||
AnimatedContent(
|
||||
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
},
|
||||
transitionSpec = {
|
||||
fadeIn(tween(200)) togetherWith fadeOut(tween(200))
|
||||
},
|
||||
label = "record_center_content"
|
||||
) { isLockedOrPaused ->
|
||||
if (isLockedOrPaused) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
VoiceWaveformBar(
|
||||
waves = voiceWaves,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
RecordLockedControls(
|
||||
isPaused = recordUiState == RecordUiState.PAUSED,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onDelete = {
|
||||
.height(44.dp)
|
||||
.padding(end = 52.dp), // space for circle overlay
|
||||
transitionSpec = {
|
||||
fadeIn(tween(200)) togetherWith fadeOut(tween(200))
|
||||
},
|
||||
label = "record_panel_mode"
|
||||
) { locked ->
|
||||
if (locked) {
|
||||
// ── LOCKED/PAUSED panel (Telegram: recordedAudioPanel) ──
|
||||
// [Delete 44dp] [Waveform fills rest]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(22.dp))
|
||||
.background(recordingPanelColor),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Delete button — Telegram: 44×44dp, Lottie trash icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
inputJumpLog("tap DELETE (locked/paused) mode=$recordMode state=$recordUiState")
|
||||
stopVoiceRecording(send = false)
|
||||
},
|
||||
onTogglePause = {
|
||||
inputJumpLog("tap PAUSE/RESUME mode=$recordMode state=$recordUiState")
|
||||
if (recordUiState == RecordUiState.PAUSED) {
|
||||
resumeVoiceRecording()
|
||||
} else {
|
||||
pauseVoiceRecording()
|
||||
}
|
||||
}
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Delete recording",
|
||||
tint = if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
// Waveform — Telegram: 32dp height, fills remaining width
|
||||
VoiceWaveformBar(
|
||||
waves = voiceWaves,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// ── RECORDING panel ──
|
||||
// [dot][timer] [◀ Slide to cancel]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(22.dp))
|
||||
.background(recordingPanelColor)
|
||||
.padding(start = 13.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Blink dot + timer
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RecordBlinkDot(isDarkTheme = isDarkTheme)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = formatVoiceRecordTimer(voiceElapsedMs),
|
||||
color = recordingTextColor,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Slide to cancel
|
||||
SlideToCancel(
|
||||
slideDx = slideDx,
|
||||
cancelThresholdPx = cancelDragThresholdPx,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user