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:
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user