fix: переписать SlideToCancel 1:1 с Telegram — chevron arrow, пульсация, entry animation
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user