Update self-pip placement in compose screen.

This commit is contained in:
Alex Hart
2025-08-21 12:49:31 -03:00
committed by Jeffrey Starke
parent 3c02ff0894
commit c117082f23
9 changed files with 284 additions and 115 deletions

View File

@@ -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)
}
)
)
}
}

View File

@@ -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 = {}
)
)
}

View File

@@ -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)
}
}
}

View File

@@ -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
) {

View File

@@ -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
)

View File

@@ -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
)
}
}

View File

@@ -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) {

View File

@@ -22,7 +22,7 @@ fun PictureInPictureCallScreen(
) {
val scope = rememberCoroutineScope()
CallParticipantsPager(
CallParticipantsLayoutComponent(
callParticipantsPagerState = callParticipantsPagerState,
modifier = Modifier
.fillMaxSize()

View File

@@ -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
)
}