mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Add blurring for focused state for local call pip.
This commit is contained in:
committed by
Cody Henthorne
parent
2a3888472f
commit
be600f769d
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
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
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.NightPreview
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Blurrable container. Wrap whatever you want blurred in this.
|
||||
*/
|
||||
@Composable
|
||||
fun BlurContainer(
|
||||
isBlurred: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
blurRadius: Dp = 20.dp,
|
||||
skipOverlay: Boolean = false,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val blurRadius = if (isBlurred) blurRadius else 0.dp
|
||||
val blur by animateDpAsState(blurRadius)
|
||||
|
||||
Box(
|
||||
modifier = modifier.blur(blur, edgeTreatment = BlurredEdgeTreatment.Unbounded)
|
||||
) {
|
||||
content()
|
||||
|
||||
if (!skipOverlay) {
|
||||
BlurOverlay(visible = isBlurred, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Glass' blur overlay.
|
||||
* ```
|
||||
*/
|
||||
@Composable
|
||||
private fun BlurOverlay(visible: Boolean, modifier: Modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
modifier = modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color(0xB3252525))
|
||||
.background(color = Color(0x109C9C9C))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@NightPreview
|
||||
@Composable
|
||||
fun BlurContainerPreview() {
|
||||
Previews.Preview {
|
||||
var isBlurred by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isBlurred) {
|
||||
if (isBlurred) {
|
||||
delay(3.seconds)
|
||||
isBlurred = false
|
||||
}
|
||||
}
|
||||
|
||||
BlurContainer(
|
||||
isBlurred = isBlurred,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_add_a_profile_megaphone_image),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = {
|
||||
isBlurred = true
|
||||
}
|
||||
) {
|
||||
Text("Blur")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.togetherWith
|
||||
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
|
||||
@@ -38,7 +37,6 @@ 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 +44,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onPlaced
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
@@ -337,7 +333,7 @@ fun CallScreen(
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BoxScope.Viewport(
|
||||
private fun Viewport(
|
||||
localParticipant: CallParticipant,
|
||||
localRenderState: WebRtcLocalRenderState,
|
||||
webRtcCallState: WebRtcViewModel.State,
|
||||
@@ -377,68 +373,64 @@ private fun BoxScope.Viewport(
|
||||
}
|
||||
}
|
||||
|
||||
var spacerOffset by remember { mutableStateOf(Offset.Zero) }
|
||||
BlurContainer(
|
||||
isBlurred = localRenderState == WebRtcLocalRenderState.FOCUSED,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
CallParticipantsPager(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
pagerState = callScreenController.callParticipantsVerticalPagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.clickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
callScreenController.handleEvent(CallScreenController.Event.TOGGLE_CONTROLS)
|
||||
}
|
||||
},
|
||||
enabled = !callControlsState.skipHiddenState
|
||||
)
|
||||
)
|
||||
|
||||
Row(modifier = modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
CallParticipantsPager(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
pagerState = callScreenController.callParticipantsVerticalPagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.clickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
callScreenController.handleEvent(CallScreenController.Event.TOGGLE_CONTROLS)
|
||||
}
|
||||
},
|
||||
enabled = !callControlsState.skipHiddenState
|
||||
)
|
||||
)
|
||||
if (isPortrait && isLargeGroupCall) {
|
||||
Row {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp)
|
||||
.height(OVERFLOW_ITEM_SIZE)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
if (isPortrait && isLargeGroupCall) {
|
||||
Row {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp)
|
||||
.size(OVERFLOW_ITEM_SIZE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPortrait && isLargeGroupCall) {
|
||||
Column {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp)
|
||||
.height(OVERFLOW_ITEM_SIZE)
|
||||
.width(OVERFLOW_ITEM_SIZE + 32.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.onPlaced { coordinates ->
|
||||
spacerOffset = coordinates.localToRoot(Offset.Zero)
|
||||
}
|
||||
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp)
|
||||
.size(OVERFLOW_ITEM_SIZE)
|
||||
modifier = Modifier.size(OVERFLOW_ITEM_SIZE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPortrait && isLargeGroupCall) {
|
||||
Column {
|
||||
CallParticipantsOverflow(
|
||||
overflowParticipants = overflowParticipants,
|
||||
modifier = Modifier
|
||||
.width(OVERFLOW_ITEM_SIZE + 32.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.onPlaced { coordinates ->
|
||||
spacerOffset = coordinates.localToRoot(Offset.Zero)
|
||||
}
|
||||
.size(OVERFLOW_ITEM_SIZE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,28 +6,42 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.dropShadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.shadow.Shadow
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.NightPreview
|
||||
@@ -66,12 +80,11 @@ fun MoveableLocalVideoRenderer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier)
|
||||
.padding(16.dp)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
val targetSize = size.let {
|
||||
if (it == DpSize.Unspecified) {
|
||||
DpSize(maxWidth, maxHeight)
|
||||
DpSize(maxWidth - 32.dp, maxHeight - 32.dp)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
@@ -80,24 +93,40 @@ fun MoveableLocalVideoRenderer(
|
||||
val state = remember { PictureInPictureState(initialContentSize = targetSize) }
|
||||
state.animateTo(targetSize)
|
||||
|
||||
val selfPipMode = when (localRenderState) {
|
||||
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED -> {
|
||||
CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP
|
||||
}
|
||||
WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
||||
CallParticipantView.SelfPipMode.MINI_SELF_PIP
|
||||
}
|
||||
else -> {
|
||||
CallParticipantView.SelfPipMode.NORMAL_SELF_PIP
|
||||
}
|
||||
}
|
||||
|
||||
val clip by animateClip(localRenderState)
|
||||
val shadow by animateShadow(localRenderState)
|
||||
|
||||
PictureInPicture(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
CallParticipantRenderer(
|
||||
callParticipant = localParticipant,
|
||||
isLocalParticipant = true,
|
||||
renderInPip = true,
|
||||
selfPipMode = if (localRenderState == WebRtcLocalRenderState.EXPANDED || localRenderState == WebRtcLocalRenderState.FOCUSED) {
|
||||
CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP
|
||||
} else {
|
||||
CallParticipantView.SelfPipMode.NORMAL_SELF_PIP
|
||||
},
|
||||
selfPipMode = selfPipMode,
|
||||
onToggleCameraDirection = onToggleCameraDirectionClick,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.dropShadow(
|
||||
shape = RoundedCornerShape(clip),
|
||||
shadow = shadow
|
||||
)
|
||||
.clip(RoundedCornerShape(clip))
|
||||
.clickable(onClick = {
|
||||
onClick()
|
||||
})
|
||||
@@ -135,38 +164,98 @@ fun MoveableLocalVideoRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun animateClip(localRenderState: WebRtcLocalRenderState): State<Dp> {
|
||||
val targetDp = when (localRenderState) {
|
||||
WebRtcLocalRenderState.FOCUSED -> 32.dp
|
||||
WebRtcLocalRenderState.EXPANDED -> 28.dp
|
||||
else -> 24.dp
|
||||
}
|
||||
|
||||
return animateDpAsState(targetValue = targetDp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun animateShadow(localRenderState: WebRtcLocalRenderState): State<Shadow> {
|
||||
val targetShadowRadius = when (localRenderState) {
|
||||
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED, WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
||||
14.dp
|
||||
}
|
||||
else -> {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
|
||||
val targetShadowOffset = when (localRenderState) {
|
||||
WebRtcLocalRenderState.EXPANDED, WebRtcLocalRenderState.FOCUSED, WebRtcLocalRenderState.SMALLER_RECTANGLE -> {
|
||||
4.dp
|
||||
}
|
||||
else -> {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
|
||||
val shadowRadius by animateDpAsState(targetShadowRadius)
|
||||
val shadowOffset by animateDpAsState(targetShadowOffset)
|
||||
return remember {
|
||||
derivedStateOf { Shadow(radius = shadowRadius, offset = DpOffset(0.dp, shadowOffset)) }
|
||||
}
|
||||
}
|
||||
|
||||
@NightPreview
|
||||
@Composable
|
||||
fun MoveableLocalVideoRendererPreview() {
|
||||
private fun MoveableLocalVideoRendererPreview() {
|
||||
var localRenderState by remember { mutableStateOf(WebRtcLocalRenderState.SMALL_RECTANGLE) }
|
||||
|
||||
Previews.Preview {
|
||||
MoveableLocalVideoRenderer(
|
||||
localParticipant = remember {
|
||||
CallParticipant()
|
||||
},
|
||||
localRenderState = localRenderState,
|
||||
onClick = {
|
||||
localRenderState = when (localRenderState) {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE -> {
|
||||
WebRtcLocalRenderState.EXPANDED
|
||||
}
|
||||
|
||||
WebRtcLocalRenderState.EXPANDED -> {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE
|
||||
}
|
||||
|
||||
else -> localRenderState
|
||||
}
|
||||
},
|
||||
onToggleCameraDirectionClick = {},
|
||||
onFocusLocalParticipantClick = {
|
||||
localRenderState = if (localRenderState == WebRtcLocalRenderState.FOCUSED) {
|
||||
WebRtcLocalRenderState.EXPANDED
|
||||
} else {
|
||||
WebRtcLocalRenderState.FOCUSED
|
||||
}
|
||||
val blur by animateDpAsState(
|
||||
if (localRenderState == WebRtcLocalRenderState.FOCUSED) {
|
||||
20.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.background(color = Color.Green)) {
|
||||
BlurContainer(
|
||||
isBlurred = localRenderState == WebRtcLocalRenderState.FOCUSED
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_add_a_profile_megaphone_image),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.blur(blur)
|
||||
)
|
||||
}
|
||||
|
||||
MoveableLocalVideoRenderer(
|
||||
localParticipant = remember {
|
||||
CallParticipant()
|
||||
},
|
||||
localRenderState = localRenderState,
|
||||
onClick = {
|
||||
localRenderState = when (localRenderState) {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE -> {
|
||||
WebRtcLocalRenderState.EXPANDED
|
||||
}
|
||||
|
||||
WebRtcLocalRenderState.EXPANDED -> {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE
|
||||
}
|
||||
|
||||
else -> localRenderState
|
||||
}
|
||||
},
|
||||
onToggleCameraDirectionClick = {},
|
||||
onFocusLocalParticipantClick = {
|
||||
localRenderState = if (localRenderState == WebRtcLocalRenderState.FOCUSED) {
|
||||
WebRtcLocalRenderState.EXPANDED
|
||||
} else {
|
||||
WebRtcLocalRenderState.FOCUSED
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user