From 946ba7838cb42e7850923e97f82e98abd1752a7e Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 11 Apr 2026 22:01:53 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D1=82=D1=8C=20LockIcon=201:1=20=D1=81=20Telegram?= =?UTF-8?q?=20=E2=80=94=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=BC=D0=BE=D0=BA=20=D1=81=20keyhol?= =?UTF-8?q?e=20=D0=B8=20idle=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telegram-exact lock icon: - Body: 16×16dp прямоугольник, radius 3dp (заливка) - Shackle: 8×8dp полукруг (stroke 1.7dp) + две ножки - Левая ножка: idle "breathing" animation (1.2s cycle) - Левая ножка: удлиняется при snap lock - Keyhole: 4dp точка в центре body (цвет фона) - Pause transform: body раздваивается с gap 1.66dp (Telegram exact) - Pill background: 36×50dp с тенью - Lock виден сразу при начале записи (не ждёт свайпа) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ui/chats/input/ChatDetailInput.kt | 178 +++++++++++------- 1 file changed, 112 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt index 7f2aed8..308a277 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt @@ -408,118 +408,164 @@ private fun LockIcon( val progress = lockProgress.coerceIn(0f, 1f) val lockedOrPaused = isLocked || isPaused - // Staggered snap animations (Fix #3) - val snapRotation by animateFloatAsState( + // Staggered snap animations — Telegram timing + val snapAnim by animateFloatAsState( targetValue = if (lockedOrPaused) 1f else 0f, animationSpec = tween(durationMillis = 250, easing = EaseOutQuint), - label = "lock_snap_rotation" - ) - val snapTranslate by animateFloatAsState( - targetValue = if (lockedOrPaused) 1f else 0f, - animationSpec = tween(durationMillis = 350, delayMillis = 100, easing = LinearOutSlowInEasing), - label = "lock_snap_translate" + label = "lock_snap" ) val pauseTransform by animateFloatAsState( targetValue = if (lockedOrPaused) 1f else 0f, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + animationSpec = tween(durationMillis = 300, delayMillis = 150, easing = FastOutSlowInEasing), label = "lock_to_pause" ) + // Idle "breathing" animation for shackle + val idlePhase by rememberInfiniteTransition(label = "lock_idle").animateFloat( + initialValue = 0f, targetValue = 1f, + animationSpec = infiniteRepeatable(tween(1200, easing = LinearEasing), RepeatMode.Reverse), + label = "lock_idle_phase" + ) val bgColor = if (isDarkTheme) Color(0xFF2A2A3E) else Color(0xFFF0F0F0) val iconColor = if (isDarkTheme) Color.White else Color(0xFF333333) val shadowColor = if (isDarkTheme) Color.Black.copy(alpha = 0.3f) else Color.Black.copy(alpha = 0.15f) + val bgPaintColor = if (isDarkTheme) Color(0xFF3A3A4E) else Color(0xFFE8E8E8) + // Lock is always visible during recording (Telegram shows it immediately) val enterAlpha by animateFloatAsState( - targetValue = if (lockProgress > 0.01f || lockedOrPaused) 1f else 0f, - animationSpec = tween(durationMillis = 150), + targetValue = 1f, + animationSpec = tween(durationMillis = 200), label = "lock_enter_alpha" ) Canvas( - modifier = modifier - .graphicsLayer { alpha = enterAlpha } + modifier = modifier.graphicsLayer { alpha = enterAlpha } ) { val cx = size.width / 2f - val lockSize = size.minDimension + val dp1 = size.width / 36f // normalize to 36dp base val moveProgress = if (lockedOrPaused) 1f else progress - // Dual-phase snap rotation (Fix #4) — Telegram formula - val snapRotateBackProgress = if (moveProgress > 0.4f) 1f else moveProgress / 0.4f - val currentRotation = if (lockedOrPaused) { - 9f * (1f - moveProgress) * (1f - snapRotation) - - 15f * snapRotation * (1f - snapRotateBackProgress) + // Telegram rotation: dual-phase snap + val snapRotateBack = if (moveProgress > 0.4f) 1f else moveProgress / 0.4f + val rotation = if (lockedOrPaused) { + 9f * (1f - moveProgress) * (1f - snapAnim) - + 15f * snapAnim * (1f - snapRotateBack) } else { 9f * (1f - moveProgress) } - // Shadow first + // ── Background pill with shadow ── + val pillW = 36f * dp1 + val pillH = 50f * dp1 + val pillLeft = cx - pillW / 2f + val pillTop = 0f + val pillRadius = pillW / 2f + + // Shadow drawRoundRect( color = shadowColor, - topLeft = Offset(cx - lockSize / 2f - 2f, -2f), - size = androidx.compose.ui.geometry.Size(lockSize + 4f, lockSize * 1.38f + 4f), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(lockSize / 2f + 2f) + topLeft = Offset(pillLeft - 3f * dp1, pillTop - 2f * dp1), + size = androidx.compose.ui.geometry.Size(pillW + 6f * dp1, pillH + 4f * dp1), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(pillRadius + 3f * dp1) ) - - // Background pill - val bgAlpha = 0.7f + 0.3f * moveProgress + // Pill background drawRoundRect( - color = bgColor.copy(alpha = bgAlpha), - topLeft = Offset(cx - lockSize / 2f, 0f), - size = androidx.compose.ui.geometry.Size(lockSize, lockSize * 1.38f), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(lockSize / 2f) + color = bgColor, + topLeft = Offset(pillLeft, pillTop), + size = androidx.compose.ui.geometry.Size(pillW, pillH), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(pillRadius) ) + // ── Lock icon drawing (Telegram-exact) ── + // Body: 16dp × 16dp centered, with corner radius 3dp + val bodyW = 16f * dp1 + val bodyH = 16f * dp1 + val bodyRadius = 3f * dp1 val bodyCx = cx - val bodyCy = lockSize * 0.78f + val bodyCy = pillTop + pillH * 0.62f // body center in lower part of pill + val bodyLeft = bodyCx - bodyW / 2f + val bodyTop = bodyCy - bodyH / 2f - // Transform to pause: body splits into two bars - if (pauseTransform > 0.01f) { - val gap = 3.3f * pauseTransform - val barW = lockSize * 0.12f - val barH = lockSize * 0.35f - drawRoundRect( - color = iconColor, - topLeft = Offset(bodyCx - gap - barW, bodyCy - barH / 2f), - size = androidx.compose.ui.geometry.Size(barW, barH), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(barW / 4f) - ) - drawRoundRect( - color = iconColor, - topLeft = Offset(bodyCx + gap, bodyCy - barH / 2f), - size = androidx.compose.ui.geometry.Size(barW, barH), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(barW / 4f) - ) - } + // Shackle: 8dp × 8dp arc above body, stroke 1.7dp + val shackleW = 8f * dp1 + val shackleH = 8f * dp1 + val shackleStroke = 1.7f * dp1 + val shackleLeft = bodyCx - shackleW / 2f + val shackleTop = bodyTop - shackleH * 0.7f - 2f * dp1 - // Lock icon (fades out as pause transform progresses) - val lockAlpha = 1f - pauseTransform - if (lockAlpha > 0.01f) { - rotate(degrees = currentRotation, pivot = Offset(bodyCx, bodyCy)) { - val bodyW = lockSize * 0.38f - val bodyH = lockSize * 0.28f - drawRoundRect( - color = iconColor.copy(alpha = lockAlpha), - topLeft = Offset(bodyCx - bodyW / 2f, bodyCy - bodyH / 4f), - size = androidx.compose.ui.geometry.Size(bodyW, bodyH), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(lockSize * 0.05f) - ) - val shackleW = lockSize * 0.26f - val shackleH = lockSize * 0.22f - val shackleStroke = lockSize * 0.047f + val lockIconAlpha = 1f - pauseTransform + val idleOffset = idlePhase * 2f * dp1 * (1f - moveProgress) // breathing on left leg + + if (lockIconAlpha > 0.01f) { + rotate(degrees = rotation, pivot = Offset(bodyCx, bodyCy)) { + // Shackle arc (half circle) drawArc( - color = iconColor.copy(alpha = lockAlpha), + color = iconColor.copy(alpha = lockIconAlpha), startAngle = 180f, sweepAngle = 180f, useCenter = false, - topLeft = Offset(bodyCx - shackleW / 2f, bodyCy - bodyH / 4f - shackleH), + topLeft = Offset(shackleLeft, shackleTop), size = androidx.compose.ui.geometry.Size(shackleW, shackleH), style = androidx.compose.ui.graphics.drawscope.Stroke( width = shackleStroke, cap = androidx.compose.ui.graphics.StrokeCap.Round ) ) + // Right leg (fixed) + drawLine( + color = iconColor.copy(alpha = lockIconAlpha), + start = Offset(shackleLeft + shackleW, shackleTop + shackleH / 2f), + end = Offset(shackleLeft + shackleW, bodyTop + 2f * dp1), + strokeWidth = shackleStroke, + cap = androidx.compose.ui.graphics.StrokeCap.Round + ) + // Left leg (animated — idle breathing + lock closing) + val leftLegEnd = bodyTop + 2f * dp1 + idleOffset + + 4f * dp1 * snapAnim * (1f - moveProgress) + drawLine( + color = iconColor.copy(alpha = lockIconAlpha), + start = Offset(shackleLeft, shackleTop + shackleH / 2f), + end = Offset(shackleLeft, leftLegEnd), + strokeWidth = shackleStroke, + cap = androidx.compose.ui.graphics.StrokeCap.Round + ) + + // Body (filled rounded rect) + drawRoundRect( + color = iconColor.copy(alpha = lockIconAlpha), + topLeft = Offset(bodyLeft, bodyTop), + size = androidx.compose.ui.geometry.Size(bodyW, bodyH), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(bodyRadius) + ) + + // Keyhole dot (Telegram: dpf2(2) radius at center) + drawCircle( + color = bgPaintColor.copy(alpha = lockIconAlpha), + radius = 2f * dp1, + center = Offset(bodyCx, bodyCy) + ) } } + + // ── Pause transform: body splits into two bars ── + if (pauseTransform > 0.01f) { + val gap = 1.66f * dp1 * pauseTransform + val barW = 4f * dp1 + val barH = 14f * dp1 + val barRadius = 1.5f * dp1 + drawRoundRect( + color = iconColor.copy(alpha = pauseTransform), + topLeft = Offset(bodyCx - gap - barW, bodyCy - barH / 2f), + size = androidx.compose.ui.geometry.Size(barW, barH), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(barRadius) + ) + drawRoundRect( + color = iconColor.copy(alpha = pauseTransform), + topLeft = Offset(bodyCx + gap, bodyCy - barH / 2f), + size = androidx.compose.ui.geometry.Size(barW, barH), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(barRadius) + ) + } } }