From aa3cc766461232603cbcb68f924310790ded92ab Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 11 Apr 2026 23:57:10 +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=20SlideToCancel=201:1=20=D1=81=20Teleg?= =?UTF-8?q?ram=20=E2=80=94=20chevron=20arrow,=20=D0=BF=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=D1=81=D0=B0=D1=86=D0=B8=D1=8F,=20entry=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telegram-exact SlideTextView: - Chevron arrow: Canvas-drawn path 4×5dp, stroke 1.6dp, round caps (не текст ◀) - Пульсация: ±6dp ТОЛЬКО при slideProgress > 0.8, скорость 12dp/s (3dp/250ms) - Frame-based animation через LaunchedEffect (не infiniteTransition) - Entry: slide in from right (translationX 20dp→0, 200ms) + fade in - Текст: "Slide to cancel" 15sp normal weight (было 13sp medium) - Цвет: #8E8E93 (Telegram key_chat_recordTime) - Translation: finger × 0.3 damping + pulse offset × slideProgress - Alpha: slideProgress × entryAlpha (плавно появляется и исчезает при свайпе) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ui/chats/input/ChatDetailInput.kt | 140 ++++++++++++++---- 1 file changed, 113 insertions(+), 27 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 308a277..b219512 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 @@ -569,6 +569,19 @@ private fun LockIcon( } } +/** + * Telegram-exact SlideToCancel. + * + * Layout: [chevron arrow] "Slide to cancel" + * + * - Arrow is a Canvas-drawn chevron (4×5dp, stroke 1.6dp, round caps) + * - Arrow oscillates ±6dp ONLY when slideProgress > 0.8 at 12dp/s + * - Text alpha = slideProgress (fades in with drag, 0 = invisible at rest) + * - Translation follows finger × 0.3 damping + * - Entry: slides in from right (translationX 20dp→0) with fade + * + * Reference: ChatActivityEnterView.SlideTextView (lines 13083-13357) + */ @Composable private fun SlideToCancel( slideDx: Float, @@ -576,46 +589,119 @@ private fun SlideToCancel( isDarkTheme: Boolean, modifier: Modifier = Modifier ) { - val slideRatio = ((-slideDx) / cancelThresholdPx).coerceIn(0f, 1f) - val textAlpha = if (slideRatio > 0.7f) 1f - ((slideRatio - 0.7f) / 0.3f) else 1f + // slideProgress: 1.0 = at rest, decreases as finger drags left toward cancel + val slideProgress = 1f - ((-slideDx) / cancelThresholdPx).coerceIn(0f, 1f) - // Fix #5: Arrow pulsation ±6dp when slideRatio > 0.8, else ±3dp - val arrowAmplitude = if (slideRatio > 0.8f) -6f else -3f - val arrowPulse = rememberInfiniteTransition(label = "slide_cancel_arrow") - val arrowOffset by arrowPulse.animateFloat( - initialValue = 0f, - targetValue = arrowAmplitude, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 500, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), - label = "slide_cancel_arrow_offset" + val density = LocalDensity.current + + // Pre-compute px values for use in LaunchedEffect + val maxOffsetPx = with(density) { 6.dp.toPx() } + val speedPxPerMs = with(density) { (3f / 250f).dp.toPx() } // 12dp/s + + // Telegram: arrow oscillates ±6dp only when slideProgress > 0.8 + var xOffset by remember { mutableFloatStateOf(0f) } + var moveForward by remember { mutableStateOf(true) } + + LaunchedEffect(slideProgress > 0.8f) { + if (slideProgress <= 0.8f) { + xOffset = 0f + moveForward = true + return@LaunchedEffect + } + var lastTime = System.nanoTime() + while (true) { + delay(16) // ~60fps + val now = System.nanoTime() + val dtMs = (now - lastTime) / 1_000_000f + lastTime = now + + val step = speedPxPerMs * dtMs + if (moveForward) { + xOffset += step + if (xOffset > maxOffsetPx) { + xOffset = maxOffsetPx + moveForward = false + } + } else { + xOffset -= step + if (xOffset < -maxOffsetPx) { + xOffset = -maxOffsetPx + moveForward = true + } + } + } + } + + // Colors — Telegram: key_chat_recordTime (gray), key_glass_defaultIcon (arrow) + val textColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93) + val arrowColor = if (isDarkTheme) Color(0xFFAAAAAA) else Color(0xFF8E8E93) + + // Entry animation: slide in from right + var entered by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + entered = true + } + val entryTranslation by animateFloatAsState( + targetValue = if (entered) 0f else with(density) { 20.dp.toPx() }, + animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing), + label = "slide_cancel_entry" + ) + val entryAlpha by animateFloatAsState( + targetValue = if (entered) 1f else 0f, + animationSpec = tween(durationMillis = 200), + label = "slide_cancel_entry_alpha" ) - val textColor = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.4f) - - // Fix #6: damping 0.3 (Telegram-style) Row( modifier = modifier .graphicsLayer { - translationX = slideDx * 0.3f - alpha = textAlpha + // Telegram: text follows finger × damping + entry slide + pulse offset + translationX = slideDx * 0.3f + entryTranslation + + xOffset * slideProgress + alpha = slideProgress * entryAlpha }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Text( - text = "◀", - color = textColor, - fontSize = 13.sp, - modifier = Modifier.graphicsLayer { translationX = arrowOffset } - ) + // Chevron arrow — Canvas-drawn, NOT text character + // Telegram: 4dp × 5dp chevron, stroke 1.6dp, round caps + Canvas( + modifier = Modifier + .size(width = 10.dp, height = 14.dp) + .graphicsLayer { + translationX = xOffset * slideProgress + } + ) { + val midY = size.height / 2f + val arrowW = 4.dp.toPx() + val arrowH = 5.dp.toPx() + val strokeW = 1.6f.dp.toPx() + val startX = (size.width - arrowW) / 2f + + drawLine( + color = arrowColor, + start = Offset(startX + arrowW, midY - arrowH), + end = Offset(startX, midY), + strokeWidth = strokeW, + cap = androidx.compose.ui.graphics.StrokeCap.Round + ) + drawLine( + color = arrowColor, + start = Offset(startX, midY), + end = Offset(startX + arrowW, midY + arrowH), + strokeWidth = strokeW, + cap = androidx.compose.ui.graphics.StrokeCap.Round + ) + } + Spacer(modifier = Modifier.width(4.dp)) + + // "Slide to cancel" text — Telegram: 15sp, normal weight Text( - text = "Slide to Cancel", + text = "Slide to cancel", color = textColor, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + fontSize = 15.sp, + fontWeight = FontWeight.Normal, maxLines = 1 ) }