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"
|
||||
)
|
||||
|
||||
// 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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1967,20 +1969,20 @@ fun MessageInputBar(
|
||||
recordingInputRowY = coordinates.positionInWindow().y
|
||||
}
|
||||
) {
|
||||
// ── Layer 1: Panel bar ──
|
||||
Box(
|
||||
// ── Layer 1: Panel bar (timer + center) ──
|
||||
// Telegram: full-width bar, circle overlaps right edge
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.padding(end = 44.dp) // space for circle overlap
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.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(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
@@ -1988,18 +1990,9 @@ fun MessageInputBar(
|
||||
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)
|
||||
)
|
||||
)
|
||||
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)
|
||||
@@ -2013,11 +2006,13 @@ fun MessageInputBar(
|
||||
)
|
||||
}
|
||||
|
||||
// Center content: SlideToCancel or Waveform+Controls
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Center: SlideToCancel or Waveform+Controls
|
||||
AnimatedContent(
|
||||
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.weight(1f)
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
@@ -2066,22 +2061,24 @@ fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Layer 2: Mic/Send circle overlay ──
|
||||
// Positioned at right edge, overlapping the panel
|
||||
// ── Layer 2: Circle + Lock overlay ──
|
||||
// 48dp layout box at right edge; visuals overflow via graphicsLayer
|
||||
// Telegram: circle center at 26dp from right, radius 41dp = 82dp visual
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.offset(x = 4.dp) // slight overlap into right padding
|
||||
.zIndex(5f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// ── Layer 3: LockIcon above circle ──
|
||||
// Lock icon: floats ~70dp above circle center (Telegram: ~92dp)
|
||||
if (recordUiState == RecordUiState.RECORDING ||
|
||||
recordUiState == RecordUiState.LOCKED ||
|
||||
recordUiState == RecordUiState.PAUSED
|
||||
) {
|
||||
val lockSizeDp = (50.dp - 14.dp * lockProgress)
|
||||
val lockYOffset = ((-56).dp + 14.dp * lockProgress)
|
||||
val lockSizeDp = 50.dp - 14.dp * lockProgress
|
||||
val lockYDp = -70.dp + 14.dp * lockProgress
|
||||
LockIcon(
|
||||
lockProgress = lockProgress,
|
||||
isLocked = recordUiState == RecordUiState.LOCKED,
|
||||
@@ -2090,7 +2087,7 @@ fun MessageInputBar(
|
||||
modifier = Modifier
|
||||
.size(lockSizeDp)
|
||||
.graphicsLayer {
|
||||
translationY = with(density) { lockYOffset.toPx() }
|
||||
translationY = with(density) { lockYDp.toPx() }
|
||||
clip = false
|
||||
}
|
||||
.zIndex(10f)
|
||||
@@ -2102,8 +2099,8 @@ fun MessageInputBar(
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
translationX = with(density) { (-80).dp.toPx() }
|
||||
translationY = with(density) { (-56).dp.toPx() }
|
||||
translationX = with(density) { (-90).dp.toPx() }
|
||||
translationY = with(density) { (-70).dp.toPx() }
|
||||
clip = false
|
||||
}
|
||||
.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(
|
||||
voiceLevel = voiceLevel,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = 1.8f
|
||||
scaleY = 1.8f
|
||||
scaleX = 1.7f
|
||||
scaleY = 1.7f
|
||||
clip = false
|
||||
}
|
||||
)
|
||||
|
||||
// Send or Mic button
|
||||
// Solid circle: 48dp layout, scaled to 82dp visual
|
||||
val sendScale by animateFloatAsState(
|
||||
targetValue = if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 150, easing = FastOutSlowInEasing),
|
||||
label = "send_btn_scale"
|
||||
)
|
||||
val circleScale = 1.71f // 48dp * 1.71 ≈ 82dp (Telegram)
|
||||
if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = sendScale
|
||||
scaleY = sendScale
|
||||
}
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = CircleShape,
|
||||
scaleX = circleScale * sendScale
|
||||
scaleY = circleScale * sendScale
|
||||
clip = false
|
||||
)
|
||||
shadowElevation = 8f
|
||||
shape = CircleShape
|
||||
}
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue)
|
||||
.clickable(
|
||||
@@ -2161,13 +2157,18 @@ fun MessageInputBar(
|
||||
imageVector = TelegramSendIcon,
|
||||
contentDescription = "Send voice message",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = circleScale
|
||||
scaleY = circleScale
|
||||
clip = false
|
||||
}
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -2176,7 +2177,7 @@ fun MessageInputBar(
|
||||
imageVector = if (recordMode == RecordMode.VOICE) Icons.Default.Mic else Icons.Default.Videocam,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user