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 progress = lockProgress.coerceIn(0f, 1f)
|
||||||
val lockedOrPaused = isLocked || isPaused
|
val lockedOrPaused = isLocked || isPaused
|
||||||
|
|
||||||
// Staggered snap animations (Fix #3)
|
// Staggered snap animations — Telegram timing
|
||||||
val snapRotation by animateFloatAsState(
|
val snapAnim by animateFloatAsState(
|
||||||
targetValue = if (lockedOrPaused) 1f else 0f,
|
targetValue = if (lockedOrPaused) 1f else 0f,
|
||||||
animationSpec = tween(durationMillis = 250, easing = EaseOutQuint),
|
animationSpec = tween(durationMillis = 250, easing = EaseOutQuint),
|
||||||
label = "lock_snap_rotation"
|
label = "lock_snap"
|
||||||
)
|
|
||||||
val snapTranslate by animateFloatAsState(
|
|
||||||
targetValue = if (lockedOrPaused) 1f else 0f,
|
|
||||||
animationSpec = tween(durationMillis = 350, delayMillis = 100, easing = LinearOutSlowInEasing),
|
|
||||||
label = "lock_snap_translate"
|
|
||||||
)
|
)
|
||||||
val pauseTransform by animateFloatAsState(
|
val pauseTransform by animateFloatAsState(
|
||||||
targetValue = if (lockedOrPaused) 1f else 0f,
|
targetValue = if (lockedOrPaused) 1f else 0f,
|
||||||
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
|
animationSpec = tween(durationMillis = 300, delayMillis = 150, easing = FastOutSlowInEasing),
|
||||||
label = "lock_to_pause"
|
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 bgColor = if (isDarkTheme) Color(0xFF2A2A3E) else Color(0xFFF0F0F0)
|
||||||
val iconColor = if (isDarkTheme) Color.White else Color(0xFF333333)
|
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 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(
|
val enterAlpha by animateFloatAsState(
|
||||||
targetValue = if (lockProgress > 0.01f || lockedOrPaused) 1f else 0f,
|
targetValue = 1f,
|
||||||
animationSpec = tween(durationMillis = 150),
|
animationSpec = tween(durationMillis = 200),
|
||||||
label = "lock_enter_alpha"
|
label = "lock_enter_alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = modifier
|
modifier = modifier.graphicsLayer { alpha = enterAlpha }
|
||||||
.graphicsLayer { alpha = enterAlpha }
|
|
||||||
) {
|
) {
|
||||||
val cx = size.width / 2f
|
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
|
val moveProgress = if (lockedOrPaused) 1f else progress
|
||||||
|
|
||||||
// Dual-phase snap rotation (Fix #4) — Telegram formula
|
// Telegram rotation: dual-phase snap
|
||||||
val snapRotateBackProgress = if (moveProgress > 0.4f) 1f else moveProgress / 0.4f
|
val snapRotateBack = if (moveProgress > 0.4f) 1f else moveProgress / 0.4f
|
||||||
val currentRotation = if (lockedOrPaused) {
|
val rotation = if (lockedOrPaused) {
|
||||||
9f * (1f - moveProgress) * (1f - snapRotation) -
|
9f * (1f - moveProgress) * (1f - snapAnim) -
|
||||||
15f * snapRotation * (1f - snapRotateBackProgress)
|
15f * snapAnim * (1f - snapRotateBack)
|
||||||
} else {
|
} else {
|
||||||
9f * (1f - moveProgress)
|
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(
|
drawRoundRect(
|
||||||
color = shadowColor,
|
color = shadowColor,
|
||||||
topLeft = Offset(cx - lockSize / 2f - 2f, -2f),
|
topLeft = Offset(pillLeft - 3f * dp1, pillTop - 2f * dp1),
|
||||||
size = androidx.compose.ui.geometry.Size(lockSize + 4f, lockSize * 1.38f + 4f),
|
size = androidx.compose.ui.geometry.Size(pillW + 6f * dp1, pillH + 4f * dp1),
|
||||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(lockSize / 2f + 2f)
|
cornerRadius = androidx.compose.ui.geometry.CornerRadius(pillRadius + 3f * dp1)
|
||||||
)
|
)
|
||||||
|
// Pill background
|
||||||
// Background pill
|
|
||||||
val bgAlpha = 0.7f + 0.3f * moveProgress
|
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = bgColor.copy(alpha = bgAlpha),
|
color = bgColor,
|
||||||
topLeft = Offset(cx - lockSize / 2f, 0f),
|
topLeft = Offset(pillLeft, pillTop),
|
||||||
size = androidx.compose.ui.geometry.Size(lockSize, lockSize * 1.38f),
|
size = androidx.compose.ui.geometry.Size(pillW, pillH),
|
||||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(lockSize / 2f)
|
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 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
|
// Shackle: 8dp × 8dp arc above body, stroke 1.7dp
|
||||||
if (pauseTransform > 0.01f) {
|
val shackleW = 8f * dp1
|
||||||
val gap = 3.3f * pauseTransform
|
val shackleH = 8f * dp1
|
||||||
val barW = lockSize * 0.12f
|
val shackleStroke = 1.7f * dp1
|
||||||
val barH = lockSize * 0.35f
|
val shackleLeft = bodyCx - shackleW / 2f
|
||||||
drawRoundRect(
|
val shackleTop = bodyTop - shackleH * 0.7f - 2f * dp1
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock icon (fades out as pause transform progresses)
|
val lockIconAlpha = 1f - pauseTransform
|
||||||
val lockAlpha = 1f - pauseTransform
|
val idleOffset = idlePhase * 2f * dp1 * (1f - moveProgress) // breathing on left leg
|
||||||
if (lockAlpha > 0.01f) {
|
|
||||||
rotate(degrees = currentRotation, pivot = Offset(bodyCx, bodyCy)) {
|
if (lockIconAlpha > 0.01f) {
|
||||||
val bodyW = lockSize * 0.38f
|
rotate(degrees = rotation, pivot = Offset(bodyCx, bodyCy)) {
|
||||||
val bodyH = lockSize * 0.28f
|
// Shackle arc (half circle)
|
||||||
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
|
|
||||||
drawArc(
|
drawArc(
|
||||||
color = iconColor.copy(alpha = lockAlpha),
|
color = iconColor.copy(alpha = lockIconAlpha),
|
||||||
startAngle = 180f,
|
startAngle = 180f,
|
||||||
sweepAngle = 180f,
|
sweepAngle = 180f,
|
||||||
useCenter = false,
|
useCenter = false,
|
||||||
topLeft = Offset(bodyCx - shackleW / 2f, bodyCy - bodyH / 4f - shackleH),
|
topLeft = Offset(shackleLeft, shackleTop),
|
||||||
size = androidx.compose.ui.geometry.Size(shackleW, shackleH),
|
size = androidx.compose.ui.geometry.Size(shackleW, shackleH),
|
||||||
style = androidx.compose.ui.graphics.drawscope.Stroke(
|
style = androidx.compose.ui.graphics.drawscope.Stroke(
|
||||||
width = shackleStroke,
|
width = shackleStroke,
|
||||||
cap = androidx.compose.ui.graphics.StrokeCap.Round
|
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