Rewrite layout for call participants to match new spec.

This commit is contained in:
Alex Hart
2025-11-18 10:29:09 -04:00
committed by Cody Henthorne
parent 9f0f8b7cbc
commit 69d2ad410f
7 changed files with 542 additions and 81 deletions

View File

@@ -10,9 +10,11 @@ import android.widget.FrameLayout
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.viewinterop.AndroidView
import org.thoughtcrime.securesms.R
@@ -35,12 +37,21 @@ fun CallParticipantRenderer(
onToggleCameraDirection: () -> Unit = {}
) {
if (LocalInspectionMode.current) {
Box(modifier.background(color = Color.Red))
Box(
contentAlignment = Alignment.Center,
modifier = modifier.background(color = MaterialTheme.colorScheme.secondaryContainer)
) {
Text(
text = "${callParticipant.callParticipantId.recipientId.toLong()}",
style = MaterialTheme.typography.titleLarge
)
}
} else {
AndroidView(
factory = { LayoutInflater.from(it).inflate(R.layout.call_participant_item, FrameLayout(it), false) as CallParticipantView },
modifier = modifier.fillMaxSize().background(color = Color.Red),
onRelease = { it.releaseRenderer() }
modifier = modifier.fillMaxSize(),
onRelease = { it.releaseRenderer() },
onReset = {} // Allows reuse in lazy lists
) { view ->
view.setCallParticipant(callParticipant)
view.setMirror(isLocalParticipant && callParticipant.cameraState.activeDirection == CameraState.Direction.FRONT)

View File

@@ -5,40 +5,138 @@
package org.thoughtcrime.securesms.components.webrtc.v2
import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallParticipantsRecyclerAdapter
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.NightPreview
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Wrapper composable for the CallParticipants overflow recycler view.
* Replacement composable for the CallParticipants overflow recycler view.
*
* Displays a scrollable list of users that are in the call but are not displayed in the primary grid.
*/
@Composable
fun CallParticipantsOverflow(
lineType: LayoutStrategyLineType,
overflowParticipants: List<CallParticipant>,
modifier: Modifier = Modifier
) {
val adapter = remember { WebRtcCallParticipantsRecyclerAdapter() }
AndroidView(
factory = {
val view = LayoutInflater.from(it).inflate(R.layout.webrtc_call_participant_overflow_recycler, FrameLayout(it), false) as RecyclerView
view.adapter = adapter
view
},
modifier = modifier,
update = {
it.visible = true
adapter.submitList(overflowParticipants)
if (lineType == LayoutStrategyLineType.ROW) {
LazyRow(
reverseLayout = true,
modifier = modifier,
contentPadding = PaddingValues(start = 16.dp, end = CallScreenMetrics.SmallRendererSize + 32.dp),
horizontalArrangement = spacedBy(4.dp)
) {
appendItems(CallScreenMetrics.SmallRendererSize, overflowParticipants)
}
)
} else {
LazyColumn(
reverseLayout = true,
modifier = modifier,
contentPadding = PaddingValues(top = 16.dp, bottom = CallScreenMetrics.SmallRendererSize + 32.dp),
verticalArrangement = spacedBy(4.dp)
) {
appendItems(CallScreenMetrics.SmallRendererSize, overflowParticipants)
}
}
}
private fun LazyListScope.appendItems(
contentSize: Dp,
overflowParticipants: List<CallParticipant>
) {
items(
items = overflowParticipants,
key = { it.callParticipantId }
) { participant ->
CallParticipantRenderer(
callParticipant = participant,
renderInPip = false,
modifier = Modifier
.size(contentSize)
.clip(CallScreenMetrics.SmallRendererShape)
)
}
}
@NightPreview
@Composable
private fun CallParticipantsOverflowPreview() {
Previews.Preview {
val participants = remember {
(1..10).map {
CallParticipant(
callParticipantId = CallParticipantId(
demuxId = 0,
recipientId = RecipientId.from(it.toLong())
),
recipient = Recipient(
isResolving = false,
chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT
)
)
}
}
CallParticipantsOverflow(
lineType = LayoutStrategyLineType.ROW,
overflowParticipants = participants,
modifier = Modifier
.padding(vertical = 16.dp)
.height(CallScreenMetrics.SmallRendererSize)
.fillMaxWidth()
)
}
}
@NightPreview
@Composable
private fun CallParticipantsOverflowColumnPreview() {
Previews.Preview {
val participants = remember {
(1..10).map {
CallParticipant(
callParticipantId = CallParticipantId(
demuxId = 0,
recipientId = RecipientId.from(it.toLong())
),
recipient = Recipient(
isResolving = false,
chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT
)
)
}
}
CallParticipantsOverflow(
lineType = LayoutStrategyLineType.COLUMN,
overflowParticipants = participants,
modifier = Modifier
.padding(horizontal = 16.dp)
.width(CallScreenMetrics.SmallRendererSize)
.fillMaxHeight()
)
}
}

View File

@@ -5,21 +5,38 @@
package org.thoughtcrime.securesms.components.webrtc.v2
import android.content.res.Configuration
import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.viewinterop.AndroidView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsLayout
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsLayoutStrategies
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.signal.core.ui.compose.AllNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import kotlin.math.min
@Composable
fun CallParticipantsPager(
@@ -43,6 +60,7 @@ fun CallParticipantsPager(
modifier = Modifier.fillMaxSize()
)
}
1 -> {
CallParticipantRenderer(
callParticipant = callParticipantsPagerState.focusedParticipant,
@@ -60,32 +78,259 @@ fun CallParticipantsPager(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CallParticipantsLayoutComponent(
callParticipantsPagerState: CallParticipantsPagerState,
modifier: Modifier = Modifier
) {
if (callParticipantsPagerState.focusedParticipant == null) {
return
val layoutStrategy = rememberRemoteParticipantsLayoutStrategy()
val count = min(callParticipantsPagerState.callParticipants.size, layoutStrategy.maxDeviceCount)
val state = remember(count) {
layoutStrategy.buildStateForCount(count)
}
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
AndroidView(
factory = {
LayoutInflater.from(it).inflate(R.layout.webrtc_call_participants_layout, FrameLayout(it), false) as CallParticipantsLayout
},
modifier = modifier
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = modifier.padding(state.outerInsets)
) {
it.update(
callParticipantsPagerState.callParticipants,
callParticipantsPagerState.focusedParticipant,
callParticipantsPagerState.isRenderInPip,
isPortrait,
callParticipantsPagerState.hideAvatar,
0,
CallParticipantsLayoutStrategies.getStrategy(isPortrait, true)
val width = maxWidth
val height = maxHeight
val nLines = (count + state.maxItemsInEachLine - 1) / state.maxItemsInEachLine
when (layoutStrategy.lineType) {
LayoutStrategyLineType.COLUMN -> {
ColumnBasedLayout(
containerWidth = width,
containerHeight = height,
numberOfLines = nLines,
numberOfParticipants = count,
state = state,
callParticipantsPagerState = callParticipantsPagerState
)
}
LayoutStrategyLineType.ROW -> {
RowBasedLayout(
containerWidth = width,
containerHeight = height,
numberOfLines = nLines,
numberOfParticipants = count,
state = state,
callParticipantsPagerState = callParticipantsPagerState
)
}
}
}
}
@Composable
private fun RowBasedLayout(
containerWidth: Dp,
containerHeight: Dp,
numberOfLines: Int,
numberOfParticipants: Int,
state: RemoteParticipantsLayoutState,
callParticipantsPagerState: CallParticipantsPagerState
) {
Row(
horizontalArrangement = spacedBy(state.innerInsets),
verticalAlignment = Alignment.CenterVertically
) {
val batches = callParticipantsPagerState.callParticipants
.take(numberOfParticipants)
.chunked(state.maxItemsInEachLine) {
it.reversed()
}
val lastParticipant = batches.last().last()
batches.forEach { batch ->
Column(
verticalArrangement = spacedBy(state.innerInsets),
horizontalAlignment = Alignment.CenterHorizontally
) {
batch.forEach { participant ->
AutoSizedParticipant(
lineType = LayoutStrategyLineType.ROW,
containerWidth = containerWidth,
containerHeight = containerHeight,
numberOfLines = numberOfLines,
numberOfParticipants = numberOfParticipants,
isLastParticipant = lastParticipant == participant,
isRenderInPip = callParticipantsPagerState.isRenderInPip,
state = state,
participant = participant
)
}
}
}
}
}
@Composable
private fun ColumnBasedLayout(
containerWidth: Dp,
containerHeight: Dp,
numberOfLines: Int,
numberOfParticipants: Int,
state: RemoteParticipantsLayoutState,
callParticipantsPagerState: CallParticipantsPagerState
) {
Column(
verticalArrangement = spacedBy(state.innerInsets),
horizontalAlignment = Alignment.CenterHorizontally
) {
val batches = callParticipantsPagerState.callParticipants
.take(numberOfParticipants)
.chunked(state.maxItemsInEachLine)
val lastParticipant = batches.last().last()
batches.forEach { batch ->
Row(
horizontalArrangement = spacedBy(state.innerInsets),
verticalAlignment = Alignment.CenterVertically
) {
batch.forEach { participant ->
AutoSizedParticipant(
lineType = LayoutStrategyLineType.COLUMN,
containerWidth = containerWidth,
containerHeight = containerHeight,
numberOfLines = numberOfLines,
numberOfParticipants = numberOfParticipants,
isLastParticipant = lastParticipant == participant,
isRenderInPip = callParticipantsPagerState.isRenderInPip,
state = state,
participant = participant
)
}
}
}
}
}
@Composable
private fun AutoSizedParticipant(
lineType: LayoutStrategyLineType,
containerWidth: Dp,
containerHeight: Dp,
numberOfLines: Int,
numberOfParticipants: Int,
isLastParticipant: Boolean,
isRenderInPip: Boolean,
state: RemoteParticipantsLayoutState,
participant: CallParticipant
) {
val maxSize = when (lineType) {
LayoutStrategyLineType.COLUMN -> {
val itemMaximumHeight = (containerHeight - (state.innerInsets * (numberOfLines - 1))) / numberOfLines.toFloat()
val itemMaximumWidth = (containerWidth - (state.innerInsets * (state.maxItemsInEachLine - 1))) / state.maxItemsInEachLine.toFloat()
DpSize(itemMaximumWidth, itemMaximumHeight)
}
LayoutStrategyLineType.ROW -> {
val itemMaximumWidth = (containerWidth - (state.innerInsets * (numberOfLines - 1))) / numberOfLines.toFloat()
val itemMaximumHeight = (containerHeight - (state.innerInsets * (state.maxItemsInEachLine - 1))) / state.maxItemsInEachLine.toFloat()
DpSize(itemMaximumWidth, itemMaximumHeight)
}
}
val aspectRatio = state.aspectRatio ?: -1f
val sizeModifier = when {
aspectRatio > 0f ->
Modifier.size(
largestRectangleWithAspectRatio(
maxSize.width,
maxSize.height,
aspectRatio
)
)
isLastParticipant && numberOfParticipants % 2 == 1 -> Modifier.fillMaxSize()
else -> Modifier.size(DpSize(maxSize.width, maxSize.height))
}
CallParticipantRenderer(
callParticipant = participant,
renderInPip = isRenderInPip,
modifier = sizeModifier
.clip(RoundedCornerShape(state.cornerRadius))
)
}
private fun largestRectangleWithAspectRatio(
containerWidth: Dp,
containerHeight: Dp,
aspectRatio: Float
): DpSize {
val containerAspectRatio = containerWidth / containerHeight
return if (containerAspectRatio > aspectRatio) {
DpSize(
width = containerHeight * aspectRatio,
height = containerHeight
)
} else {
DpSize(
width = containerWidth,
height = containerWidth / aspectRatio
)
}
}
@Composable
private fun rememberRemoteParticipantsLayoutStrategy(): RemoteParticipantsLayoutStrategy {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
return remember(windowSizeClass) {
when {
windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT -> RemoteParticipantsLayoutStrategy.SmallLandscape()
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT -> RemoteParticipantsLayoutStrategy.SmallPortrait()
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM -> RemoteParticipantsLayoutStrategy.Medium()
else -> RemoteParticipantsLayoutStrategy.Large()
}
}
}
@AllNightPreviews
@Composable
private fun CallParticipantsLayoutComponentPreview() {
Previews.Preview {
val participants = remember {
(1..5).map {
CallParticipant(
callParticipantId = CallParticipantId(0, RecipientId.from(it.toLong())),
recipient = Recipient(
isResolving = false,
chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT
)
)
}
}
val state = remember {
CallParticipantsPagerState(
callParticipants = participants,
focusedParticipant = participants.first(),
isRenderInPip = false,
hideAvatar = false
)
}
Surface(
modifier = Modifier.fillMaxSize()
) {
CallParticipantsLayoutComponent(
callParticipantsPagerState = state,
modifier = Modifier.fillMaxSize()
)
}
}
}
@@ -96,3 +341,76 @@ data class CallParticipantsPagerState(
val isRenderInPip: Boolean = false,
val hideAvatar: Boolean = false
)
private sealed class RemoteParticipantsLayoutStrategy(
val maxDeviceCount: Int,
val lineType: LayoutStrategyLineType = LayoutStrategyLineType.COLUMN
) {
abstract fun buildStateForCount(count: Int): RemoteParticipantsLayoutState
class SmallLandscape : RemoteParticipantsLayoutStrategy(6, LayoutStrategyLineType.ROW) {
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
return RemoteParticipantsLayoutState(
outerInsets = if (count < 2) 0.dp else 16.dp,
innerInsets = if (count < 2) 0.dp else 12.dp,
cornerRadius = if (count < 2) 0.dp else CallScreenMetrics.FocusedRendererCornerSize,
maxItemsInEachLine = if (count < 3) 1 else 2
)
}
}
class SmallPortrait : RemoteParticipantsLayoutStrategy(6) {
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
return RemoteParticipantsLayoutState(
outerInsets = if (count < 2) 0.dp else 16.dp,
innerInsets = if (count < 2) 0.dp else 12.dp,
cornerRadius = if (count < 2) 0.dp else CallScreenMetrics.FocusedRendererCornerSize,
maxItemsInEachLine = if (count < 3) 1 else 2
)
}
}
class Medium : RemoteParticipantsLayoutStrategy(9) {
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
return RemoteParticipantsLayoutState(
outerInsets = 24.dp,
innerInsets = 12.dp,
cornerRadius = CallScreenMetrics.FocusedRendererCornerSize,
aspectRatio = if (count < 2) 9 / 16f else 5 / 4f,
maxItemsInEachLine = when {
count < 3 -> 1
count < 7 -> 2
else -> 3
}
)
}
}
class Large : RemoteParticipantsLayoutStrategy(12) {
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
return RemoteParticipantsLayoutState(
outerInsets = 24.dp,
innerInsets = 12.dp,
cornerRadius = CallScreenMetrics.FocusedRendererCornerSize,
aspectRatio = if (count < 2) 9 / 16f else 5 / 4f,
maxItemsInEachLine = when {
count < 4 -> 3
count == 4 -> 2
count < 7 -> 3
else -> 4
}
)
}
}
}
@Immutable
private data class RemoteParticipantsLayoutState(
val outerInsets: Dp,
val innerInsets: Dp,
val cornerRadius: Dp,
val maxItemsInEachLine: Int,
val aspectRatio: Float? = null
)

View File

@@ -18,14 +18,11 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.BottomSheetScaffold
@@ -48,7 +45,6 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -61,10 +57,12 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState
import org.thoughtcrime.securesms.components.webrtc.controls.RaiseHandSnackbar
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent
import org.thoughtcrime.securesms.events.GroupCallReactionEvent
import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.CameraState
import kotlin.math.max
import kotlin.math.round
@@ -73,7 +71,6 @@ import kotlin.time.Duration.Companion.seconds
private const val DRAG_HANDLE_HEIGHT = 22
private const val SHEET_TOP_PADDING = 9
private const val SHEET_BOTTOM_PADDING = 16
private val OVERFLOW_ITEM_SIZE = 90.dp
/**
* In-App calling screen displaying controls, info, and participant camera feeds.
@@ -400,18 +397,13 @@ private fun Viewport(
if (isPortrait && isLargeGroupCall) {
Row {
CallParticipantsOverflow(
lineType = LayoutStrategyLineType.ROW,
overflowParticipants = overflowParticipants,
modifier = Modifier
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp)
.height(OVERFLOW_ITEM_SIZE)
.padding(vertical = 16.dp)
.height(CallScreenMetrics.SmallRendererSize)
.weight(1f)
)
Spacer(
modifier = Modifier
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp)
.size(OVERFLOW_ITEM_SIZE)
)
}
}
}
@@ -419,15 +411,13 @@ private fun Viewport(
if (!isPortrait && isLargeGroupCall) {
Column {
CallParticipantsOverflow(
lineType = LayoutStrategyLineType.COLUMN,
overflowParticipants = overflowParticipants,
modifier = Modifier
.width(OVERFLOW_ITEM_SIZE + 32.dp)
.padding(horizontal = 16.dp)
.width(CallScreenMetrics.SmallRendererSize)
.weight(1f)
)
Spacer(
modifier = Modifier.size(OVERFLOW_ITEM_SIZE)
)
}
}
}
@@ -500,6 +490,7 @@ private fun CallScreenPreview() {
val participants = remember {
(1..10).map {
CallParticipant(
callParticipantId = CallParticipantId(0, RecipientId.from(it.toLong())),
recipient = Recipient(
isResolving = false,
chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT
@@ -572,9 +563,3 @@ private fun CallScreenPreview() {
)
}
}
data class SelfPictureInPictureDimensions(
val small: DpSize,
val expanded: DpSize,
val paddingValues: PaddingValues
)

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.v2
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
object CallScreenMetrics {
val SmallRendererSize = 90.dp
val SmallRendererCornerSize = 24.dp
val ExpandedRendererCornerSize = 28.dp
val FocusedRendererCornerSize = 32.dp
/**
* Shape of self renderer when in large group calls.
*/
val SmallRendererShape = RoundedCornerShape(SmallRendererCornerSize)
/**
* Size of self renderer when in large group calls
*/
val SmallRendererDpSize = DpSize(SmallRendererSize, SmallRendererSize)
/**
* Size of self renderer when in small group calls and 1:1 calls
*/
val NormalRendererDpSize = DpSize(90.dp, 160.dp)
/**
* Size of self renderer after clicking on it to expand
*/
val ExpandedRendererDpSize = DpSize(170.dp, 300.dp)
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.v2
enum class LayoutStrategyLineType {
COLUMN,
ROW
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -39,7 +40,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
@@ -67,11 +67,11 @@ fun MoveableLocalVideoRenderer(
val size = remember(localRenderState) {
when (localRenderState) {
WebRtcLocalRenderState.GONE -> DpSize.Zero
WebRtcLocalRenderState.SMALL_RECTANGLE -> DpSize(90.dp, 160.dp)
WebRtcLocalRenderState.SMALLER_RECTANGLE -> DpSize(90.dp, 90.dp)
WebRtcLocalRenderState.SMALL_RECTANGLE -> CallScreenMetrics.NormalRendererDpSize
WebRtcLocalRenderState.SMALLER_RECTANGLE -> CallScreenMetrics.SmallRendererDpSize
WebRtcLocalRenderState.LARGE -> DpSize.Zero
WebRtcLocalRenderState.LARGE_NO_VIDEO -> DpSize.Zero
WebRtcLocalRenderState.EXPANDED -> DpSize(170.dp, 300.dp)
WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererDpSize
WebRtcLocalRenderState.FOCUSED -> DpSize.Unspecified
}
}
@@ -81,6 +81,7 @@ fun MoveableLocalVideoRenderer(
.fillMaxSize()
.then(modifier)
.statusBarsPadding()
.displayCutoutPadding()
) {
val targetSize = size.let {
if (it == DpSize.Unspecified) {
@@ -167,9 +168,9 @@ fun MoveableLocalVideoRenderer(
@Composable
private fun animateClip(localRenderState: WebRtcLocalRenderState): State<Dp> {
val targetDp = when (localRenderState) {
WebRtcLocalRenderState.FOCUSED -> 32.dp
WebRtcLocalRenderState.EXPANDED -> 28.dp
else -> 24.dp
WebRtcLocalRenderState.FOCUSED -> CallScreenMetrics.FocusedRendererCornerSize
WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererCornerSize
else -> CallScreenMetrics.SmallRendererCornerSize
}
return animateDpAsState(targetValue = targetDp)