From edd0e73de99855457d7c179715f67009b0b1b5b8 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 17 Apr 2026 14:19:32 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20waveform=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=BE=D1=82=D0=BA=D0=B8=20=D0=93=D0=A1=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BA=20=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chats/components/AttachmentComponents.kt | 76 ++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index f486bac..a1dc386 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -215,10 +215,14 @@ data class VoiceQueueEntry( object VoicePlaybackCoordinator { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val speedSteps = listOf(1f, 1.5f, 2f) + private const val SEEK_STALE_SAMPLE_WINDOW_MS = 900L + private const val SEEK_SETTLE_TOLERANCE_MS = 180 private var player: MediaPlayer? = null private var currentAttachmentId: String? = null private var appContext: Context? = null private var progressJob: Job? = null + private var pendingSeekTargetMs: Int? = null + private var pendingSeekExpiresAtMs: Long = 0L private val queueEntries = LinkedHashMap() private val _playingAttachmentId = MutableStateFlow(null) val playingAttachmentId: StateFlow = _playingAttachmentId.asStateFlow() @@ -246,6 +250,29 @@ object VoicePlaybackCoordinator { queueEntries.remove(attachmentId) } + private fun setPendingSeek(targetMs: Int) { + pendingSeekTargetMs = targetMs.coerceAtLeast(0) + pendingSeekExpiresAtMs = SystemClock.elapsedRealtime() + SEEK_STALE_SAMPLE_WINDOW_MS + } + + private fun clearPendingSeek() { + pendingSeekTargetMs = null + pendingSeekExpiresAtMs = 0L + } + + private fun resolvePositionForUi(sampledPositionMs: Int): Int { + val pendingTarget = pendingSeekTargetMs ?: return sampledPositionMs + val now = SystemClock.elapsedRealtime() + val settled = kotlin.math.abs(sampledPositionMs - pendingTarget) <= SEEK_SETTLE_TOLERANCE_MS + val expired = now >= pendingSeekExpiresAtMs + return if (settled || expired) { + clearPendingSeek() + sampledPositionMs + } else { + pendingTarget + } + } + fun toggle( attachmentId: String, sourceFile: File, @@ -269,6 +296,7 @@ object VoicePlaybackCoordinator { } stop() + clearPendingSeek() val mediaPlayer = MediaPlayer() try { mediaPlayer.setAudioAttributes( @@ -322,6 +350,7 @@ object VoicePlaybackCoordinator { } if (currentAttachmentId == attachmentId && player != null) { + clearPendingSeek() val active = player ?: return _playingAttachmentId.value = attachmentId _playingDialogKey.value = dialogKey.trim().ifBlank { null } @@ -333,6 +362,7 @@ object VoicePlaybackCoordinator { } stop() + clearPendingSeek() val mediaPlayer = MediaPlayer() try { mediaPlayer.setAudioAttributes( @@ -364,6 +394,7 @@ object VoicePlaybackCoordinator { } fun pause() { + clearPendingSeek() val active = player ?: return runCatching { if (active.isPlaying) active.pause() @@ -403,7 +434,7 @@ object VoicePlaybackCoordinator { val duration = runCatching { active.duration }.getOrDefault(_durationMs.value).coerceAtLeast(0) if (duration <= 0) return val clampedPosition = positionMs.coerceIn(0, duration) - runCatching { + val seekResult = runCatching { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { active.seekTo(clampedPosition.toLong(), MediaPlayer.SEEK_CLOSEST_SYNC) } else { @@ -411,6 +442,11 @@ object VoicePlaybackCoordinator { active.seekTo(clampedPosition) } } + if (seekResult.isSuccess) { + setPendingSeek(clampedPosition) + } else { + clearPendingSeek() + } if (keepPaused) { // Some devices auto-resume after seek; for paused scrub we force pause every time. runCatching { @@ -461,7 +497,8 @@ object VoicePlaybackCoordinator { scope.launch { while (isActive && currentAttachmentId == attachmentId) { val active = player ?: break - _positionMs.value = active.currentPosition.coerceAtLeast(0) + val sampledPosition = active.currentPosition.coerceAtLeast(0) + _positionMs.value = resolvePositionForUi(sampledPosition) _durationMs.value = active.duration.coerceAtLeast(0) if (!active.isPlaying) break delay(120) @@ -471,6 +508,7 @@ object VoicePlaybackCoordinator { } fun stop() { + clearPendingSeek() val active = player player = null currentAttachmentId = null @@ -2543,17 +2581,34 @@ private fun VoiceAttachment( var waveformWidthPx by remember(attachment.id) { mutableFloatStateOf(0f) } var isWaveformTouchLocked by remember(attachment.id) { mutableStateOf(false) } var suppressMainActionUntilMs by remember(attachment.id) { mutableLongStateOf(0L) } - val smoothProgress by - animateFloatAsState( - targetValue = progress, + val progressAnimator = remember(attachment.id) { Animatable(0f) } + LaunchedEffect(attachment.id, isActiveTrack) { + if (!isActiveTrack) { + progressAnimator.snapTo(0f) + } + } + LaunchedEffect(progress, isPlaying, isScrubbing, isActiveTrack) { + if (!isActiveTrack || isScrubbing) return@LaunchedEffect + + val target = progress.coerceIn(0f, 1f) + val delta = kotlin.math.abs(target - progressAnimator.value) + + // Для seek/резких скачков сразу фиксируем позицию без "пробега" от старого значения. + if (delta >= 0.12f) { + progressAnimator.snapTo(target) + } else { + progressAnimator.animateTo( + targetValue = target, animationSpec = if (isPlaying) { - tween(durationMillis = 140, easing = LinearEasing) + tween(durationMillis = 120, easing = LinearEasing) } else { - tween(durationMillis = 180, easing = FastOutSlowInEasing) - }, - label = "voice_wave_progress" + tween(durationMillis = 150, easing = FastOutSlowInEasing) + } ) + } + } + val smoothProgress = progressAnimator.value val displayProgress = if (isScrubbing) scrubProgress else smoothProgress val liveWaveLevel = remember(isPlaying, progress, waves) { @@ -3033,6 +3088,9 @@ private fun VoiceAttachment( if (isDraggingWaveform) { val releaseProgress = progressFromWaveX(lastTouchX) scrubProgress = releaseProgress + scope.launch { + progressAnimator.snapTo(releaseProgress) + } VoicePlaybackCoordinator.seekToProgress( releaseProgress, keepPaused = !wasPlayingOnTouchDown