mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 16:49:40 +01:00
Rewrite layout for call participants to match new spec.
This commit is contained in:
committed by
Cody Henthorne
parent
9f0f8b7cbc
commit
69d2ad410f
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user