Enable pinch zoom and bounded panning in Android live viewer
This commit is contained in:
1 parent
c39610f079
commit
8eef506eed
1 file changed
+52
-27
+52
-27
@@ -8,6 +8,7 @@ import android.view.Surface
|
||||
import android.view.TextureView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
@@ -18,9 +19,13 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Size
|
||||
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.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -50,6 +55,8 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
|
||||
var aspect by remember { mutableStateOf(0f) }
|
||||
var status by remember { mutableStateOf(R.string.stream_connecting) }
|
||||
var controls by remember { mutableStateOf(true) }
|
||||
var hideTick by remember { mutableStateOf(0) }
|
||||
val zoom = remember { VideoZoomState() }
|
||||
|
||||
DisposableEffect(owner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
@@ -90,16 +97,38 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
|
||||
}
|
||||
}
|
||||
LaunchedEffect(status) { controls = true }
|
||||
LaunchedEffect(controls, status, visible) {
|
||||
LaunchedEffect(controls, status, visible, hideTick) {
|
||||
if (controls && status == R.string.stream_live && visible) { delay(3000); controls = false }
|
||||
}
|
||||
LaunchedEffect(controls) { LogManager.addLog("DEBUG", "stream", "viewer controls source=$source visible=$controls") }
|
||||
LaunchedEffect(zoom.scale, zoom.offset) {
|
||||
delay(300)
|
||||
LogManager.addLog("DEBUG", "stream", "viewer zoom source=$source scale=${zoom.scale} offset=${zoom.offset}")
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize().background(Color.Black)) {
|
||||
if (visible) StreamViewerPlayback(group, source, epoch, { aspect = it }, { status = it })
|
||||
Box(Modifier.matchParentSize().pointerInput(status) {
|
||||
detectTapGestures { if (status == R.string.stream_live) controls = !controls else controls = true }
|
||||
})
|
||||
BoxWithConstraints(Modifier.fillMaxSize().background(Color.Black).clipToBounds()) {
|
||||
val ratio = aspect.takeIf { it > 0f } ?: 16f / 9f
|
||||
val width = minOf(maxWidth, maxHeight * ratio)
|
||||
val height = width / ratio
|
||||
val density = LocalDensity.current
|
||||
val videoSize = with(density) { Size(width.toPx(), height.toPx()) }
|
||||
val viewport = with(density) { Size(maxWidth.toPx(), maxHeight.toPx()) }
|
||||
LaunchedEffect(videoSize, viewport) { zoom.constrain(videoSize, viewport) }
|
||||
if (visible) StreamViewerPlayback(group, source, epoch, { aspect = it }, { status = it },
|
||||
Modifier.size(width, height).align(Alignment.Center).graphicsLayer {
|
||||
scaleX = zoom.scale; scaleY = zoom.scale
|
||||
translationX = zoom.offset.x; translationY = zoom.offset.y
|
||||
})
|
||||
Box(Modifier.matchParentSize()
|
||||
.pointerInput(status) {
|
||||
detectTapGestures { if (status == R.string.stream_live) controls = !controls else controls = true }
|
||||
}
|
||||
.pointerInput(videoSize, viewport) {
|
||||
detectTransformGestures { centroid, pan, factor, _ ->
|
||||
controls = true; hideTick++
|
||||
zoom.transform(centroid, pan, factor, videoSize, viewport)
|
||||
}
|
||||
})
|
||||
if (controls) {
|
||||
Row(Modifier.fillMaxWidth().align(Alignment.TopCenter).background(Color.Black.copy(alpha = 0.65f))
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing).padding(start = 16.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
||||
@@ -117,33 +146,29 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
|
||||
|
||||
/** Смена размеров окна и скрытие controls сохраняют TextureView, decoder и сетевой reader. */
|
||||
@Composable
|
||||
private fun StreamViewerPlayback(group: Long, source: Long, epoch: Long, onAspect: (Float) -> Unit, onStatus: (Int) -> Unit) {
|
||||
private fun StreamViewerPlayback(group: Long, source: Long, epoch: Long, onAspect: (Float) -> Unit, onStatus: (Int) -> Unit,
|
||||
modifier: Modifier) {
|
||||
val engine = remember(group, source, epoch) { StreamPlayerEngine(group, source, epoch) }
|
||||
val aspect by engine.aspect.collectAsState()
|
||||
val status by engine.status.collectAsState()
|
||||
var surface by remember { mutableStateOf<Surface?>(null) }
|
||||
LaunchedEffect(aspect, status) { onAspect(aspect); onStatus(status) }
|
||||
DisposableEffect(engine) { onDispose { engine.stop() } }
|
||||
BoxWithConstraints(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
val ratio = aspect.takeIf { it > 0f } ?: 16f / 9f
|
||||
val width = minOf(maxWidth, maxHeight * ratio)
|
||||
val height = width / ratio
|
||||
AndroidView(factory = { context ->
|
||||
TextureView(context).apply {
|
||||
keepScreenOn = true
|
||||
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
|
||||
surface = Surface(texture); engine.start(surface!!)
|
||||
}
|
||||
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {}
|
||||
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
|
||||
engine.stop(); surface?.release(); surface = null; return true
|
||||
}
|
||||
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
|
||||
AndroidView(factory = { context ->
|
||||
TextureView(context).apply {
|
||||
keepScreenOn = true
|
||||
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
|
||||
surface = Surface(texture); engine.start(surface!!)
|
||||
}
|
||||
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {}
|
||||
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
|
||||
engine.stop(); surface?.release(); surface = null; return true
|
||||
}
|
||||
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
|
||||
}
|
||||
}, modifier = Modifier.size(width, height), onRelease = { view ->
|
||||
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
|
||||
})
|
||||
}
|
||||
}
|
||||
}, modifier = modifier, onRelease = { view ->
|
||||
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user