feat: implement Telegram-style swipe functionality and animation enhancements

This commit is contained in:
2026-02-07 02:38:19 +05:00
parent 76ad853f79
commit ec4259492b
5 changed files with 142 additions and 117 deletions

View File

@@ -23,11 +23,11 @@ import kotlinx.coroutines.launch
// Telegram's CubicBezierInterpolator(0.25, 0.1, 0.25, 1.0)
private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
// Swipe-back thresholds
private const val COMPLETION_THRESHOLD = 0.3f // 30% of screen width (was 50%)
private const val FLING_VELOCITY_THRESHOLD = 400f // px/s (was 600)
// Swipe-back thresholds (Telegram-like)
private const val COMPLETION_THRESHOLD = 0.2f // 20% of screen width — very easy to complete
private const val FLING_VELOCITY_THRESHOLD = 150f // px/s — very sensitive to flings
private const val ANIMATION_DURATION_ENTER = 300
private const val ANIMATION_DURATION_EXIT = 250
private const val ANIMATION_DURATION_EXIT = 200
private const val EDGE_ZONE_DP = 200
/**
@@ -148,6 +148,7 @@ fun SwipeBackContainer(
if (swipeEnabled && !isAnimatingIn && !isAnimatingOut) {
Modifier.pointerInput(Unit) {
val velocityTracker = VelocityTracker()
val touchSlop = viewConfiguration.touchSlop
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
@@ -159,35 +160,54 @@ fun SwipeBackContainer(
velocityTracker.resetTracking()
var startedSwipe = false
var totalDragX = 0f
var totalDragY = 0f
var passedSlop = false
try {
horizontalDrag(down.id) { change ->
val dragAmount = change.positionChange().x
// Use Initial pass to intercept BEFORE children
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
val change = event.changes.firstOrNull { it.id == down.id }
?: break
// Only start swipe if moving right
if (!startedSwipe && dragAmount > 0) {
if (change.changedToUpIgnoreConsumed()) {
break
}
val dragDelta = change.positionChange()
totalDragX += dragDelta.x
totalDragY += dragDelta.y
if (!passedSlop) {
val totalDistance = kotlin.math.sqrt(totalDragX * totalDragX + totalDragY * totalDragY)
if (totalDistance < touchSlop) continue
// Slop exceeded — only claim rightward + mostly horizontal
if (totalDragX > 0 && kotlin.math.abs(totalDragX) > kotlin.math.abs(totalDragY) * 1.5f) {
passedSlop = true
startedSwipe = true
isDragging = true
dragOffset = offsetAnimatable.value
// 🔥 Скрываем клавиатуру при начале свайпа (надёжный метод)
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
focusManager.clearFocus()
}
if (startedSwipe) {
// Direct state update - NO coroutines!
dragOffset = (dragOffset + dragAmount)
.coerceIn(0f, screenWidthPx)
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
change.consume()
} else {
// Vertical or leftward — let children handle
break
}
} else {
// We own the gesture — update drag
dragOffset = (dragOffset + dragDelta.x)
.coerceIn(0f, screenWidthPx)
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
change.consume()
}
} catch (_: Exception) {
// Gesture was cancelled
}
// Handle drag end
@@ -196,13 +216,12 @@ fun SwipeBackContainer(
val velocity = velocityTracker.calculateVelocity().x
val currentProgress = dragOffset / screenWidthPx
// Telegram logic: fling OR 50% threshold
val shouldComplete =
velocity > FLING_VELOCITY_THRESHOLD ||
currentProgress > 0.5f || // Past 50% — always complete
velocity > FLING_VELOCITY_THRESHOLD || // Fast fling right
(currentProgress > COMPLETION_THRESHOLD &&
velocity > -FLING_VELOCITY_THRESHOLD)
velocity > -FLING_VELOCITY_THRESHOLD) // 20%+ and not flinging back
// Sync animatable with current drag position and animate
scope.launch {
offsetAnimatable.snapTo(dragOffset)