fix(media): zone-based video controls — play always visible, controls on hover
- Center play button always visible when paused - Pause button appears on center hover when playing - Bottom controls bar only appears when hovering the bottom area - Controls also show when paused for discoverability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+53
-35
@@ -51,7 +51,6 @@ import androidx.compose.material3.Slider
|
|||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -62,7 +61,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoControls(
|
fun VideoControls(
|
||||||
@@ -83,50 +81,44 @@ fun VideoControls(
|
|||||||
onViewModeChange: ((ViewMode) -> Unit)? = null,
|
onViewModeChange: ((ViewMode) -> Unit)? = null,
|
||||||
trailingControls: @Composable (() -> Unit)? = null,
|
trailingControls: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var hovering by remember { mutableStateOf(false) }
|
var hoveringCenter by remember { mutableStateOf(false) }
|
||||||
var showControls by remember { mutableStateOf(false) }
|
var hoveringBottom by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Show controls on hover, auto-hide 2s after mouse leaves
|
|
||||||
LaunchedEffect(hovering) {
|
|
||||||
if (hovering) {
|
|
||||||
showControls = true
|
|
||||||
} else {
|
|
||||||
delay(2000)
|
|
||||||
showControls = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable { onPlayPause() }
|
.clickable { onPlayPause() },
|
||||||
.pointerInput(Unit) {
|
) {
|
||||||
awaitPointerEventScope {
|
// Center hover zone — top 70% of the video
|
||||||
while (true) {
|
Box(
|
||||||
val event = awaitPointerEvent()
|
modifier =
|
||||||
when (event.type) {
|
Modifier
|
||||||
PointerEventType.Enter -> {
|
.fillMaxWidth()
|
||||||
hovering = true
|
.fillMaxSize(0.7f)
|
||||||
showControls = true
|
.align(Alignment.TopCenter)
|
||||||
}
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
PointerEventType.Exit -> {
|
while (true) {
|
||||||
hovering = false
|
val event = awaitPointerEvent()
|
||||||
|
when (event.type) {
|
||||||
|
PointerEventType.Enter -> hoveringCenter = true
|
||||||
|
PointerEventType.Exit -> hoveringCenter = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
) {
|
|
||||||
// Center play/buffering indicator — only on hover
|
// Center play/buffering indicator
|
||||||
if (isBuffering) {
|
if (isBuffering) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center).size(48.dp),
|
modifier = Modifier.align(Alignment.Center).size(48.dp),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
strokeWidth = 3.dp,
|
strokeWidth = 3.dp,
|
||||||
)
|
)
|
||||||
} else if (!isPlaying && showControls) {
|
} else if (!isPlaying) {
|
||||||
|
// Always show play button when paused
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onPlayPause,
|
onClick = onPlayPause,
|
||||||
modifier = Modifier.align(Alignment.Center).size(64.dp),
|
modifier = Modifier.align(Alignment.Center).size(64.dp),
|
||||||
@@ -138,14 +130,40 @@ fun VideoControls(
|
|||||||
modifier = Modifier.size(48.dp),
|
modifier = Modifier.size(48.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (hoveringCenter) {
|
||||||
|
// Show pause button on center hover when playing
|
||||||
|
IconButton(
|
||||||
|
onClick = onPlayPause,
|
||||||
|
modifier = Modifier.align(Alignment.Center).size(64.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Pause,
|
||||||
|
contentDescription = "Pause",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom controls — two rows: seek slider on top, buttons below
|
// Bottom controls — show on hover over bottom area
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showControls,
|
visible = hoveringBottom || !isPlaying,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
when (event.type) {
|
||||||
|
PointerEventType.Enter -> hoveringBottom = true
|
||||||
|
PointerEventType.Exit -> hoveringBottom = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
Reference in New Issue
Block a user