chatgui-android: видеоплеер — зум, поворот, полноэкранный режим
This commit is contained in:
1 parent
aa166a2030
commit
fe1726ebea
1 file changed
+380
-79
+380
-79
@@ -1,33 +1,74 @@
|
||||
package com.utun.chat.ui.screens
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaPlayer
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.Surface
|
||||
import android.view.TextureView
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.ScreenRotation
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.utun.chat.data.LogManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private data class VideoInfo(val width: Int, val height: Int, val rotation: Int) {
|
||||
val displayWidth: Int get() = if (rotation == 90 || rotation == 270) height else width
|
||||
val displayHeight: Int get() = if (rotation == 90 || rotation == 270) width else height
|
||||
val aspect: Float get() = if (displayHeight > 0) displayWidth.toFloat() / displayHeight else 0f
|
||||
}
|
||||
|
||||
private tailrec fun Context.findActivity(): Activity? = when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun clampOffset(scale: Float, o: Offset, vW: Float, vH: Float, sW: Float, sH: Float): Offset {
|
||||
val mx = ((vW * scale - sW) / 2f).coerceAtLeast(0f)
|
||||
val my = ((vH * scale - sH) / 2f).coerceAtLeast(0f)
|
||||
return Offset(o.x.coerceIn(-mx, mx), o.y.coerceIn(-my, my))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
@@ -42,10 +83,93 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
var controlsVisible by remember { mutableStateOf(true) }
|
||||
var dragging by remember { mutableStateOf(false) }
|
||||
var dragPosition by remember { mutableStateOf(0) }
|
||||
var wasPlayingBeforeDrag by remember { mutableStateOf(false) }
|
||||
var videoInfo by remember { mutableStateOf<VideoInfo?>(null) }
|
||||
var zoomScale by remember(filePath) { mutableStateOf(1f) }
|
||||
var zoomOffset by remember(filePath) { mutableStateOf(Offset.Zero) }
|
||||
var hideTick by remember { mutableStateOf(0) }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
/* ── immersive: hide system bars while playing ── */
|
||||
DisposableEffect(Unit) {
|
||||
val act = context.findActivity()
|
||||
val controller = act?.let { WindowCompat.getInsetsController(it.window, it.window.decorView) }
|
||||
controller?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller?.hide(WindowInsetsCompat.Type.systemBars())
|
||||
LogManager.addLog("INFO", "VideoPlayer", "system bars hidden")
|
||||
onDispose {
|
||||
controller?.show(WindowInsetsCompat.Type.systemBars())
|
||||
LogManager.addLog("INFO", "VideoPlayer", "system bars restored")
|
||||
}
|
||||
}
|
||||
|
||||
/* ── video metadata (dimensions + rotation) ── */
|
||||
LaunchedEffect(filePath) {
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
val mmr = MediaMetadataRetriever()
|
||||
try {
|
||||
mmr.setDataSource(filePath)
|
||||
val w = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0
|
||||
val h = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0
|
||||
val r = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
|
||||
VideoInfo(w, h, r)
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "VideoPlayer", "metadata failed: ${e.message}")
|
||||
null
|
||||
} finally {
|
||||
try { mmr.release() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
videoInfo = info
|
||||
if (info != null) {
|
||||
LogManager.addLog(
|
||||
"INFO", "VideoPlayer",
|
||||
"video ${info.width}x${info.height} rot=${info.rotation} -> display ${info.displayWidth}x${info.displayHeight} aspect=%.3f".format(info.aspect)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── auto-rotate screen to match video orientation ── */
|
||||
DisposableEffect(videoInfo) {
|
||||
val act = context.findActivity()
|
||||
val prev = act?.requestedOrientation
|
||||
val info = videoInfo
|
||||
var oel: OrientationEventListener? = null
|
||||
if (act != null && info != null && info.displayWidth > 0 && info.displayHeight > 0) {
|
||||
if (info.displayWidth > info.displayHeight) {
|
||||
/* landscape: determine side from sensor once, then lock */
|
||||
oel = object : OrientationEventListener(context) {
|
||||
override fun onOrientationChanged(angle: Int) {
|
||||
if (angle == ORIENTATION_UNKNOWN) return
|
||||
val reverse = angle in 45..134
|
||||
val orient = if (reverse) ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
LogManager.addLog("INFO", "VideoPlayer", "auto-orient sensor=$angle -> ${if (reverse) "REVERSE_LANDSCAPE" else "LANDSCAPE"} (prev=$prev)")
|
||||
act.requestedOrientation = orient
|
||||
disable()
|
||||
}
|
||||
}
|
||||
oel.enable()
|
||||
} else {
|
||||
LogManager.addLog("INFO", "VideoPlayer", "auto-orient -> PORTRAIT (prev=$prev)")
|
||||
act.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
}
|
||||
}
|
||||
onDispose {
|
||||
oel?.disable()
|
||||
if (act != null && prev != null) {
|
||||
act.requestedOrientation = prev
|
||||
LogManager.addLog("INFO", "VideoPlayer", "restore orientation $prev")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── MediaPlayer lifecycle ── */
|
||||
DisposableEffect(filePath) {
|
||||
player.setOnPreparedListener { mp ->
|
||||
try { mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT) } catch (_: Exception) {}
|
||||
LogManager.addLog("INFO", "VideoPlayer", "prepared ${mp.videoWidth}x${mp.videoHeight} dur=${mp.duration}")
|
||||
durationMs = mp.duration
|
||||
prepared.value = true
|
||||
if (surfaceReady.value) {
|
||||
@@ -54,9 +178,8 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
player.setOnCompletionListener {
|
||||
isPlaying = false
|
||||
positionMs = 0
|
||||
try { it.seekTo(0) } catch (_: Exception) {}
|
||||
LogManager.addLog("INFO", "VideoSeek", "onCompletion, closing player")
|
||||
onClose()
|
||||
}
|
||||
player.setOnErrorListener { _, what, extra ->
|
||||
LogManager.addLog("ERROR", "VideoPlayer", "MediaPlayer error what=$what extra=$extra file=$filePath")
|
||||
@@ -85,16 +208,29 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── controls auto-hide ── */
|
||||
LaunchedEffect(controlsVisible, isPlaying) {
|
||||
/* ── controls auto-hide (only while playing) ── */
|
||||
LaunchedEffect(controlsVisible, isPlaying, hideTick) {
|
||||
if (controlsVisible && isPlaying) {
|
||||
delay(3000)
|
||||
controlsVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ── controls always visible while paused ── */
|
||||
LaunchedEffect(isPlaying) {
|
||||
if (!isPlaying) {
|
||||
controlsVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
fun onInteraction() {
|
||||
hideTick++
|
||||
controlsVisible = true
|
||||
}
|
||||
|
||||
fun togglePlay() {
|
||||
if (error != null) return
|
||||
onInteraction()
|
||||
if (isPlaying) {
|
||||
try { player.pause() } catch (_: Exception) {}
|
||||
isPlaying = false
|
||||
@@ -108,9 +244,53 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
fun flipLandscape() {
|
||||
onInteraction()
|
||||
val act = context.findActivity() ?: return
|
||||
val cur = act.requestedOrientation
|
||||
val newOrient = if (cur == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
LogManager.addLog("INFO", "VideoPlayer", "flip 180 -> $newOrient")
|
||||
act.requestedOrientation = newOrient
|
||||
}
|
||||
|
||||
val isLandscape = (videoInfo?.displayWidth ?: 0) > (videoInfo?.displayHeight ?: 0)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxSize().background(Color.Black)
|
||||
) {
|
||||
val ratio = videoInfo?.aspect ?: 0f
|
||||
val w: Dp
|
||||
val h: Dp
|
||||
if (ratio > 0f) {
|
||||
if (maxWidth / ratio <= maxHeight) {
|
||||
w = maxWidth
|
||||
h = maxWidth / ratio
|
||||
} else {
|
||||
h = maxHeight
|
||||
w = maxHeight * ratio
|
||||
}
|
||||
} else {
|
||||
w = maxWidth
|
||||
h = maxHeight
|
||||
}
|
||||
val videoModifier = if (ratio > 0f) {
|
||||
Modifier.size(w, h).align(Alignment.Center)
|
||||
} else {
|
||||
Modifier.fillMaxSize()
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val screenW = with(density) { maxWidth.toPx() }
|
||||
val screenH = with(density) { maxHeight.toPx() }
|
||||
val videoW = with(density) { w.toPx() }
|
||||
val videoH = with(density) { h.toPx() }
|
||||
|
||||
/* re-clamp zoom offset when screen size changes (e.g. flip 180) */
|
||||
LaunchedEffect(screenW, screenH) {
|
||||
zoomOffset = clampOffset(zoomScale, zoomOffset, videoW, videoH, screenW, screenH)
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
TextureView(ctx).apply {
|
||||
@@ -133,7 +313,12 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = videoModifier.graphicsLayer {
|
||||
scaleX = zoomScale
|
||||
scaleY = zoomScale
|
||||
translationX = zoomOffset.x
|
||||
translationY = zoomOffset.y
|
||||
}
|
||||
)
|
||||
|
||||
if (error != null) {
|
||||
@@ -145,84 +330,200 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/* tap surface toggles controls */
|
||||
/* gestures: tap toggles controls; pinch zooms; drag pans */
|
||||
if (error == null) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) { controlsVisible = !controlsVisible }
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
hideTick++
|
||||
if (isPlaying) controlsVisible = !controlsVisible
|
||||
}
|
||||
}
|
||||
.pointerInput(screenW, screenH, videoW, videoH) {
|
||||
detectTransformGestures { centroid, pan, zoom, _ ->
|
||||
onInteraction()
|
||||
val old = zoomScale
|
||||
val new = (zoomScale * zoom).coerceIn(1f, 4f)
|
||||
val z = new / old
|
||||
val rel = centroid - Offset(screenW / 2f, screenH / 2f)
|
||||
val raw = Offset(
|
||||
zoomOffset.x * z + rel.x * (1f - z) + pan.x,
|
||||
zoomOffset.y * z + rel.y * (1f - z) + pan.y
|
||||
)
|
||||
zoomScale = new
|
||||
zoomOffset = clampOffset(new, raw, videoW, videoH, screenW, screenH)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (controlsVisible) {
|
||||
/* top bar: close */
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(Color(0xAA000000))
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(Icons.Filled.Close, "Закрыть", tint = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
/* center: play/pause */
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (error == null) {
|
||||
IconButton(
|
||||
onClick = { togglePlay() },
|
||||
modifier = Modifier.size(72.dp).background(Color(0x66000000), MaterialTheme.shapes.extraLarge)
|
||||
) {
|
||||
Icon(
|
||||
if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
|
||||
contentDescription = if (isPlaying) "Пауза" else "Плей",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = controlsVisible,
|
||||
enter = fadeIn(tween(1000)),
|
||||
exit = fadeOut(tween(1000)),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
/* top bar: close (right) */
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(Color(0xAA000000))
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(Icons.Filled.Close, "Закрыть", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* bottom bar: seek + time */
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)
|
||||
.background(Color(0xAA000000))
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
formatDurationMs(if (dragging) dragPosition else positionMs),
|
||||
color = Color.White, fontSize = 12.sp
|
||||
)
|
||||
Slider(
|
||||
value = (if (dragging) dragPosition else positionMs).toFloat(),
|
||||
onValueChange = { v ->
|
||||
dragging = true
|
||||
dragPosition = v.toInt().coerceIn(0, durationMs.coerceAtLeast(0))
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
val target = dragPosition
|
||||
try { player.seekTo(target) } catch (_: Exception) {}
|
||||
positionMs = target
|
||||
dragging = false
|
||||
},
|
||||
valueRange = 0f..durationMs.coerceAtLeast(1).toFloat(),
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = Color.White,
|
||||
activeTrackColor = Color.White,
|
||||
inactiveTrackColor = Color.White.copy(alpha = 0.4f)
|
||||
/* center: play/pause */
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (error == null) {
|
||||
IconButton(
|
||||
onClick = { togglePlay() },
|
||||
modifier = Modifier.size(72.dp).background(Color(0x66000000), MaterialTheme.shapes.extraLarge)
|
||||
) {
|
||||
Icon(
|
||||
if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
|
||||
contentDescription = if (isPlaying) "Пауза" else "Плей",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* center-left + center-right: rotate 180 (landscape only) */
|
||||
if (isLandscape) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { flipLandscape() },
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
.size(56.dp)
|
||||
.background(Color(0x66000000), MaterialTheme.shapes.extraLarge)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.ScreenRotation,
|
||||
contentDescription = "Перевернуть",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { flipLandscape() },
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
.size(56.dp)
|
||||
.background(Color(0x66000000), MaterialTheme.shapes.extraLarge)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.ScreenRotation,
|
||||
contentDescription = "Перевернуть",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* bottom bar: seek + time */
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)
|
||||
.background(Color(0xAA000000))
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
formatDurationMs(if (dragging) dragPosition else positionMs),
|
||||
color = Color.White, fontSize = 12.sp
|
||||
)
|
||||
)
|
||||
Text(formatDurationMs(durationMs), color = Color.White, fontSize = 12.sp)
|
||||
|
||||
val fraction = if (durationMs > 0)
|
||||
((if (dragging) dragPosition else positionMs).toFloat() / durationMs).coerceIn(0f, 1f)
|
||||
else 0f
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(32.dp)
|
||||
.pointerInput(durationMs) {
|
||||
detectTapGestures { offset ->
|
||||
onInteraction()
|
||||
val pos = (offset.x / size.width * durationMs).toInt().coerceIn(0, durationMs)
|
||||
try { player.seekTo(pos) } catch (_: Exception) {}
|
||||
positionMs = pos
|
||||
}
|
||||
}
|
||||
.pointerInput(durationMs) {
|
||||
detectDragGestures(
|
||||
onDragStart = { offset ->
|
||||
wasPlayingBeforeDrag = isPlaying
|
||||
if (isPlaying) {
|
||||
try { player.pause() } catch (_: Exception) {}
|
||||
isPlaying = false
|
||||
}
|
||||
dragging = true
|
||||
dragPosition = (offset.x / size.width * durationMs).toInt().coerceIn(0, durationMs)
|
||||
try { player.seekTo(dragPosition) } catch (_: Exception) {}
|
||||
},
|
||||
onDrag = { change, _ ->
|
||||
change.consume()
|
||||
dragPosition = (change.position.x / size.width * durationMs).toInt().coerceIn(0, durationMs)
|
||||
try { player.seekTo(dragPosition) } catch (_: Exception) {}
|
||||
},
|
||||
onDragEnd = {
|
||||
try { player.seekTo(dragPosition) } catch (_: Exception) {}
|
||||
positionMs = dragPosition
|
||||
dragging = false
|
||||
if (wasPlayingBeforeDrag) {
|
||||
try { player.start() } catch (_: Exception) {}
|
||||
isPlaying = true
|
||||
}
|
||||
},
|
||||
onDragCancel = {
|
||||
try { player.seekTo(dragPosition) } catch (_: Exception) {}
|
||||
positionMs = dragPosition
|
||||
dragging = false
|
||||
if (wasPlayingBeforeDrag) {
|
||||
try { player.start() } catch (_: Exception) {}
|
||||
isPlaying = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier.align(Alignment.CenterStart).fillMaxWidth().height(4.dp)
|
||||
.background(Color.White.copy(alpha = 0.4f), RoundedCornerShape(2.dp))
|
||||
)
|
||||
Box(
|
||||
Modifier.align(Alignment.CenterStart).fillMaxWidth(fraction).height(4.dp)
|
||||
.background(Color.White, RoundedCornerShape(2.dp))
|
||||
)
|
||||
Box(
|
||||
Modifier.align(Alignment.CenterStart).fillMaxWidth(fraction).height(32.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Box(Modifier.size(14.dp).background(Color.White, CircleShape))
|
||||
}
|
||||
}
|
||||
|
||||
Text(formatDurationMs(durationMs), color = Color.White, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user