fix: переписать recording layout 1:1 с Telegram — правильные пропорции и overlay
Telegram dimensions: - Circle: 48dp layout → 82dp visual (scale 1.71x), как circleRadius=41dp - Lock: 50dp→36dp pill, 70dp выше центра круга - Panel bar: full width Row с end=52dp для overlap - Blob: 1.7x scale = 82dp visual (Telegram blob minRadius) - Controls: 36dp (delete + pause) - Tooltip: 90dp левее, 70dp выше Layout architecture: - Layer 1: Panel bar (Row с clip RoundedCornerShape) - Layer 2: Circle overlay (graphicsLayer scale, NO clip) - Layer 3: Lock overlay (graphicsLayer translationY, NO clip) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1956,7 +1956,9 @@ fun MessageInputBar(
|
|||||||
label = "record_ui_shift"
|
label = "record_ui_shift"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Outer Box — no clip, allows children to overflow
|
// ── Telegram-style recording layout ──
|
||||||
|
// Telegram uses separate overlay layers (RecordCircle 194dp, ControlsView 250dp)
|
||||||
|
// We replicate with Box layers + graphicsLayer for overflow
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -1967,20 +1969,20 @@ fun MessageInputBar(
|
|||||||
recordingInputRowY = coordinates.positionInWindow().y
|
recordingInputRowY = coordinates.positionInWindow().y
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// ── Layer 1: Panel bar ──
|
// ── Layer 1: Panel bar (timer + center) ──
|
||||||
Box(
|
// Telegram: full-width bar, circle overlaps right edge
|
||||||
|
Row(
|
||||||
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(horizontal = 13.dp)
|
.padding(start = 13.dp, end = 52.dp), // 52dp = half-circle overlap
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Left: blink dot + timer
|
// Blink dot + timer
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
alpha = recordUiAlpha
|
alpha = recordUiAlpha
|
||||||
translationX = with(density) { recordUiShift.toPx() }
|
translationX = with(density) { recordUiShift.toPx() }
|
||||||
@@ -1988,18 +1990,9 @@ fun MessageInputBar(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (recordUiState == RecordUiState.PAUSED) {
|
if (recordUiState == RecordUiState.PAUSED) {
|
||||||
Box(
|
Box(modifier = Modifier.size(28.dp), contentAlignment = Alignment.Center) {
|
||||||
modifier = Modifier.size(28.dp),
|
Box(modifier = Modifier.size(10.dp).clip(CircleShape)
|
||||||
contentAlignment = Alignment.Center
|
.background(if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D)))
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(10.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
|
||||||
if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RecordBlinkDot(isDarkTheme = isDarkTheme)
|
RecordBlinkDot(isDarkTheme = isDarkTheme)
|
||||||
@@ -2013,11 +2006,13 @@ fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center content: SlideToCancel or Waveform+Controls
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// Center: SlideToCancel or Waveform+Controls
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
|
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.weight(1f)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
alpha = recordUiAlpha
|
alpha = recordUiAlpha
|
||||||
translationX = with(density) { recordUiShift.toPx() }
|
translationX = with(density) { recordUiShift.toPx() }
|
||||||
@@ -2066,22 +2061,24 @@ fun MessageInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Layer 2: Mic/Send circle overlay ──
|
// ── Layer 2: Circle + Lock overlay ──
|
||||||
// Positioned at right edge, overlapping the panel
|
// 48dp layout box at right edge; visuals overflow via graphicsLayer
|
||||||
|
// Telegram: circle center at 26dp from right, radius 41dp = 82dp visual
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
|
.offset(x = 4.dp) // slight overlap into right padding
|
||||||
.zIndex(5f),
|
.zIndex(5f),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// ── Layer 3: LockIcon above circle ──
|
// Lock icon: floats ~70dp above circle center (Telegram: ~92dp)
|
||||||
if (recordUiState == RecordUiState.RECORDING ||
|
if (recordUiState == RecordUiState.RECORDING ||
|
||||||
recordUiState == RecordUiState.LOCKED ||
|
recordUiState == RecordUiState.LOCKED ||
|
||||||
recordUiState == RecordUiState.PAUSED
|
recordUiState == RecordUiState.PAUSED
|
||||||
) {
|
) {
|
||||||
val lockSizeDp = (50.dp - 14.dp * lockProgress)
|
val lockSizeDp = 50.dp - 14.dp * lockProgress
|
||||||
val lockYOffset = ((-56).dp + 14.dp * lockProgress)
|
val lockYDp = -70.dp + 14.dp * lockProgress
|
||||||
LockIcon(
|
LockIcon(
|
||||||
lockProgress = lockProgress,
|
lockProgress = lockProgress,
|
||||||
isLocked = recordUiState == RecordUiState.LOCKED,
|
isLocked = recordUiState == RecordUiState.LOCKED,
|
||||||
@@ -2090,7 +2087,7 @@ fun MessageInputBar(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(lockSizeDp)
|
.size(lockSizeDp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationY = with(density) { lockYOffset.toPx() }
|
translationY = with(density) { lockYDp.toPx() }
|
||||||
clip = false
|
clip = false
|
||||||
}
|
}
|
||||||
.zIndex(10f)
|
.zIndex(10f)
|
||||||
@@ -2102,8 +2099,8 @@ fun MessageInputBar(
|
|||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationX = with(density) { (-80).dp.toPx() }
|
translationX = with(density) { (-90).dp.toPx() }
|
||||||
translationY = with(density) { (-56).dp.toPx() }
|
translationY = with(density) { (-70).dp.toPx() }
|
||||||
clip = false
|
clip = false
|
||||||
}
|
}
|
||||||
.zIndex(11f)
|
.zIndex(11f)
|
||||||
@@ -2111,38 +2108,37 @@ fun MessageInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blob animation (visual-only enlargement)
|
// Blob: 48dp base → 1.7x = ~82dp visual (matches Telegram circleRadius 41dp)
|
||||||
VoiceButtonBlob(
|
VoiceButtonBlob(
|
||||||
voiceLevel = voiceLevel,
|
voiceLevel = voiceLevel,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = 1.8f
|
scaleX = 1.7f
|
||||||
scaleY = 1.8f
|
scaleY = 1.7f
|
||||||
clip = false
|
clip = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send or Mic button
|
// Solid circle: 48dp layout, scaled to 82dp visual
|
||||||
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),
|
||||||
label = "send_btn_scale"
|
label = "send_btn_scale"
|
||||||
)
|
)
|
||||||
|
val circleScale = 1.71f // 48dp * 1.71 ≈ 82dp (Telegram)
|
||||||
if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) {
|
if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = sendScale
|
scaleX = circleScale * sendScale
|
||||||
scaleY = sendScale
|
scaleY = circleScale * sendScale
|
||||||
}
|
|
||||||
.shadow(
|
|
||||||
elevation = 6.dp,
|
|
||||||
shape = CircleShape,
|
|
||||||
clip = false
|
clip = false
|
||||||
)
|
shadowElevation = 8f
|
||||||
|
shape = CircleShape
|
||||||
|
}
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(PrimaryBlue)
|
.background(PrimaryBlue)
|
||||||
.clickable(
|
.clickable(
|
||||||
@@ -2161,13 +2157,18 @@ fun MessageInputBar(
|
|||||||
imageVector = TelegramSendIcon,
|
imageVector = TelegramSendIcon,
|
||||||
contentDescription = "Send voice message",
|
contentDescription = "Send voice message",
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = circleScale
|
||||||
|
scaleY = circleScale
|
||||||
|
clip = false
|
||||||
|
}
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(PrimaryBlue),
|
.background(PrimaryBlue),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -2176,7 +2177,7 @@ fun MessageInputBar(
|
|||||||
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(22.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user