Browse Source

Replace stream rotation menu with three marked toggle buttons

stream-multiview
evgeny 2 weeks ago
parent
commit
8bee8c10ff
  1. 83
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt

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

@ -9,25 +9,34 @@ import android.view.TextureView
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -166,10 +175,13 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
}, label = { Text("${camera.lens.label} ${camera.lens.mm}".trim()) }) }, label = { Text("${camera.lens.label} ${camera.lens.mm}".trim()) })
} }
} }
val modes = cameras.firstOrNull { it.id == mode?.camera }?.modes.orEmpty() val camera = cameras.firstOrNull { it.id == mode?.camera }
val modes = camera?.modes.orEmpty()
val context = LocalContext.current
val configuration = LocalConfiguration.current
val display = remember(configuration) { ((context as? Activity)?.windowManager?.defaultDisplay?.rotation ?: 0) * 90 }
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var orientationsExpanded by remember { mutableStateOf(false) } Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Box { Box {
OutlinedButton(onClick = { expanded = true }, enabled = enabled && modes.isNotEmpty()) { Text(mode?.label.orEmpty()) } OutlinedButton(onClick = { expanded = true }, enabled = enabled && modes.isNotEmpty()) { Text(mode?.label.orEmpty()) }
DropdownMenu(expanded = expanded && enabled, onDismissRequest = { expanded = false }) { DropdownMenu(expanded = expanded && enabled, onDismissRequest = { expanded = false }) {
@ -178,16 +190,45 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
}) } }) }
} }
} }
Box { for (option in listOf(StreamOrientation.UPRIGHT, StreamOrientation.RIGHT, StreamOrientation.LEFT)) {
OutlinedButton(onClick = { orientationsExpanded = true }, enabled = enabled && config != null) { val active = config?.orientation == option
Text(stringResource(R.string.stream_orientation, config?.orientation?.degrees ?: 0)) val label = stringResource(R.string.stream_orientation, option.degrees)
val top = if (camera?.lens?.type == LensType.SELFIE) (display - option.degrees + 360) % 360
else (option.degrees - display + 360) % 360
OutlinedButton(onClick = { config?.let { onChange(it.copy(orientation = option)) } }, enabled = enabled && config != null,
modifier = Modifier.size(44.dp).semantics { selected = active; contentDescription = label },
shape = RoundedCornerShape(6.dp), contentPadding = PaddingValues(2.dp),
border = BorderStroke(if (active) 2.dp else 1.dp,
if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (active) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
StreamRotationMark(top)
Text("${option.degrees}°", fontSize = 10.sp, lineHeight = 10.sp)
}
} }
DropdownMenu(expanded = orientationsExpanded && enabled, onDismissRequest = { orientationsExpanded = false }) { }
StreamOrientation.values().forEach { option -> DropdownMenuItem(text = { Text("${option.degrees}°") }, onClick = { }
orientationsExpanded = false; config?.let { onChange(it.copy(orientation = option)) } }
}) }
@Composable
private fun StreamRotationMark(top: Int) {
val frame = LocalContentColor.current
val marker = MaterialTheme.colorScheme.error
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
Canvas(Modifier.matchParentSize()) {
val left = size.width * 0.15f; val right = size.width * 0.85f
val upper = size.height * 0.08f; val lower = size.height * 0.92f
drawRoundRect(frame, Offset(left, upper), Size(right - left, lower - upper), CornerRadius(2.dp.toPx()), style = Stroke(1.dp.toPx()))
val edge = when (top) {
90 -> Offset(right, upper) to Offset(right, lower)
180 -> Offset(left, lower) to Offset(right, lower)
270 -> Offset(left, upper) to Offset(left, lower)
else -> Offset(left, upper) to Offset(right, upper)
} }
drawLine(marker, edge.first, edge.second, 3.dp.toPx(), StrokeCap.Round)
} }
Text(stringResource(R.string.stream_top), fontSize = 7.sp, lineHeight = 7.sp)
} }
} }
@ -215,7 +256,6 @@ private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview:
private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPreview: StreamPreview?, displayDegrees: Int) { private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPreview: StreamPreview?, displayDegrees: Int) {
val engine = remember(group, source, epoch, localPreview) { StreamPlayerEngine(group, source, epoch, localPreview, displayDegrees) } val engine = remember(group, source, epoch, localPreview) { StreamPlayerEngine(group, source, epoch, localPreview, displayDegrees) }
val aspect by engine.aspect.collectAsState() val aspect by engine.aspect.collectAsState()
val top by engine.top.collectAsState()
val status by engine.status.collectAsState() val status by engine.status.collectAsState()
var surface by remember { mutableStateOf<Surface?>(null) } var surface by remember { mutableStateOf<Surface?>(null) }
DisposableEffect(engine) { onDispose { engine.stop() } } DisposableEffect(engine) { onDispose { engine.stop() } }
@ -240,25 +280,8 @@ private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPrevi
}, modifier = Modifier.matchParentSize(), onRelease = { view -> }, modifier = Modifier.matchParentSize(), onRelease = { view ->
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
}) })
top?.let { StreamTopMarker(it) }
} }
} }
} }
} }
@Composable
private fun BoxScope.StreamTopMarker(direction: Int) {
val alignment = when (direction) {
90 -> Alignment.CenterEnd
180 -> Alignment.BottomCenter
270 -> Alignment.CenterStart
else -> Alignment.TopCenter
}
Surface(modifier = Modifier.align(alignment).padding(8.dp), shape = RoundedCornerShape(6.dp), color = Color.Black.copy(alpha = 0.65f)) {
Row(Modifier.padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.ArrowUpward, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp).rotate(direction.toFloat()))
Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.stream_top), color = Color.White, style = MaterialTheme.typography.bodySmall)
}
}
}

Loading…
Cancel
Save