Browse Source

Share video zoom geometry between Android viewers

master
evgeny 5 days ago
parent
commit
c39610f079
  1. 36
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoPlayerScreen.kt
  2. 30
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoZoomState.kt
  3. 51
      tools/chatgui-android/app/src/test/java/com/utun/chat/ui/screens/VideoZoomStateTest.kt

36
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoPlayerScreen.kt

@ -36,7 +36,7 @@ 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.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
@ -67,12 +67,6 @@ private tailrec fun Context.findActivity(): Activity? = when (this) {
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) {
val player = remember(filePath) { MediaPlayer() }
@ -88,8 +82,7 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
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) }
val zoom = remember(filePath) { VideoZoomState() }
var hideTick by remember { mutableStateOf(0) }
val context = LocalContext.current
@ -290,8 +283,8 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
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)
LaunchedEffect(screenW, screenH, videoW, videoH) {
zoom.constrain(Size(videoW, videoH), Size(screenW, screenH))
}
AndroidView(
@ -317,10 +310,10 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
}
},
modifier = videoModifier.graphicsLayer {
scaleX = zoomScale
scaleY = zoomScale
translationX = zoomOffset.x
translationY = zoomOffset.y
scaleX = zoom.scale
scaleY = zoom.scale
translationX = zoom.offset.x
translationY = zoom.offset.y
}
)
@ -344,18 +337,9 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
}
}
.pointerInput(screenW, screenH, videoW, videoH) {
detectTransformGestures { centroid, pan, zoom, _ ->
detectTransformGestures { centroid, pan, factor, _ ->
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)
zoom.transform(centroid, pan, factor, Size(videoW, videoH), Size(screenW, screenH))
}
}
)

30
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoZoomState.kt

@ -0,0 +1,30 @@
package com.utun.chat.ui.screens
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
/** Общая геометрия увеличения для файла и live: точка под пальцами сохраняется, края ограничивают сдвиг. */
internal class VideoZoomState {
var scale by mutableStateOf(1f)
private set
var offset by mutableStateOf(Offset.Zero)
private set
fun transform(centroid: Offset, pan: Offset, factor: Float, video: Size, viewport: Size) {
val next = (scale * factor).coerceIn(1f, 4f)
val ratio = next / scale
val relative = centroid - Offset(viewport.width / 2f, viewport.height / 2f)
offset = offset * ratio + relative * (1f - ratio) + pan
scale = next
constrain(video, viewport)
}
fun constrain(video: Size, viewport: Size) {
val x = ((video.width * scale - viewport.width) / 2f).coerceAtLeast(0f)
val y = ((video.height * scale - viewport.height) / 2f).coerceAtLeast(0f)
offset = Offset(offset.x.coerceIn(-x, x), offset.y.coerceIn(-y, y))
}
}

51
tools/chatgui-android/app/src/test/java/com/utun/chat/ui/screens/VideoZoomStateTest.kt

@ -0,0 +1,51 @@
package com.utun.chat.ui.screens
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import org.junit.Assert.assertEquals
import org.junit.Test
class VideoZoomStateTest {
private val video = Size(1920f, 1080f)
private val viewport = Size(2400f, 1080f)
private val center = Offset(1200f, 540f)
@Test fun pinchKeepsPointUnderFingers() {
val zoom = VideoZoomState()
zoom.transform(center + Offset(100f, 50f), Offset.Zero, 2f, video, viewport)
assertEquals(2f, zoom.scale)
assertEquals(Offset(-100f, -50f), zoom.offset)
}
@Test fun panCannotExposeSpaceBeyondVideoEdges() {
val zoom = VideoZoomState()
zoom.transform(center, Offset(10000f, -10000f), 2f, video, viewport)
assertEquals(Offset(720f, -540f), zoom.offset)
}
@Test fun returningToOneCentersVideoAndPreventsDragging() {
val zoom = VideoZoomState()
zoom.transform(center, Offset(300f, 300f), 3f, video, viewport)
zoom.transform(center, Offset(100f, 100f), 0.1f, video, viewport)
assertEquals(1f, zoom.scale)
assertEquals(Offset.Zero, zoom.offset)
}
@Test fun zoomLimitUsesActualScaleForAnchoring() {
val zoom = VideoZoomState()
zoom.transform(center, Offset.Zero, 3f, video, viewport)
zoom.transform(center + Offset(300f, 0f), Offset.Zero, 2f, video, viewport)
assertEquals(4f, zoom.scale)
assertEquals(-100f, zoom.offset.x, 0.001f)
}
@Test fun changingVideoOrWindowSizeClampsExistingOffset() {
val zoom = VideoZoomState()
zoom.transform(center, Offset(700f, 500f), 2f, video, viewport)
zoom.constrain(Size(600f, 1080f), viewport)
assertEquals(Offset(0f, 500f), zoom.offset)
zoom.constrain(Size(1080f, 607.5f), Size(1080f, 2400f))
assertEquals(Offset.Zero, zoom.offset)
assertEquals(2f, zoom.scale)
}
}
Loading…
Cancel
Save