fix: переписать LockIcon 1:1 с Telegram — правильный замок с keyhole и idle animation
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user