mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Update self-pip placement in compose screen.
This commit is contained in:
committed by
Jeffrey Starke
parent
3c02ff0894
commit
c117082f23
@@ -20,6 +20,7 @@ import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -51,16 +52,15 @@ private const val SHOW_PICKER_THRESHOLD = 3
|
||||
*/
|
||||
@Composable
|
||||
fun CallAudioToggleButton(
|
||||
outputState: ToggleButtonOutputState,
|
||||
contentDescription: String,
|
||||
onSelectedDeviceChanged: (WebRtcAudioDevice) -> Unit,
|
||||
onSheetDisplayChanged: (Boolean) -> Unit,
|
||||
pickerController: AudioOutputPickerController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val buttonSize = dimensionResource(id = R.dimen.webrtc_button_size)
|
||||
|
||||
val currentOutput = outputState.currentDevice
|
||||
val allOutputs = outputState.availableDevices
|
||||
val currentOutput = pickerController.outputState.currentDevice
|
||||
val allOutputs = pickerController.outputState.availableDevices
|
||||
|
||||
val containerColor = if (currentOutput == WebRtcAudioOutput.HANDSET || allOutputs.size >= SHOW_PICKER_THRESHOLD) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
@@ -74,11 +74,6 @@ fun CallAudioToggleButton(
|
||||
colorResource(id = R.color.signal_light_colorOnSecondaryContainer)
|
||||
}
|
||||
|
||||
val pickerController = rememberPickerController(
|
||||
onSelectedDeviceChanged = onSelectedDeviceChanged,
|
||||
outputState = outputState
|
||||
)
|
||||
|
||||
IconButtons.IconButton(
|
||||
size = buttonSize,
|
||||
onClick = {
|
||||
@@ -125,12 +120,12 @@ fun CallAudioToggleButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberPickerController(
|
||||
fun rememberAudioOutputPickerController(
|
||||
onSelectedDeviceChanged: (WebRtcAudioDevice) -> Unit,
|
||||
outputState: ToggleButtonOutputState
|
||||
): PickerController {
|
||||
): AudioOutputPickerController {
|
||||
return remember(onSelectedDeviceChanged, outputState) {
|
||||
PickerController(
|
||||
AudioOutputPickerController(
|
||||
onSelectedDeviceChanged,
|
||||
outputState
|
||||
)
|
||||
@@ -141,9 +136,10 @@ private fun rememberPickerController(
|
||||
* Controller for the Audio picker which contains different state variables for choosing whether
|
||||
* or not to display the sheet.
|
||||
*/
|
||||
private class PickerController(
|
||||
@Stable
|
||||
class AudioOutputPickerController(
|
||||
private val onSelectedDeviceChanged: (WebRtcAudioDevice) -> Unit,
|
||||
private val outputState: ToggleButtonOutputState
|
||||
val outputState: ToggleButtonOutputState
|
||||
) {
|
||||
|
||||
var displaySheet: Boolean by mutableStateOf(false)
|
||||
@@ -172,6 +168,10 @@ private class PickerController(
|
||||
displaySheet = true
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
displaySheet = false
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Sheet() {
|
||||
@@ -291,12 +291,14 @@ private fun TwoDeviceCallAudioToggleButtonPreview() {
|
||||
|
||||
Previews.Preview {
|
||||
CallAudioToggleButton(
|
||||
outputState = outputState,
|
||||
contentDescription = "",
|
||||
onSelectedDeviceChanged = {
|
||||
outputState.setCurrentOutput(it.webRtcAudioOutput)
|
||||
},
|
||||
onSheetDisplayChanged = {}
|
||||
onSheetDisplayChanged = {},
|
||||
pickerController = rememberAudioOutputPickerController(
|
||||
outputState = outputState,
|
||||
onSelectedDeviceChanged = {
|
||||
outputState.setCurrentOutput(it.webRtcAudioOutput)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -313,12 +315,14 @@ private fun ThreeDeviceCallAudioToggleButtonPreview() {
|
||||
|
||||
Previews.Preview {
|
||||
CallAudioToggleButton(
|
||||
outputState = outputState,
|
||||
contentDescription = "",
|
||||
onSelectedDeviceChanged = {
|
||||
outputState.setCurrentOutput(it.webRtcAudioOutput)
|
||||
},
|
||||
onSheetDisplayChanged = {}
|
||||
onSheetDisplayChanged = {},
|
||||
pickerController = rememberAudioOutputPickerController(
|
||||
outputState = outputState,
|
||||
onSelectedDeviceChanged = {
|
||||
outputState.setCurrentOutput(it.webRtcAudioOutput)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -16,7 +15,6 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -36,7 +34,6 @@ import org.signal.core.ui.compose.TriggerAlignedPopupState.Companion.rememberTri
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.ToggleButtonOutputState
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
@@ -53,6 +50,7 @@ fun CallControls(
|
||||
callScreenControlsListener: CallScreenControlsListener,
|
||||
callScreenSheetDisplayListener: CallScreenSheetDisplayListener,
|
||||
additionalActionsState: AdditionalActionsState,
|
||||
audioOutputPickerController: AudioOutputPickerController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
@@ -76,37 +74,10 @@ fun CallControls(
|
||||
horizontalArrangement = spacedBy(20.dp)
|
||||
) {
|
||||
if (callControlsState.displayAudioOutputToggle) {
|
||||
val outputState = remember {
|
||||
ToggleButtonOutputState().apply {
|
||||
isEarpieceAvailable = callControlsState.isEarpieceAvailable
|
||||
isWiredHeadsetAvailable = callControlsState.isWiredHeadsetAvailable
|
||||
isBluetoothHeadsetAvailable = callControlsState.isBluetoothHeadsetAvailable
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(callControlsState.isEarpieceAvailable, callControlsState.isWiredHeadsetAvailable, callControlsState.isBluetoothHeadsetAvailable) {
|
||||
outputState.apply {
|
||||
isEarpieceAvailable = callControlsState.isEarpieceAvailable
|
||||
isWiredHeadsetAvailable = callControlsState.isWiredHeadsetAvailable
|
||||
isBluetoothHeadsetAvailable = callControlsState.isBluetoothHeadsetAvailable
|
||||
}
|
||||
}
|
||||
|
||||
val onSelectedAudioDeviceChanged: (WebRtcAudioDevice) -> Unit = remember {
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
callScreenControlsListener.onAudioOutputChanged31(it)
|
||||
} else {
|
||||
callScreenControlsListener.onAudioOutputChanged(it.webRtcAudioOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallAudioToggleButton(
|
||||
outputState = outputState,
|
||||
contentDescription = stringResource(id = R.string.WebRtcAudioOutputToggle__audio_output),
|
||||
onSelectedDeviceChanged = onSelectedAudioDeviceChanged,
|
||||
onSheetDisplayChanged = callScreenSheetDisplayListener::onAudioDeviceSheetDisplayChanged
|
||||
onSheetDisplayChanged = callScreenSheetDisplayListener::onAudioDeviceSheetDisplayChanged,
|
||||
pickerController = audioOutputPickerController
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,6 +167,10 @@ fun CallControlsPreview() {
|
||||
callScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty,
|
||||
additionalActionsState = AdditionalActionsState(
|
||||
triggerAlignedPopupState = rememberTriggerAlignedPopupState()
|
||||
),
|
||||
audioOutputPickerController = AudioOutputPickerController(
|
||||
outputState = ToggleButtonOutputState(),
|
||||
onSelectedDeviceChanged = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ import org.webrtc.RendererCommon
|
||||
* Displays video for the local participant or an appropriate avatar.
|
||||
*/
|
||||
@Composable
|
||||
fun LocalParticipantRenderer(
|
||||
localParticipant: CallParticipant,
|
||||
fun CallParticipantRenderer(
|
||||
callParticipant: CallParticipant,
|
||||
modifier: Modifier = Modifier,
|
||||
force: Boolean = false
|
||||
) {
|
||||
@@ -50,7 +50,7 @@ fun LocalParticipantRenderer(
|
||||
val localRecipient = if (LocalInspectionMode.current) {
|
||||
Recipient()
|
||||
} else {
|
||||
localParticipant.recipient
|
||||
callParticipant.recipient
|
||||
}
|
||||
|
||||
val model = remember {
|
||||
@@ -69,20 +69,20 @@ fun LocalParticipantRenderer(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
if (force || localParticipant.isVideoEnabled) {
|
||||
if (force || callParticipant.isVideoEnabled) {
|
||||
AndroidView(
|
||||
factory = ::TextureViewRenderer,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onRelease = { it.release() }
|
||||
) { renderer ->
|
||||
renderer.setMirror(localParticipant.cameraDirection == CameraState.Direction.FRONT)
|
||||
renderer.setMirror(callParticipant.cameraDirection == CameraState.Direction.FRONT)
|
||||
renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
|
||||
|
||||
localParticipant.videoSink.lockableEglBase.performWithValidEglBase {
|
||||
callParticipant.videoSink.lockableEglBase.performWithValidEglBase {
|
||||
renderer.init(it)
|
||||
}
|
||||
|
||||
renderer.attachBroadcastVideoSink(localParticipant.videoSink)
|
||||
renderer.attachBroadcastVideoSink(callParticipant.videoSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ 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.fillMaxSize
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -21,16 +24,43 @@ import org.thoughtcrime.securesms.events.CallParticipant
|
||||
@Composable
|
||||
fun CallParticipantsPager(
|
||||
callParticipantsPagerState: CallParticipantsPagerState,
|
||||
pagerState: PagerState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
modifier = modifier
|
||||
)
|
||||
if (callParticipantsPagerState.focusedParticipant == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (callParticipantsPagerState.callParticipants.size > 1) {
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
1 -> {
|
||||
CallParticipantRenderer(
|
||||
callParticipant = callParticipantsPagerState.focusedParticipant,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallParticipantsLayoutComponent(
|
||||
fun CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState: CallParticipantsPagerState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
@@ -14,17 +14,20 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
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.fillMaxHeight
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
@@ -38,6 +41,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
@@ -46,9 +50,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onPlaced
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -67,6 +76,7 @@ import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.GroupCallReactionEvent
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
import kotlin.math.max
|
||||
import kotlin.math.round
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -89,7 +99,9 @@ fun CallScreen(
|
||||
callControlsState: CallControlsState,
|
||||
callScreenController: CallScreenController = CallScreenController.rememberCallScreenController(
|
||||
skipHiddenState = callControlsState.skipHiddenState,
|
||||
onControlsToggled = {}
|
||||
onControlsToggled = {},
|
||||
callControlsState = callControlsState,
|
||||
callControlsListener = CallScreenControlsListener.Empty
|
||||
),
|
||||
callScreenControlsListener: CallScreenControlsListener = CallScreenControlsListener.Empty,
|
||||
callScreenSheetDisplayListener: CallScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty,
|
||||
@@ -199,6 +211,7 @@ fun CallScreen(
|
||||
callScreenSheetDisplayListener = callScreenSheetDisplayListener,
|
||||
displayVideoTooltip = callScreenState.displayVideoTooltip,
|
||||
additionalActionsState = additionalActionsState,
|
||||
audioOutputPickerController = callScreenController.audioOutputPickerController,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(callControlsAlpha)
|
||||
@@ -339,14 +352,15 @@ private fun BoxScope.Viewport(
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = modifier.fillMaxWidth()) {
|
||||
val overflowSize = dimensionResource(R.dimen.call_screen_overflow_item_size)
|
||||
var spacerOffset by remember { mutableStateOf(Offset.Zero) }
|
||||
|
||||
Row(modifier = modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
CallParticipantsPager(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
pagerState = callScreenController.callParticipantsVerticalPagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
@@ -361,23 +375,50 @@ private fun BoxScope.Viewport(
|
||||
)
|
||||
|
||||
if (isPortrait && isLargeGroupCall) {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.height(overflowSize)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
val overflowSize = dimensionResource(R.dimen.call_screen_overflow_item_size)
|
||||
val selfPipSize = rememberTinyPortraitSize()
|
||||
|
||||
Row {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp)
|
||||
.height(overflowSize)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.onPlaced { coordinates ->
|
||||
spacerOffset = coordinates.localToRoot(Offset.Zero)
|
||||
}
|
||||
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp)
|
||||
.size(selfPipSize.small)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPortrait && isLargeGroupCall) {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.width(overflowSize + 32.dp)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
val overflowSize = dimensionResource(R.dimen.call_screen_overflow_item_size)
|
||||
val selfPipSize = rememberTinyPortraitSize()
|
||||
|
||||
Column {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.width(overflowSize + 32.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.onPlaced { coordinates ->
|
||||
spacerOffset = coordinates.localToRoot(Offset.Zero)
|
||||
}
|
||||
.size(selfPipSize.small)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +426,16 @@ private fun BoxScope.Viewport(
|
||||
TinyLocalVideoRenderer(
|
||||
localParticipant = localParticipant,
|
||||
localRenderState = localRenderState,
|
||||
modifier = Modifier.align(Alignment.BottomEnd),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(
|
||||
start = with(LocalDensity.current) {
|
||||
spacerOffset.x.toDp()
|
||||
},
|
||||
top = with(LocalDensity.current) {
|
||||
spacerOffset.y.toDp()
|
||||
}
|
||||
),
|
||||
onClick = onPipClick
|
||||
)
|
||||
}
|
||||
@@ -409,8 +459,8 @@ private fun LargeLocalVideoRenderer(
|
||||
localParticipant: CallParticipant,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LocalParticipantRenderer(
|
||||
localParticipant = localParticipant,
|
||||
CallParticipantRenderer(
|
||||
callParticipant = localParticipant,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
)
|
||||
@@ -426,24 +476,29 @@ private fun TinyLocalVideoRenderer(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
val smallSize = remember(isPortrait) {
|
||||
if (isPortrait) DpSize(40.dp, 72.dp) else DpSize(72.dp, 40.dp)
|
||||
}
|
||||
val expandedSize = remember(isPortrait) {
|
||||
if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp)
|
||||
}
|
||||
|
||||
val (smallSize, expandedSize, padding) = rememberTinyPortraitSize()
|
||||
val size = if (localRenderState == WebRtcLocalRenderState.EXPANDED) expandedSize else smallSize
|
||||
|
||||
val width by animateDpAsState(label = "tiny-width", targetValue = size.width)
|
||||
val height by animateDpAsState(label = "tiny-height", targetValue = size.height)
|
||||
|
||||
LocalParticipantRenderer(
|
||||
localParticipant = localParticipant,
|
||||
if (LocalInspectionMode.current) {
|
||||
Text(
|
||||
"Test ${WindowSizeClass.rememberWindowSizeClass()}",
|
||||
modifier = modifier
|
||||
.padding(padding)
|
||||
.height(height)
|
||||
.width(width)
|
||||
.background(color = Color.Red)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
CallParticipantRenderer(
|
||||
callParticipant = localParticipant,
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
.padding(padding)
|
||||
.height(height)
|
||||
.width(width)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
@@ -483,8 +538,8 @@ private fun SmallMoveableLocalVideoRenderer(
|
||||
.padding(16.dp)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
LocalParticipantRenderer(
|
||||
localParticipant = localParticipant,
|
||||
CallParticipantRenderer(
|
||||
callParticipant = localParticipant,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
@@ -495,6 +550,34 @@ private fun SmallMoveableLocalVideoRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberTinyPortraitSize(): SelfPictureInPictureDimensions {
|
||||
val smallWidth = dimensionResource(R.dimen.call_screen_overflow_item_size)
|
||||
val windowClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
val smallSize = when (windowClass) {
|
||||
WindowSizeClass.COMPACT_PORTRAIT -> DpSize(40.dp, smallWidth)
|
||||
WindowSizeClass.COMPACT_LANDSCAPE -> DpSize(smallWidth, 40.dp)
|
||||
WindowSizeClass.EXTENDED_PORTRAIT, WindowSizeClass.EXTENDED_LANDSCAPE -> DpSize(124.dp, 217.dp)
|
||||
else -> DpSize(smallWidth, smallWidth)
|
||||
}
|
||||
|
||||
val expandedSize = when (windowClass) {
|
||||
WindowSizeClass.COMPACT_PORTRAIT -> DpSize(180.dp, 320.dp)
|
||||
WindowSizeClass.COMPACT_LANDSCAPE -> DpSize(320.dp, 180.dp)
|
||||
else -> DpSize(smallWidth, smallWidth)
|
||||
}
|
||||
|
||||
val padding = when (windowClass) {
|
||||
WindowSizeClass.COMPACT_PORTRAIT -> PaddingValues(vertical = 16.dp)
|
||||
else -> PaddingValues(16.dp)
|
||||
}
|
||||
|
||||
return remember(windowClass) {
|
||||
SelfPictureInPictureDimensions(smallSize, expandedSize, padding)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for a CallStateUpdate popup that animates its display on the screen, sliding up from either
|
||||
* above the controls or from the bottom of the screen if the controls are hidden.
|
||||
@@ -566,7 +649,8 @@ private fun CallScreenPreview() {
|
||||
recipient = Recipient(
|
||||
isResolving = false,
|
||||
isSelf = true
|
||||
)
|
||||
),
|
||||
isVideoEnabled = true
|
||||
),
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE,
|
||||
callScreenDialogType = CallScreenDialogType.NONE,
|
||||
@@ -582,3 +666,9 @@ private fun CallScreenPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SelfPictureInPictureDimensions(
|
||||
val small: DpSize,
|
||||
val expanded: DpSize,
|
||||
val paddingValues: PaddingValues
|
||||
)
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.BottomSheetScaffoldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -20,13 +25,18 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import org.thoughtcrime.securesms.components.webrtc.ToggleButtonOutputState
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
|
||||
/**
|
||||
* Collects and manages state objects for manipulating the call screen UI programatically.
|
||||
*/
|
||||
@Stable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class CallScreenController private constructor(
|
||||
val scaffoldState: BottomSheetScaffoldState,
|
||||
val audioOutputPickerController: AudioOutputPickerController,
|
||||
val callParticipantsVerticalPagerState: PagerState,
|
||||
val onControlsToggled: (Boolean) -> Unit
|
||||
) {
|
||||
|
||||
@@ -34,8 +44,14 @@ class CallScreenController private constructor(
|
||||
|
||||
suspend fun handleEvent(event: Event) {
|
||||
when (event) {
|
||||
Event.SWITCH_TO_SPEAKER_VIEW -> {} // TODO [calling-v2]
|
||||
Event.DISMISS_AUDIO_PICKER -> {} // TODO [calling-v2]
|
||||
Event.SWITCH_TO_SPEAKER_VIEW -> {
|
||||
callParticipantsVerticalPagerState.animateScrollToPage(1)
|
||||
}
|
||||
|
||||
Event.DISMISS_AUDIO_PICKER -> {
|
||||
audioOutputPickerController.hide()
|
||||
}
|
||||
|
||||
Event.TOGGLE_CONTROLS -> {
|
||||
if (scaffoldState.bottomSheetState.isVisible) {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
@@ -58,7 +74,12 @@ class CallScreenController private constructor(
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun rememberCallScreenController(skipHiddenState: Boolean, onControlsToggled: (Boolean) -> Unit): CallScreenController {
|
||||
fun rememberCallScreenController(
|
||||
skipHiddenState: Boolean,
|
||||
onControlsToggled: (Boolean) -> Unit,
|
||||
callControlsState: CallControlsState,
|
||||
callControlsListener: CallScreenControlsListener
|
||||
): CallScreenController {
|
||||
val skip by rememberUpdatedState(skipHiddenState)
|
||||
val valueChangeOperation: (SheetValue) -> Boolean = remember {
|
||||
{
|
||||
@@ -73,10 +94,48 @@ class CallScreenController private constructor(
|
||||
)
|
||||
)
|
||||
|
||||
return remember(scaffoldState) {
|
||||
val onSelectedAudioDeviceChanged: (WebRtcAudioDevice) -> Unit = remember {
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
callControlsListener.onAudioOutputChanged31(it)
|
||||
} else {
|
||||
callControlsListener.onAudioOutputChanged(it.webRtcAudioOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val audioOutputPickerOutputState = remember {
|
||||
ToggleButtonOutputState().apply {
|
||||
isEarpieceAvailable = callControlsState.isEarpieceAvailable
|
||||
isWiredHeadsetAvailable = callControlsState.isWiredHeadsetAvailable
|
||||
isBluetoothHeadsetAvailable = callControlsState.isBluetoothHeadsetAvailable
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(callControlsState.isEarpieceAvailable, callControlsState.isWiredHeadsetAvailable, callControlsState.isBluetoothHeadsetAvailable) {
|
||||
audioOutputPickerOutputState.apply {
|
||||
isEarpieceAvailable = callControlsState.isEarpieceAvailable
|
||||
isWiredHeadsetAvailable = callControlsState.isWiredHeadsetAvailable
|
||||
isBluetoothHeadsetAvailable = callControlsState.isBluetoothHeadsetAvailable
|
||||
}
|
||||
}
|
||||
|
||||
val audioOutputPickerController = rememberAudioOutputPickerController(
|
||||
onSelectedDeviceChanged = onSelectedAudioDeviceChanged,
|
||||
outputState = audioOutputPickerOutputState
|
||||
)
|
||||
|
||||
val callParticipantsVerticalPagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 2 }
|
||||
)
|
||||
|
||||
return remember(scaffoldState, callParticipantsVerticalPagerState, audioOutputPickerController) {
|
||||
CallScreenController(
|
||||
scaffoldState = scaffoldState,
|
||||
onControlsToggled = onControlsToggled
|
||||
onControlsToggled = onControlsToggled,
|
||||
callParticipantsVerticalPagerState = callParticipantsVerticalPagerState,
|
||||
audioOutputPickerController = audioOutputPickerController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,9 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
|
||||
val callScreenController = CallScreenController.rememberCallScreenController(
|
||||
skipHiddenState = callControlsState.skipHiddenState,
|
||||
onControlsToggled = onControlsToggled
|
||||
onControlsToggled = onControlsToggled,
|
||||
callControlsState = callControlsState,
|
||||
callControlsListener = callScreenControlsListener
|
||||
)
|
||||
|
||||
LaunchedEffect(callScreenController) {
|
||||
|
||||
@@ -22,7 +22,7 @@ fun PictureInPictureCallScreen(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
CallParticipantsPager(
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -83,6 +83,7 @@ enum class WindowSizeClass(
|
||||
fun isExtended(): Boolean = this == EXTENDED_PORTRAIT || this == EXTENDED_LANDSCAPE
|
||||
|
||||
fun isLandscape(): Boolean = this == COMPACT_LANDSCAPE || this == MEDIUM_LANDSCAPE || this == EXTENDED_LANDSCAPE
|
||||
fun isPortrait(): Boolean = !isLandscape()
|
||||
|
||||
fun isSplitPane(): Boolean {
|
||||
return if (SignalStore.internal.largeScreenUi && SignalStore.internal.forceSplitPaneOnCompactLandscape) {
|
||||
@@ -159,10 +160,16 @@ enum class WindowSizeClass(
|
||||
}
|
||||
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
when (windowSizeClass.windowHeightSizeClass) {
|
||||
WindowHeightSizeClass.COMPACT -> COMPACT_LANDSCAPE
|
||||
WindowHeightSizeClass.MEDIUM -> MEDIUM_LANDSCAPE
|
||||
WindowHeightSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
|
||||
when (windowSizeClass.windowWidthSizeClass) {
|
||||
WindowWidthSizeClass.COMPACT -> COMPACT_LANDSCAPE
|
||||
WindowWidthSizeClass.MEDIUM -> {
|
||||
if (windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT) {
|
||||
COMPACT_LANDSCAPE
|
||||
} else {
|
||||
MEDIUM_LANDSCAPE
|
||||
}
|
||||
}
|
||||
WindowWidthSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
|
||||
else -> error("Unsupported.")
|
||||
}
|
||||
}
|
||||
@@ -261,6 +268,8 @@ private fun ListAndNavigation(
|
||||
@Composable
|
||||
private fun AppScaffoldPreview() {
|
||||
Previews.Preview {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
AppScaffold(
|
||||
navigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
@@ -277,7 +286,7 @@ private fun AppScaffoldPreview() {
|
||||
.background(color = Color.Red)
|
||||
) {
|
||||
Text(
|
||||
text = "ListContent",
|
||||
text = "ListContent\n$windowSizeClass",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user