mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 11:08:31 +00:00
Various picture in picture updates.
This commit is contained in:
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.avatar
|
package org.thoughtcrime.securesms.avatar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -14,8 +14,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import org.signal.core.ui.compose.DayNightPreviews
|
||||||
|
import org.signal.core.ui.compose.Previews
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
|
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
@@ -44,9 +49,11 @@ fun AvatarImage(
|
|||||||
contentDescription: String? = null
|
contentDescription: String? = null
|
||||||
) {
|
) {
|
||||||
if (LocalInspectionMode.current) {
|
if (LocalInspectionMode.current) {
|
||||||
Spacer(
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_avatar_abstract_02),
|
||||||
|
contentDescription = null,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(color = Color.Red, shape = CircleShape)
|
.background(color = Color(AvatarColor.random().colorInt()), CircleShape)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -76,6 +83,16 @@ fun AvatarImage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
private fun AvatarImagePreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
AvatarImage(
|
||||||
|
recipientId = RecipientId.from(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class AvatarImageState(
|
private data class AvatarImageState(
|
||||||
val displayName: String?,
|
val displayName: String?,
|
||||||
val self: Recipient,
|
val self: Recipient,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ object CallInfoView {
|
|||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallInfoPreview() {
|
private fun CallInfoPreview() {
|
||||||
Previews.Preview {
|
Previews.BottomSheetContentPreview {
|
||||||
val remoteParticipants = listOf(CallParticipant(recipient = Recipient(isResolving = false, systemContactName = "Miles Morales")))
|
val remoteParticipants = listOf(CallParticipant(recipient = Recipient(isResolving = false, systemContactName = "Miles Morales")))
|
||||||
CallInfo(
|
CallInfo(
|
||||||
participantsState = ParticipantsState(remoteParticipants = remoteParticipants, raisedHands = remoteParticipants.map { GroupCallRaiseHandEvent(it, System.currentTimeMillis()) }),
|
participantsState = ParticipantsState(remoteParticipants = remoteParticipants, raisedHands = remoteParticipants.map { GroupCallRaiseHandEvent(it, System.currentTimeMillis()) }),
|
||||||
@@ -162,9 +162,7 @@ private fun CallInfo(
|
|||||||
item {
|
item {
|
||||||
val text = if (controlAndInfoState.callLink == null) {
|
val text = if (controlAndInfoState.callLink == null) {
|
||||||
stringResource(id = R.string.CallLinkInfoSheet__call_info)
|
stringResource(id = R.string.CallLinkInfoSheet__call_info)
|
||||||
} else if (controlAndInfoState.callLink.state.name.isNotEmpty()) {
|
} else controlAndInfoState.callLink.state.name.ifEmpty {
|
||||||
controlAndInfoState.callLink.state.name
|
|
||||||
} else {
|
|
||||||
stringResource(id = R.string.Recipient_signal_call)
|
stringResource(id = R.string.Recipient_signal_call)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import org.thoughtcrime.securesms.events.CallParticipant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays video for the local participant or an appropriate avatar.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CallParticipantRenderer(
|
|
||||||
callParticipant: CallParticipant,
|
|
||||||
renderInPip: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isRaiseHandAllowed: Boolean = false,
|
|
||||||
selfPipMode: SelfPipMode = SelfPipMode.NOT_SELF_PIP,
|
|
||||||
onToggleCameraDirection: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
CallParticipantViewer(
|
|
||||||
participant = callParticipant,
|
|
||||||
renderInPip = renderInPip,
|
|
||||||
raiseHandAllowed = isRaiseHandAllowed,
|
|
||||||
selfPipMode = selfPipMode,
|
|
||||||
isMoreThanOneCameraAvailable = callParticipant.cameraState.cameraCount > 1,
|
|
||||||
onSwitchCameraClick = if (selfPipMode != SelfPipMode.NOT_SELF_PIP) {
|
|
||||||
{ onToggleCameraDirection() }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,8 @@ package org.thoughtcrime.securesms.components.webrtc.v2
|
|||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -22,6 +24,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -50,26 +53,30 @@ import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageLarge
|
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageLarge
|
||||||
import org.thoughtcrime.securesms.components.webrtc.AudioIndicatorView
|
import org.thoughtcrime.securesms.components.webrtc.AudioIndicatorView
|
||||||
import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||||
|
import org.thoughtcrime.securesms.compose.GlideImage
|
||||||
|
import org.thoughtcrime.securesms.compose.GlideImageScaleType
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
|
||||||
import org.thoughtcrime.securesms.events.CallParticipant
|
import org.thoughtcrime.securesms.events.CallParticipant
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.AvatarUtil
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates views needed to show a call participant including their
|
* Displays a remote participant (or local participant in pre-join screen).
|
||||||
* avatar in full screen or pip mode, and their video feed.
|
* Handles both full-size grid view and system PIP mode.
|
||||||
*
|
*
|
||||||
* This is a Compose reimplementation of [org.thoughtcrime.securesms.components.webrtc.CallParticipantView].
|
* This is a Compose reimplementation of [org.thoughtcrime.securesms.components.webrtc.CallParticipantView].
|
||||||
|
*
|
||||||
|
* @param participant The call participant to display
|
||||||
|
* @param renderInPip Whether rendering in system PIP mode (smaller, simplified UI)
|
||||||
|
* @param raiseHandAllowed Whether to show raise hand indicator
|
||||||
|
* @param onInfoMoreInfoClick Callback when "More Info" is tapped on blocked/missing keys overlay
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CallParticipantViewer(
|
fun RemoteParticipantContent(
|
||||||
participant: CallParticipant,
|
participant: CallParticipant,
|
||||||
modifier: Modifier = Modifier,
|
renderInPip: Boolean,
|
||||||
renderInPip: Boolean = false,
|
raiseHandAllowed: Boolean,
|
||||||
raiseHandAllowed: Boolean = false,
|
onInfoMoreInfoClick: (() -> Unit)?,
|
||||||
selfPipMode: SelfPipMode = SelfPipMode.NOT_SELF_PIP,
|
modifier: Modifier = Modifier
|
||||||
isMoreThanOneCameraAvailable: Boolean = false,
|
|
||||||
onSwitchCameraClick: (() -> Unit)? = null,
|
|
||||||
onInfoMoreInfoClick: (() -> Unit)? = null
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val recipient = participant.recipient
|
val recipient = participant.recipient
|
||||||
@@ -96,24 +103,63 @@ fun CallParticipantViewer(
|
|||||||
participant = participant,
|
participant = participant,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
} else if (renderInPip) {
|
||||||
|
PipAvatar(
|
||||||
|
recipient = recipient,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (!renderInPip) {
|
AvatarWithBadge(
|
||||||
AvatarWithBadge(
|
recipient = recipient,
|
||||||
recipient = recipient,
|
modifier = Modifier.align(Alignment.Center)
|
||||||
modifier = Modifier.align(Alignment.Center)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderInPip) {
|
|
||||||
PipAvatar(
|
|
||||||
recipient = recipient,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AudioIndicator(
|
||||||
|
participant = participant,
|
||||||
|
selfPipMode = SelfPipMode.NOT_SELF_PIP,
|
||||||
|
modifier = Modifier.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (raiseHandAllowed && !renderInPip && participant.isHandRaised) {
|
||||||
|
RaiseHandIndicator(
|
||||||
|
name = participant.getShortRecipientDisplayName(context),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(start = 8.dp, top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the local camera preview overlay (self PIP).
|
||||||
|
* Shows video feed with audio indicator and camera switch button when camera is enabled.
|
||||||
|
* Shows a blurred gray background with camera-off icon when camera is disabled.
|
||||||
|
*
|
||||||
|
* @param participant The local call participant
|
||||||
|
* @param selfPipMode The current self-pip display mode
|
||||||
|
* @param isMoreThanOneCameraAvailable Whether to show camera switch button
|
||||||
|
* @param onSwitchCameraClick Callback when camera switch is tapped
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SelfPipContent(
|
||||||
|
participant: CallParticipant,
|
||||||
|
selfPipMode: SelfPipMode,
|
||||||
|
isMoreThanOneCameraAvailable: Boolean,
|
||||||
|
onSwitchCameraClick: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (participant.isVideoEnabled) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
VideoRenderer(
|
||||||
|
participant = participant,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
AudioIndicator(
|
AudioIndicator(
|
||||||
participant = participant,
|
participant = participant,
|
||||||
selfPipMode = selfPipMode,
|
selfPipMode = selfPipMode,
|
||||||
@@ -125,22 +171,99 @@ fun CallParticipantViewer(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (selfPipMode != SelfPipMode.NOT_SELF_PIP && isMoreThanOneCameraAvailable && selfPipMode != SelfPipMode.MINI_SELF_PIP) {
|
if (isMoreThanOneCameraAvailable && selfPipMode != SelfPipMode.MINI_SELF_PIP) {
|
||||||
SwitchCameraButton(
|
SwitchCameraButton(
|
||||||
selfPipMode = selfPipMode,
|
selfPipMode = selfPipMode,
|
||||||
onClick = onSwitchCameraClick,
|
onClick = onSwitchCameraClick,
|
||||||
modifier = Modifier.align(Alignment.BottomEnd)
|
modifier = Modifier.align(Alignment.BottomEnd)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SelfPipCameraOffContent(
|
||||||
|
participant = participant,
|
||||||
|
selfPipMode = selfPipMode,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (raiseHandAllowed && participant.isHandRaised) {
|
/**
|
||||||
RaiseHandIndicator(
|
* Camera-off state for self PIP.
|
||||||
name = participant.getShortRecipientDisplayName(context),
|
* Shows a blurred avatar background with a semi-transparent gray overlay,
|
||||||
modifier = Modifier
|
* centered video-off icon, and audio indicator in lower-start.
|
||||||
.align(Alignment.TopStart)
|
*/
|
||||||
.padding(start = 8.dp, top = 8.dp)
|
@Composable
|
||||||
|
private fun SelfPipCameraOffContent(
|
||||||
|
participant: CallParticipant,
|
||||||
|
selfPipMode: SelfPipMode,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
BlurredBackgroundAvatar(recipient = participant.recipient)
|
||||||
|
|
||||||
|
// Semi-transparent overlay
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
color = Color(0x995E5E5E), // rgba(94, 94, 94, 0.6)
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.symbol_video_slash_fill_24),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
|
||||||
|
AudioIndicator(
|
||||||
|
participant = participant,
|
||||||
|
selfPipMode = selfPipMode,
|
||||||
|
modifier = Modifier.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a remote participant in the overflow strip.
|
||||||
|
* Shows video if enabled, otherwise shows the participant's avatar filling the tile.
|
||||||
|
*
|
||||||
|
* This is a simplified version of [RemoteParticipantContent] that:
|
||||||
|
* - Always renders in "pip mode" style (avatar fills the space when video is off)
|
||||||
|
* - Uses the same audio indicator metrics as [SelfPipMode.MINI_SELF_PIP]
|
||||||
|
* - Does not show raise hand indicators or info overlays
|
||||||
|
*
|
||||||
|
* @param participant The call participant to display
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun OverflowParticipantContent(
|
||||||
|
participant: CallParticipant,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val recipient = participant.recipient
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
BlurredBackgroundAvatar(recipient = recipient)
|
||||||
|
|
||||||
|
val hasContentToRender = participant.isVideoEnabled || participant.isScreenSharing
|
||||||
|
|
||||||
|
if (hasContentToRender) {
|
||||||
|
VideoRenderer(
|
||||||
|
participant = participant,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PipAvatar(
|
||||||
|
recipient = recipient,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(rememberCallScreenMetrics().overflowParticipantRendererAvatarSize)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,28 +273,34 @@ private fun BlurredBackgroundAvatar(
|
|||||||
recipient: Recipient,
|
recipient: Recipient,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val isInPreview = LocalInspectionMode.current
|
BlurContainer(
|
||||||
|
isBlurred = true,
|
||||||
// Use a simple background in preview mode, otherwise use Glide to load the blurred avatar
|
modifier = modifier,
|
||||||
if (isInPreview) {
|
blurRadius = 15.dp
|
||||||
Box(
|
) {
|
||||||
modifier = modifier
|
if (LocalInspectionMode.current) {
|
||||||
.fillMaxSize()
|
Image(
|
||||||
.background(Color(0xFF1B1B1D))
|
painter = painterResource(R.drawable.ic_avatar_abstract_02),
|
||||||
)
|
contentDescription = null,
|
||||||
} else {
|
modifier = Modifier.fillMaxSize()
|
||||||
// Use AndroidView to leverage AvatarUtil.loadBlurredIconIntoImageView
|
)
|
||||||
AndroidView(
|
} else {
|
||||||
factory = { context ->
|
val photo = remember(recipient.isSelf, recipient.contactPhoto) {
|
||||||
androidx.appcompat.widget.AppCompatImageView(context).apply {
|
if (recipient.isSelf) {
|
||||||
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
ProfileContactPhoto(recipient)
|
||||||
|
} else {
|
||||||
|
recipient.contactPhoto
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
update = { imageView ->
|
|
||||||
AvatarUtil.loadBlurredIconIntoImageView(recipient, imageView)
|
GlideImage(
|
||||||
},
|
modifier = Modifier
|
||||||
modifier = modifier.fillMaxSize()
|
.fillMaxSize()
|
||||||
)
|
.background(color = Color.Black),
|
||||||
|
model = photo,
|
||||||
|
scaleType = GlideImageScaleType.CENTER_CROP
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +348,6 @@ private fun PipAvatar(
|
|||||||
recipient = recipient,
|
recipient = recipient,
|
||||||
modifier = avatarModifier
|
modifier = avatarModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
BadgeImageLarge(
|
|
||||||
badge = recipient.badges.firstOrNull(),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.size(36.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,24 +439,29 @@ private fun SwitchCameraButton(
|
|||||||
onClick: (() -> Unit)?,
|
onClick: (() -> Unit)?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val size = when (selfPipMode) {
|
val targetSize = when (selfPipMode) {
|
||||||
SelfPipMode.EXPANDED_SELF_PIP, SelfPipMode.FOCUSED_SELF_PIP -> 48.dp
|
SelfPipMode.EXPANDED_SELF_PIP, SelfPipMode.FOCUSED_SELF_PIP -> 48.dp
|
||||||
else -> 28.dp
|
else -> 28.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
val margin = when (selfPipMode) {
|
val size by animateDpAsState(targetSize)
|
||||||
|
|
||||||
|
val targetMargin = when (selfPipMode) {
|
||||||
SelfPipMode.FOCUSED_SELF_PIP -> 12.dp
|
SelfPipMode.FOCUSED_SELF_PIP -> 12.dp
|
||||||
else -> 10.dp
|
else -> 10.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
val iconInset = when (selfPipMode) {
|
val margin by animateDpAsState(targetMargin)
|
||||||
|
|
||||||
|
val targetIconInset = when (selfPipMode) {
|
||||||
SelfPipMode.EXPANDED_SELF_PIP, SelfPipMode.FOCUSED_SELF_PIP -> 12.dp
|
SelfPipMode.EXPANDED_SELF_PIP, SelfPipMode.FOCUSED_SELF_PIP -> 12.dp
|
||||||
SelfPipMode.MINI_SELF_PIP -> 7.dp
|
SelfPipMode.MINI_SELF_PIP -> 7.dp
|
||||||
else -> 6.dp
|
else -> 6.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only clickable in EXPANDED_SELF_PIP mode (per setSelfPipMode logic)
|
val iconInset by animateDpAsState(targetIconInset)
|
||||||
val clickModifier = if (selfPipMode == SelfPipMode.EXPANDED_SELF_PIP && onClick != null) {
|
|
||||||
|
val clickModifier = if ((selfPipMode == SelfPipMode.EXPANDED_SELF_PIP || selfPipMode == SelfPipMode.FOCUSED_SELF_PIP) && onClick != null) {
|
||||||
Modifier.clickable { onClick() }
|
Modifier.clickable { onClick() }
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
@@ -354,7 +481,7 @@ private fun SwitchCameraButton(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.symbol_switch_24),
|
painter = painterResource(id = R.drawable.symbol_switch_24),
|
||||||
contentDescription = "Switch camera direction",
|
contentDescription = stringResource(R.string.SwitchCameraButton__switch_camera_direction),
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -479,11 +606,13 @@ enum class SelfPipMode {
|
|||||||
FOCUSED_SELF_PIP
|
FOCUSED_SELF_PIP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region Remote Participant Previews
|
||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerPreview() {
|
private fun RemoteParticipantGridPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
RemoteParticipantContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "Alice Johnson"),
|
recipient = Recipient(isResolving = false, systemContactName = "Alice Johnson"),
|
||||||
isMicrophoneEnabled = true,
|
isMicrophoneEnabled = true,
|
||||||
@@ -491,6 +620,7 @@ private fun CallParticipantViewerPreview() {
|
|||||||
),
|
),
|
||||||
raiseHandAllowed = false,
|
raiseHandAllowed = false,
|
||||||
renderInPip = false,
|
renderInPip = false,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = Modifier.size(400.dp, 600.dp)
|
modifier = Modifier.size(400.dp, 600.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -498,9 +628,9 @@ private fun CallParticipantViewerPreview() {
|
|||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerRaiseHandPreview() {
|
private fun RemoteParticipantRaiseHandPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
RemoteParticipantContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "Bob Smith"),
|
recipient = Recipient(isResolving = false, systemContactName = "Bob Smith"),
|
||||||
isMicrophoneEnabled = true,
|
isMicrophoneEnabled = true,
|
||||||
@@ -509,6 +639,7 @@ private fun CallParticipantViewerRaiseHandPreview() {
|
|||||||
),
|
),
|
||||||
raiseHandAllowed = true,
|
raiseHandAllowed = true,
|
||||||
renderInPip = false,
|
renderInPip = false,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = Modifier.size(400.dp, 600.dp)
|
modifier = Modifier.size(400.dp, 600.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -516,14 +647,16 @@ private fun CallParticipantViewerRaiseHandPreview() {
|
|||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerPipPreview() {
|
private fun RemoteParticipantSystemPipPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
RemoteParticipantContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "Charlie Davis"),
|
recipient = Recipient(isResolving = false, systemContactName = "Charlie Davis"),
|
||||||
isMicrophoneEnabled = false
|
isMicrophoneEnabled = false
|
||||||
),
|
),
|
||||||
|
raiseHandAllowed = false,
|
||||||
renderInPip = true,
|
renderInPip = true,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = Modifier.size(200.dp, 200.dp)
|
modifier = Modifier.size(200.dp, 200.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -531,14 +664,16 @@ private fun CallParticipantViewerPipPreview() {
|
|||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerPipLandscapePreview() {
|
private fun RemoteParticipantSystemPipLandscapePreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
RemoteParticipantContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "Charlie Davis"),
|
recipient = Recipient(isResolving = false, systemContactName = "Charlie Davis"),
|
||||||
isMicrophoneEnabled = false
|
isMicrophoneEnabled = false
|
||||||
),
|
),
|
||||||
|
raiseHandAllowed = false,
|
||||||
renderInPip = true,
|
renderInPip = true,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = Modifier.size(200.dp, 100.dp)
|
modifier = Modifier.size(200.dp, 100.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -546,12 +681,13 @@ private fun CallParticipantViewerPipLandscapePreview() {
|
|||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerBlockedPreview() {
|
private fun RemoteParticipantBlockedPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
RemoteParticipantContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "Diana Prince", isBlocked = true)
|
recipient = Recipient(isResolving = false, systemContactName = "Diana Prince", isBlocked = true)
|
||||||
),
|
),
|
||||||
|
raiseHandAllowed = false,
|
||||||
renderInPip = false,
|
renderInPip = false,
|
||||||
onInfoMoreInfoClick = {},
|
onInfoMoreInfoClick = {},
|
||||||
modifier = Modifier.size(400.dp, 600.dp)
|
modifier = Modifier.size(400.dp, 600.dp)
|
||||||
@@ -559,57 +695,98 @@ private fun CallParticipantViewerBlockedPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Self PIP Previews
|
||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerSelfPipNormalPreview() {
|
private fun SelfPipNormalPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
SelfPipContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
||||||
isMicrophoneEnabled = true,
|
isMicrophoneEnabled = true,
|
||||||
audioLevel = CallParticipant.AudioLevel.MEDIUM
|
audioLevel = CallParticipant.AudioLevel.MEDIUM
|
||||||
),
|
),
|
||||||
renderInPip = true,
|
|
||||||
selfPipMode = SelfPipMode.NORMAL_SELF_PIP,
|
selfPipMode = SelfPipMode.NORMAL_SELF_PIP,
|
||||||
isMoreThanOneCameraAvailable = true,
|
isMoreThanOneCameraAvailable = true,
|
||||||
onSwitchCameraClick = {},
|
onSwitchCameraClick = {},
|
||||||
modifier = Modifier.size(CallScreenMetrics.NormalRendererDpSize)
|
modifier = Modifier.size(rememberCallScreenMetrics().normalRendererDpSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerSelfPipExpandedPreview() {
|
private fun SelfPipExpandedPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
SelfPipContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
||||||
isMicrophoneEnabled = true,
|
isMicrophoneEnabled = true,
|
||||||
audioLevel = CallParticipant.AudioLevel.HIGH
|
audioLevel = CallParticipant.AudioLevel.HIGH
|
||||||
),
|
),
|
||||||
renderInPip = true,
|
|
||||||
selfPipMode = SelfPipMode.EXPANDED_SELF_PIP,
|
selfPipMode = SelfPipMode.EXPANDED_SELF_PIP,
|
||||||
isMoreThanOneCameraAvailable = true,
|
isMoreThanOneCameraAvailable = true,
|
||||||
onSwitchCameraClick = {},
|
onSwitchCameraClick = {},
|
||||||
modifier = Modifier.size(CallScreenMetrics.ExpandedRendererDpSize)
|
modifier = Modifier.size(rememberCallScreenMetrics().expandedRendererDpSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NightPreview
|
@NightPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantViewerSelfPipMiniPreview() {
|
private fun SelfPipMiniPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
CallParticipantViewer(
|
SelfPipContent(
|
||||||
participant = CallParticipant.EMPTY.copy(
|
participant = CallParticipant.EMPTY.copy(
|
||||||
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
||||||
isMicrophoneEnabled = false
|
isMicrophoneEnabled = false
|
||||||
),
|
),
|
||||||
renderInPip = true,
|
|
||||||
selfPipMode = SelfPipMode.MINI_SELF_PIP,
|
selfPipMode = SelfPipMode.MINI_SELF_PIP,
|
||||||
isMoreThanOneCameraAvailable = false,
|
isMoreThanOneCameraAvailable = false,
|
||||||
modifier = Modifier.size(CallScreenMetrics.SmallRendererDpSize)
|
onSwitchCameraClick = null,
|
||||||
|
modifier = Modifier.size(rememberCallScreenMetrics().overflowParticipantRendererDpSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NightPreview
|
||||||
|
@Composable
|
||||||
|
private fun SelfPipCameraOffPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
SelfPipContent(
|
||||||
|
participant = CallParticipant.EMPTY.copy(
|
||||||
|
recipient = Recipient(isResolving = false, systemContactName = "You", isSelf = true),
|
||||||
|
isMicrophoneEnabled = false,
|
||||||
|
isVideoEnabled = false
|
||||||
|
),
|
||||||
|
selfPipMode = SelfPipMode.NORMAL_SELF_PIP,
|
||||||
|
isMoreThanOneCameraAvailable = false,
|
||||||
|
onSwitchCameraClick = null,
|
||||||
|
modifier = Modifier.size(rememberCallScreenMetrics().normalRendererDpSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Overflow Participant Previews
|
||||||
|
|
||||||
|
@NightPreview
|
||||||
|
@Composable
|
||||||
|
private fun OverflowParticipantPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
OverflowParticipantContent(
|
||||||
|
participant = CallParticipant.EMPTY.copy(
|
||||||
|
recipient = Recipient(isResolving = false, systemContactName = "Eve Wilson"),
|
||||||
|
isMicrophoneEnabled = true,
|
||||||
|
audioLevel = CallParticipant.AudioLevel.MEDIUM
|
||||||
|
),
|
||||||
|
modifier = Modifier.size(rememberCallScreenMetrics().overflowParticipantRendererDpSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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.AllNightPreviews
|
||||||
import org.signal.core.ui.compose.Previews
|
import org.signal.core.ui.compose.Previews
|
||||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||||
import org.thoughtcrime.securesms.events.CallParticipant
|
import org.thoughtcrime.securesms.events.CallParticipant
|
||||||
@@ -42,23 +42,26 @@ fun CallParticipantsOverflow(
|
|||||||
overflowParticipants: List<CallParticipant>,
|
overflowParticipants: List<CallParticipant>,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val callScreenMetrics = rememberCallScreenMetrics()
|
||||||
|
val rendererSize = callScreenMetrics.overflowParticipantRendererSize
|
||||||
|
|
||||||
if (lineType == LayoutStrategyLineType.ROW) {
|
if (lineType == LayoutStrategyLineType.ROW) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = CallScreenMetrics.SmallRendererSize + 32.dp),
|
contentPadding = PaddingValues(start = 16.dp, end = rendererSize + 32.dp),
|
||||||
horizontalArrangement = spacedBy(4.dp)
|
horizontalArrangement = spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
appendItems(CallScreenMetrics.SmallRendererSize, overflowParticipants)
|
appendItems(rendererSize, overflowParticipants)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = PaddingValues(top = 16.dp, bottom = CallScreenMetrics.SmallRendererSize + 32.dp),
|
contentPadding = PaddingValues(top = 16.dp, bottom = rendererSize + 32.dp),
|
||||||
verticalArrangement = spacedBy(4.dp)
|
verticalArrangement = spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
appendItems(CallScreenMetrics.SmallRendererSize, overflowParticipants)
|
appendItems(rendererSize, overflowParticipants)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,17 +74,16 @@ private fun LazyListScope.appendItems(
|
|||||||
items = overflowParticipants,
|
items = overflowParticipants,
|
||||||
key = { it.callParticipantId }
|
key = { it.callParticipantId }
|
||||||
) { participant ->
|
) { participant ->
|
||||||
CallParticipantRenderer(
|
OverflowParticipantContent(
|
||||||
callParticipant = participant,
|
participant = participant,
|
||||||
renderInPip = false,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(contentSize)
|
.size(contentSize)
|
||||||
.clip(CallScreenMetrics.SmallRendererShape)
|
.clip(CallScreenMetrics.OverflowParticipantRendererShape)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NightPreview
|
@AllNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantsOverflowPreview() {
|
private fun CallParticipantsOverflowPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
@@ -100,18 +102,19 @@ private fun CallParticipantsOverflowPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val callScreenMetrics = rememberCallScreenMetrics()
|
||||||
CallParticipantsOverflow(
|
CallParticipantsOverflow(
|
||||||
lineType = LayoutStrategyLineType.ROW,
|
lineType = LayoutStrategyLineType.ROW,
|
||||||
overflowParticipants = participants,
|
overflowParticipants = participants,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.height(CallScreenMetrics.SmallRendererSize)
|
.height(callScreenMetrics.overflowParticipantRendererSize)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NightPreview
|
@AllNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallParticipantsOverflowColumnPreview() {
|
private fun CallParticipantsOverflowColumnPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
@@ -130,12 +133,13 @@ private fun CallParticipantsOverflowColumnPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val callScreenMetrics = rememberCallScreenMetrics()
|
||||||
CallParticipantsOverflow(
|
CallParticipantsOverflow(
|
||||||
lineType = LayoutStrategyLineType.COLUMN,
|
lineType = LayoutStrategyLineType.COLUMN,
|
||||||
overflowParticipants = participants,
|
overflowParticipants = participants,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.width(CallScreenMetrics.SmallRendererSize)
|
.width(callScreenMetrics.overflowParticipantRendererSize)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,11 @@ fun CallParticipantsPager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
CallParticipantRenderer(
|
RemoteParticipantContent(
|
||||||
callParticipant = callParticipantsPagerState.focusedParticipant,
|
participant = callParticipantsPagerState.focusedParticipant,
|
||||||
renderInPip = callParticipantsPagerState.isRenderInPip,
|
renderInPip = callParticipantsPagerState.isRenderInPip,
|
||||||
|
raiseHandAllowed = false,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -260,9 +262,11 @@ private fun AutoSizedParticipant(
|
|||||||
else -> Modifier.size(DpSize(maxSize.width, maxSize.height))
|
else -> Modifier.size(DpSize(maxSize.width, maxSize.height))
|
||||||
}
|
}
|
||||||
|
|
||||||
CallParticipantRenderer(
|
RemoteParticipantContent(
|
||||||
callParticipant = participant,
|
participant = participant,
|
||||||
renderInPip = isRenderInPip,
|
renderInPip = isRenderInPip,
|
||||||
|
raiseHandAllowed = false,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = sizeModifier
|
modifier = sizeModifier
|
||||||
.clip(RoundedCornerShape(state.cornerRadius))
|
.clip(RoundedCornerShape(state.cornerRadius))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,16 +22,21 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
import androidx.compose.material3.BottomSheetScaffold
|
import androidx.compose.material3.BottomSheetScaffold
|
||||||
import androidx.compose.material3.BottomSheetScaffoldState
|
import androidx.compose.material3.BottomSheetScaffoldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SheetValue
|
import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
@@ -46,7 +51,10 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.signal.core.ui.compose.AllNightPreviews
|
import org.signal.core.ui.compose.AllNightPreviews
|
||||||
@@ -137,7 +145,6 @@ fun CallScreen(
|
|||||||
|
|
||||||
val scaffoldState = remember(callScreenController) { callScreenController.scaffoldState }
|
val scaffoldState = remember(callScreenController) { callScreenController.scaffoldState }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
|
||||||
|
|
||||||
val additionalActionsPopupState = TriggerAlignedPopupState.rememberTriggerAlignedPopupState()
|
val additionalActionsPopupState = TriggerAlignedPopupState.rememberTriggerAlignedPopupState()
|
||||||
val additionalActionsState = remember(
|
val additionalActionsState = remember(
|
||||||
@@ -179,7 +186,10 @@ fun CallScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = SHEET_TOP_PADDING.dp, bottom = SHEET_BOTTOM_PADDING.dp)
|
.padding(top = SHEET_TOP_PADDING.dp, bottom = SHEET_BOTTOM_PADDING.dp)
|
||||||
.height(DimensionUnit.PIXELS.toDp(maxSheetHeight).dp)
|
.heightIn(
|
||||||
|
min = with(LocalDensity.current) { maxSheetHeight.toDp() },
|
||||||
|
max = with(LocalDensity.current) { maxHeight.toDp() }
|
||||||
|
)
|
||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
val offset = it.positionInRoot().y
|
val offset = it.positionInRoot().y
|
||||||
val current = maxHeight - offset - DimensionUnit.DP.toPixels(peekHeight)
|
val current = maxHeight - offset - DimensionUnit.DP.toPixels(peekHeight)
|
||||||
@@ -192,7 +202,7 @@ fun CallScreen(
|
|||||||
val callInfoAlpha = max(0f, peekPercentage)
|
val callInfoAlpha = max(0f, peekPercentage)
|
||||||
|
|
||||||
if (callInfoAlpha > 0f) {
|
if (callInfoAlpha > 0f) {
|
||||||
Surface {
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||||
callInfoView(callInfoAlpha)
|
callInfoView(callInfoAlpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +231,35 @@ fun CallScreen(
|
|||||||
label = "animate-as-state"
|
label = "animate-as-state"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Self-pip bottom inset should be based off of:
|
||||||
|
// A. The container width
|
||||||
|
// B. The sheet width
|
||||||
|
// A - B / 2 gives you the gutter width.
|
||||||
|
// If the pip in its current state would be bigger than the gutter width (accounting for padding)
|
||||||
|
// then we need to apply the inset.
|
||||||
|
|
||||||
|
val selfPipHorizontalPadding = 32.dp
|
||||||
|
val shouldNotApplyBottomPaddingToViewPort = currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
|
||||||
|
val selfPipBottomInset: Dp = if (shouldNotApplyBottomPaddingToViewPort) {
|
||||||
|
val containerWidth = maxWidth
|
||||||
|
val sheetWidth = BottomSheetDefaults.SheetMaxWidth
|
||||||
|
val widthOfPip = rememberSelfPipSize(localRenderState).width
|
||||||
|
|
||||||
|
if (containerWidth <= sheetWidth) {
|
||||||
|
padding
|
||||||
|
} else {
|
||||||
|
val spaceRemaining: Dp = (containerWidth - sheetWidth) / 2f - selfPipHorizontalPadding
|
||||||
|
|
||||||
|
if (spaceRemaining > widthOfPip) {
|
||||||
|
0.dp
|
||||||
|
} else {
|
||||||
|
padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
|
|
||||||
Viewport(
|
Viewport(
|
||||||
localParticipant = localParticipant,
|
localParticipant = localParticipant,
|
||||||
localRenderState = localRenderState,
|
localRenderState = localRenderState,
|
||||||
@@ -235,9 +274,10 @@ fun CallScreen(
|
|||||||
onControlsToggled = onControlsToggled,
|
onControlsToggled = onControlsToggled,
|
||||||
callScreenController = callScreenController,
|
callScreenController = callScreenController,
|
||||||
onToggleCameraDirection = callScreenControlsListener::onCameraDirectionChanged,
|
onToggleCameraDirection = callScreenControlsListener::onCameraDirectionChanged,
|
||||||
modifier = if (isPortrait) {
|
selfPipBottomInset = selfPipBottomInset,
|
||||||
Modifier.padding(bottom = padding)
|
modifier = if (shouldNotApplyBottomPaddingToViewPort) {
|
||||||
} else Modifier
|
Modifier
|
||||||
|
} else Modifier.padding(bottom = padding)
|
||||||
)
|
)
|
||||||
|
|
||||||
val onCallInfoClick: () -> Unit = {
|
val onCallInfoClick: () -> Unit = {
|
||||||
@@ -349,6 +389,7 @@ private fun Viewport(
|
|||||||
onPipFocusClick: () -> Unit,
|
onPipFocusClick: () -> Unit,
|
||||||
onControlsToggled: (Boolean) -> Unit,
|
onControlsToggled: (Boolean) -> Unit,
|
||||||
onToggleCameraDirection: () -> Unit,
|
onToggleCameraDirection: () -> Unit,
|
||||||
|
selfPipBottomInset: Dp,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val isEmptyOngoingCall = webRtcCallState.inOngoingCall && callParticipantsPagerState.callParticipants.isEmpty()
|
val isEmptyOngoingCall = webRtcCallState.inOngoingCall && callParticipantsPagerState.callParticipants.isEmpty()
|
||||||
@@ -377,6 +418,7 @@ private fun Viewport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val callScreenMetrics = rememberCallScreenMetrics()
|
||||||
BlurContainer(
|
BlurContainer(
|
||||||
isBlurred = localRenderState == WebRtcLocalRenderState.FOCUSED,
|
isBlurred = localRenderState == WebRtcLocalRenderState.FOCUSED,
|
||||||
modifier = modifier.fillMaxWidth()
|
modifier = modifier.fillMaxWidth()
|
||||||
@@ -408,7 +450,7 @@ private fun Viewport(
|
|||||||
overflowParticipants = overflowParticipants,
|
overflowParticipants = overflowParticipants,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.height(CallScreenMetrics.SmallRendererSize)
|
.height(callScreenMetrics.overflowParticipantRendererSize)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -422,7 +464,7 @@ private fun Viewport(
|
|||||||
overflowParticipants = overflowParticipants,
|
overflowParticipants = overflowParticipants,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.width(CallScreenMetrics.SmallRendererSize)
|
.width(callScreenMetrics.overflowParticipantRendererSize)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -438,7 +480,7 @@ private fun Viewport(
|
|||||||
onClick = onPipClick,
|
onClick = onPipClick,
|
||||||
onToggleCameraDirectionClick = onToggleCameraDirection,
|
onToggleCameraDirectionClick = onToggleCameraDirection,
|
||||||
onFocusLocalParticipantClick = onPipFocusClick,
|
onFocusLocalParticipantClick = onPipFocusClick,
|
||||||
modifier = modifier
|
modifier = modifier.padding(bottom = selfPipBottomInset)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,9 +493,11 @@ private fun LargeLocalVideoRenderer(
|
|||||||
localParticipant: CallParticipant,
|
localParticipant: CallParticipant,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
CallParticipantRenderer(
|
RemoteParticipantContent(
|
||||||
callParticipant = localParticipant,
|
participant = localParticipant,
|
||||||
renderInPip = false,
|
renderInPip = false,
|
||||||
|
raiseHandAllowed = false,
|
||||||
|
onInfoMoreInfoClick = null,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
)
|
)
|
||||||
@@ -536,7 +580,7 @@ private fun CallScreenPreview() {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE,
|
localRenderState = WebRtcLocalRenderState.FOCUSED,
|
||||||
callScreenDialogType = CallScreenDialogType.NONE,
|
callScreenDialogType = CallScreenDialogType.NONE,
|
||||||
callInfoView = {
|
callInfoView = {
|
||||||
Text(text = "Call Info View Preview", modifier = Modifier.alpha(it))
|
Text(text = "Call Info View Preview", modifier = Modifier.alpha(it))
|
||||||
|
|||||||
@@ -6,32 +6,106 @@
|
|||||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.annotation.RememberInComposition
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
|
||||||
object CallScreenMetrics {
|
@Stable
|
||||||
val SmallRendererSize = 90.dp
|
class CallScreenMetrics @RememberInComposition constructor(
|
||||||
val SmallRendererCornerSize = 24.dp
|
private val windowSizeClass: WindowSizeClass
|
||||||
val ExpandedRendererCornerSize = 28.dp
|
) {
|
||||||
val FocusedRendererCornerSize = 32.dp
|
|
||||||
|
companion object {
|
||||||
|
val OverflowParticipantRendererCornerSize = 24.dp
|
||||||
|
val ExpandedRendererCornerSize = 28.dp
|
||||||
|
val FocusedRendererCornerSize = 32.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of self renderer when in large group calls.
|
||||||
|
*/
|
||||||
|
val OverflowParticipantRendererShape = RoundedCornerShape(OverflowParticipantRendererCornerSize)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shape of self renderer when in large group calls.
|
* Represents the size of the renderer for the participant overflow and the mini self-pip.
|
||||||
*/
|
*/
|
||||||
val SmallRendererShape = RoundedCornerShape(SmallRendererCornerSize)
|
val overflowParticipantRendererSize: Dp = forWindowSizeClass(
|
||||||
|
compact = 96.dp,
|
||||||
|
medium = 116.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
val overflowParticipantRendererAvatarSize: Dp = forWindowSizeClass(
|
||||||
|
compact = 48.dp,
|
||||||
|
medium = 56.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
private val normalRendererDpWidth: Dp = forWindowSizeClass(
|
||||||
|
compact = 96.dp,
|
||||||
|
medium = 132.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
private val normalRendererDpHeight: Dp = forWindowSizeClass(
|
||||||
|
compact = 171.dp,
|
||||||
|
medium = 235.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
private val expandedRendererDpWidth: Dp = forWindowSizeClass(
|
||||||
|
compact = 148.dp,
|
||||||
|
medium = 180.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
private val expandedRendererDpHeight: Dp = forWindowSizeClass(
|
||||||
|
compact = 263.dp,
|
||||||
|
medium = 321.dp
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Size of self renderer when in large group calls
|
* Size of self renderer when in large group calls
|
||||||
*/
|
*/
|
||||||
val SmallRendererDpSize = DpSize(SmallRendererSize, SmallRendererSize)
|
val overflowParticipantRendererDpSize get() = DpSize(overflowParticipantRendererSize, overflowParticipantRendererSize)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Size of self renderer when in small group calls and 1:1 calls
|
* Size of self renderer when in small group calls and 1:1 calls
|
||||||
*/
|
*/
|
||||||
val NormalRendererDpSize = DpSize(90.dp, 160.dp)
|
val normalRendererDpSize get() = DpSize(normalRendererDpWidth, normalRendererDpHeight)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Size of self renderer after clicking on it to expand
|
* Size of self renderer after clicking on it to expand
|
||||||
*/
|
*/
|
||||||
val ExpandedRendererDpSize = DpSize(170.dp, 300.dp)
|
val expandedRendererDpSize get() = DpSize(expandedRendererDpWidth, expandedRendererDpHeight)
|
||||||
|
|
||||||
|
private fun <T> forWindowSizeClass(
|
||||||
|
compact: T,
|
||||||
|
medium: T = compact,
|
||||||
|
expanded: T = medium
|
||||||
|
): T {
|
||||||
|
return if (windowSizeClass.isAtLeastBreakpoint(
|
||||||
|
widthDpBreakpoint = WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND,
|
||||||
|
heightDpBreakpoint = WindowSizeClass.HEIGHT_DP_EXPANDED_LOWER_BOUND
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
expanded
|
||||||
|
} else if (windowSizeClass.isAtLeastBreakpoint(
|
||||||
|
widthDpBreakpoint = WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND,
|
||||||
|
heightDpBreakpoint = WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
medium
|
||||||
|
} else {
|
||||||
|
compact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberCallScreenMetrics(): CallScreenMetrics {
|
||||||
|
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
|
|
||||||
|
return remember(windowSizeClass) { CallScreenMetrics(windowSizeClass) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -37,6 +38,7 @@ import androidx.compose.ui.draw.dropShadow
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.shadow.Shadow
|
import androidx.compose.ui.graphics.shadow.Shadow
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
@@ -44,7 +46,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
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.AllNightPreviews
|
||||||
import org.signal.core.ui.compose.Previews
|
import org.signal.core.ui.compose.Previews
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState
|
||||||
@@ -60,20 +62,9 @@ fun MoveableLocalVideoRenderer(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onToggleCameraDirectionClick: () -> Unit,
|
onToggleCameraDirectionClick: () -> Unit,
|
||||||
onFocusLocalParticipantClick: () -> Unit,
|
onFocusLocalParticipantClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier.Companion
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// 1. We need to remember our small and expanded sizes based off of the call size.
|
val size = rememberSelfPipSize(localRenderState)
|
||||||
val size = remember(localRenderState) {
|
|
||||||
when (localRenderState) {
|
|
||||||
WebRtcLocalRenderState.GONE -> DpSize.Zero
|
|
||||||
WebRtcLocalRenderState.SMALL_RECTANGLE -> CallScreenMetrics.NormalRendererDpSize
|
|
||||||
WebRtcLocalRenderState.SMALLER_RECTANGLE -> CallScreenMetrics.SmallRendererDpSize
|
|
||||||
WebRtcLocalRenderState.LARGE -> DpSize.Zero
|
|
||||||
WebRtcLocalRenderState.LARGE_NO_VIDEO -> DpSize.Zero
|
|
||||||
WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererDpSize
|
|
||||||
WebRtcLocalRenderState.FOCUSED -> DpSize.Unspecified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -84,9 +75,28 @@ fun MoveableLocalVideoRenderer(
|
|||||||
) {
|
) {
|
||||||
val targetSize = size.let {
|
val targetSize = size.let {
|
||||||
if (it == DpSize.Unspecified) {
|
if (it == DpSize.Unspecified) {
|
||||||
DpSize(maxWidth - 32.dp, maxHeight - 32.dp)
|
val orientation = LocalConfiguration.current.orientation
|
||||||
|
val desiredWidth = maxWidth - 32.dp
|
||||||
|
val desiredHeight = maxHeight - 32.dp
|
||||||
|
|
||||||
|
val aspectRatio = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
16f / 9f
|
||||||
|
} else {
|
||||||
|
9f / 16f
|
||||||
|
}
|
||||||
|
|
||||||
|
val widthFromHeight = desiredHeight * aspectRatio
|
||||||
|
val heightFromWidth = desiredWidth / aspectRatio
|
||||||
|
|
||||||
|
val size: DpSize = if (widthFromHeight <= desiredWidth) {
|
||||||
|
DpSize(widthFromHeight, desiredHeight)
|
||||||
|
} else {
|
||||||
|
DpSize(desiredWidth, heightFromWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
} else {
|
} else {
|
||||||
it
|
it.rotateForConfiguration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +106,16 @@ fun MoveableLocalVideoRenderer(
|
|||||||
val selfPipMode = when (localRenderState) {
|
val selfPipMode = when (localRenderState) {
|
||||||
WebRtcLocalRenderState.EXPANDED -> {
|
WebRtcLocalRenderState.EXPANDED -> {
|
||||||
SelfPipMode.EXPANDED_SELF_PIP
|
SelfPipMode.EXPANDED_SELF_PIP
|
||||||
} WebRtcLocalRenderState.FOCUSED -> {
|
}
|
||||||
|
|
||||||
|
WebRtcLocalRenderState.FOCUSED -> {
|
||||||
SelfPipMode.FOCUSED_SELF_PIP
|
SelfPipMode.FOCUSED_SELF_PIP
|
||||||
}
|
}
|
||||||
|
|
||||||
WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
||||||
SelfPipMode.MINI_SELF_PIP
|
SelfPipMode.MINI_SELF_PIP
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
SelfPipMode.NORMAL_SELF_PIP
|
SelfPipMode.NORMAL_SELF_PIP
|
||||||
}
|
}
|
||||||
@@ -111,16 +125,17 @@ fun MoveableLocalVideoRenderer(
|
|||||||
val shadow by animateShadow(localRenderState)
|
val shadow by animateShadow(localRenderState)
|
||||||
|
|
||||||
PictureInPicture(
|
PictureInPicture(
|
||||||
|
centerContent = size == DpSize.Unspecified,
|
||||||
state = state,
|
state = state,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
CallParticipantRenderer(
|
SelfPipContent(
|
||||||
callParticipant = localParticipant,
|
participant = localParticipant,
|
||||||
renderInPip = true,
|
|
||||||
selfPipMode = selfPipMode,
|
selfPipMode = selfPipMode,
|
||||||
onToggleCameraDirection = onToggleCameraDirectionClick,
|
isMoreThanOneCameraAvailable = localParticipant.cameraState.cameraCount > 1,
|
||||||
|
onSwitchCameraClick = onToggleCameraDirectionClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.dropShadow(
|
.dropShadow(
|
||||||
@@ -170,7 +185,7 @@ private fun animateClip(localRenderState: WebRtcLocalRenderState): State<Dp> {
|
|||||||
val targetDp = when (localRenderState) {
|
val targetDp = when (localRenderState) {
|
||||||
WebRtcLocalRenderState.FOCUSED -> CallScreenMetrics.FocusedRendererCornerSize
|
WebRtcLocalRenderState.FOCUSED -> CallScreenMetrics.FocusedRendererCornerSize
|
||||||
WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererCornerSize
|
WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererCornerSize
|
||||||
else -> CallScreenMetrics.SmallRendererCornerSize
|
else -> CallScreenMetrics.OverflowParticipantRendererCornerSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return animateDpAsState(targetValue = targetDp)
|
return animateDpAsState(targetValue = targetDp)
|
||||||
@@ -182,6 +197,7 @@ private fun animateShadow(localRenderState: WebRtcLocalRenderState): State<Shado
|
|||||||
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED, WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED, WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
||||||
14.dp
|
14.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
0.dp
|
0.dp
|
||||||
}
|
}
|
||||||
@@ -191,6 +207,7 @@ private fun animateShadow(localRenderState: WebRtcLocalRenderState): State<Shado
|
|||||||
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED, WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED, WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
||||||
4.dp
|
4.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
0.dp
|
0.dp
|
||||||
}
|
}
|
||||||
@@ -203,7 +220,7 @@ private fun animateShadow(localRenderState: WebRtcLocalRenderState): State<Shado
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NightPreview
|
@AllNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun MoveableLocalVideoRendererPreview() {
|
private fun MoveableLocalVideoRendererPreview() {
|
||||||
var localRenderState by remember { mutableStateOf(WebRtcLocalRenderState.SMALL_RECTANGLE) }
|
var localRenderState by remember { mutableStateOf(WebRtcLocalRenderState.SMALL_RECTANGLE) }
|
||||||
@@ -260,3 +277,36 @@ private fun MoveableLocalVideoRendererPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberSelfPipSize(
|
||||||
|
localRenderState: WebRtcLocalRenderState
|
||||||
|
): DpSize {
|
||||||
|
val callScreenMetrics = rememberCallScreenMetrics()
|
||||||
|
return remember(localRenderState, callScreenMetrics) {
|
||||||
|
when (localRenderState) {
|
||||||
|
WebRtcLocalRenderState.GONE -> DpSize.Zero
|
||||||
|
WebRtcLocalRenderState.SMALL_RECTANGLE -> callScreenMetrics.normalRendererDpSize
|
||||||
|
WebRtcLocalRenderState.SMALLER_RECTANGLE -> callScreenMetrics.overflowParticipantRendererDpSize
|
||||||
|
WebRtcLocalRenderState.LARGE -> DpSize.Zero
|
||||||
|
WebRtcLocalRenderState.LARGE_NO_VIDEO -> DpSize.Zero
|
||||||
|
WebRtcLocalRenderState.EXPANDED -> callScreenMetrics.expandedRendererDpSize
|
||||||
|
WebRtcLocalRenderState.FOCUSED -> DpSize.Unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the proper DpSize rotation based off the window configuration.
|
||||||
|
*
|
||||||
|
* Call-Screen DpSizes for the movable pip are expected to be in portrait by default.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun DpSize.rotateForConfiguration(): DpSize {
|
||||||
|
val orientation = LocalConfiguration.current.orientation
|
||||||
|
|
||||||
|
return when (orientation) {
|
||||||
|
Configuration.ORIENTATION_LANDSCAPE -> DpSize(this.height, this.width)
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.animation.core.AnimationVector2D
|
|||||||
import androidx.compose.animation.core.TwoWayConverter
|
import androidx.compose.animation.core.TwoWayConverter
|
||||||
import androidx.compose.animation.core.animate
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateIntOffsetAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -55,6 +56,7 @@ private const val DECELERATION_RATE = 0.99f
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PictureInPicture(
|
fun PictureInPicture(
|
||||||
|
centerContent: Boolean,
|
||||||
state: PictureInPictureState,
|
state: PictureInPictureState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
@@ -67,6 +69,8 @@ fun PictureInPicture(
|
|||||||
val maxWidth = constraints.maxWidth
|
val maxWidth = constraints.maxWidth
|
||||||
val contentWidth = with(density) { state.contentSize.width.toPx().roundToInt() }
|
val contentWidth = with(density) { state.contentSize.width.toPx().roundToInt() }
|
||||||
val contentHeight = with(density) { state.contentSize.height.toPx().roundToInt() }
|
val contentHeight = with(density) { state.contentSize.height.toPx().roundToInt() }
|
||||||
|
val targetContentWidth = with(density) { state.targetSize.width.toPx().roundToInt() }
|
||||||
|
val targetContentHeight = with(density) { state.targetSize.height.toPx().roundToInt() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
var isDragging by remember {
|
var isDragging by remember {
|
||||||
@@ -77,10 +81,6 @@ fun PictureInPicture(
|
|||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isContentFullScreen = remember(maxWidth, maxHeight, contentWidth, contentHeight) {
|
|
||||||
maxWidth == contentWidth && maxHeight == contentHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
var offsetX by remember {
|
var offsetX by remember {
|
||||||
mutableIntStateOf(maxWidth - contentWidth)
|
mutableIntStateOf(maxWidth - contentWidth)
|
||||||
}
|
}
|
||||||
@@ -92,37 +92,51 @@ fun PictureInPicture(
|
|||||||
IntOffset(0, 0)
|
IntOffset(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val topRight = remember(maxWidth, contentWidth) {
|
val topRight = remember(maxWidth, targetContentWidth) {
|
||||||
IntOffset(maxWidth - contentWidth, 0)
|
IntOffset(maxWidth - targetContentWidth, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val bottomLeft = remember(maxHeight, contentHeight) {
|
val bottomLeft = remember(maxHeight, targetContentHeight) {
|
||||||
IntOffset(0, maxHeight - contentHeight)
|
IntOffset(0, maxHeight - targetContentHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
val bottomRight = remember(maxWidth, maxHeight, contentWidth, contentHeight) {
|
val bottomRight = remember(maxWidth, maxHeight, targetContentWidth, targetContentHeight) {
|
||||||
IntOffset(maxWidth - contentWidth, maxHeight - contentHeight)
|
IntOffset(maxWidth - targetContentWidth, maxHeight - targetContentHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(maxWidth, maxHeight, isAnimating, isDragging, contentWidth, contentHeight, isContentFullScreen) {
|
DisposableEffect(maxWidth, maxHeight, isAnimating, isDragging, targetContentWidth, targetContentHeight, centerContent) {
|
||||||
if (!isAnimating && !isDragging) {
|
if (!isAnimating && !isDragging) {
|
||||||
val offset = getDesiredCornerOffset(state.corner, topLeft, topRight, bottomLeft, bottomRight)
|
if (centerContent) {
|
||||||
|
offsetX = (maxWidth / 2f).roundToInt() - (targetContentWidth / 2f).roundToInt()
|
||||||
|
offsetY = (maxHeight / 2f).roundToInt() - (targetContentHeight / 2f).roundToInt()
|
||||||
|
} else {
|
||||||
|
val offset = getDesiredCornerOffset(state.corner, topLeft, topRight, bottomLeft, bottomRight)
|
||||||
|
|
||||||
offsetX = offset.x
|
offsetX = offset.x
|
||||||
offsetY = offset.y
|
offsetY = offset.y
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDispose { }
|
onDispose { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val animatedOffset by animateIntOffsetAsState(
|
||||||
|
targetValue = IntOffset(offsetX, offsetY),
|
||||||
|
animationSpec = tween()
|
||||||
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(state.contentSize)
|
.size(state.contentSize)
|
||||||
.offset {
|
.offset {
|
||||||
IntOffset(offsetX, offsetY)
|
if (isDragging) {
|
||||||
|
IntOffset(offsetX, offsetY)
|
||||||
|
} else {
|
||||||
|
animatedOffset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.draggable2D(
|
.draggable2D(
|
||||||
enabled = !isAnimating && !isContentFullScreen,
|
enabled = !isAnimating && !centerContent,
|
||||||
state = rememberDraggable2DState { offset ->
|
state = rememberDraggable2DState { offset ->
|
||||||
offsetX += offset.x.roundToInt()
|
offsetX += offset.x.roundToInt()
|
||||||
offsetY += offset.y.roundToInt()
|
offsetY += offset.y.roundToInt()
|
||||||
@@ -201,6 +215,9 @@ class PictureInPictureState @RememberInComposition constructor(initialContentSiz
|
|||||||
var contentSize: DpSize by mutableStateOf(initialContentSize)
|
var contentSize: DpSize by mutableStateOf(initialContentSize)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var targetSize: DpSize by mutableStateOf(initialContentSize)
|
||||||
|
private set
|
||||||
|
|
||||||
var corner: Corner by mutableStateOf(initialCorner)
|
var corner: Corner by mutableStateOf(initialCorner)
|
||||||
|
|
||||||
enum class Corner {
|
enum class Corner {
|
||||||
@@ -211,9 +228,11 @@ class PictureInPictureState @RememberInComposition constructor(initialContentSiz
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun animateTo(targetSize: DpSize) {
|
fun animateTo(newTargetSize: DpSize) {
|
||||||
val targetWidth by animateDpAsState(label = "animate-pip-width", targetValue = targetSize.width, animationSpec = tween())
|
targetSize = newTargetSize
|
||||||
val targetHeight by animateDpAsState(label = "animate-pip-height", targetValue = targetSize.height, animationSpec = tween())
|
|
||||||
|
val targetWidth by animateDpAsState(label = "animate-pip-width", targetValue = newTargetSize.width, animationSpec = tween())
|
||||||
|
val targetHeight by animateDpAsState(label = "animate-pip-height", targetValue = newTargetSize.height, animationSpec = tween())
|
||||||
|
|
||||||
contentSize = DpSize(targetWidth, targetHeight)
|
contentSize = DpSize(targetWidth, targetHeight)
|
||||||
}
|
}
|
||||||
@@ -228,6 +247,7 @@ private fun distance(a: IntOffset, b: IntOffset): Float {
|
|||||||
fun PictureInPicturePreview() {
|
fun PictureInPicturePreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
PictureInPicture(
|
PictureInPicture(
|
||||||
|
centerContent = false,
|
||||||
state = remember { PictureInPictureState(initialContentSize = DpSize(90.dp, 160.dp)) },
|
state = remember { PictureInPictureState(initialContentSize = DpSize(90.dp, 160.dp)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
@@ -2694,6 +2694,10 @@
|
|||||||
<string name="CallParticipantView__cant_receive_audio_and_video_from_s">Can\'t receive audio and video from %1$s</string>
|
<string name="CallParticipantView__cant_receive_audio_and_video_from_s">Can\'t receive audio and video from %1$s</string>
|
||||||
<string name="CallParticipantView__this_may_be_Because_they_have_not_verified_your_safety_number_change">This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you.</string>
|
<string name="CallParticipantView__this_may_be_Because_they_have_not_verified_your_safety_number_change">This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you.</string>
|
||||||
|
|
||||||
|
<!-- SwitchCameraButton -->
|
||||||
|
<!-- Content description for the button that switches between front and back camera during a call -->
|
||||||
|
<string name="SwitchCameraButton__switch_camera_direction">Switch camera direction</string>
|
||||||
|
|
||||||
<!-- CallToastPopupWindow -->
|
<!-- CallToastPopupWindow -->
|
||||||
<string name="CallToastPopupWindow__swipe_to_view_screen_share">Swipe to view screen share</string>
|
<string name="CallToastPopupWindow__swipe_to_view_screen_share">Swipe to view screen share</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user