Skip to content

Commit

Permalink
feat(calling): Floating view for self user (WPB-10652) - Part 2 (#3409)
Browse files Browse the repository at this point in the history
  • Loading branch information
ohassine authored Sep 5, 2024
1 parent 9007e80 commit 2c3822a
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ private fun OngoingCallContent(
isSelfUserCameraOn = isCameraOn,
isSelfUserMuted = isMuted ?: true,
contentHeight = this@BoxWithConstraints.maxHeight,
contentWidth = this@BoxWithConstraints.constraints.maxWidth,
onSelfVideoPreviewCreated = setVideoPreview,
onSelfClearVideoPreview = clearVideoPreview,
requestVideoStreams = requestVideoStreams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ fun FullScreenTile(
} else {
it.isMuted
},
shouldFill = false,
shouldFillOthersVideoPreview = false,
isZoomingEnabled = true,
onSelfUserVideoPreviewCreated = setVideoPreview,
onClearSelfUserVideoPreview = clearVideoPreview
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.calling.ongoing.participantsview

import android.view.View
import androidx.compose.animation.core.animateOffsetAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.wire.android.ui.calling.model.UICallParticipant
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dimensions

private const val DAMPING_RATIO_MEDIUM_BOUNCY = 0.6f
private const val STIFFNESS_MEDIUM_LOW = 300f
private const val DEFAULT_OFFSETX_SELF_USER_TILE = -50f
private const val DEFAULT_OFFSETY_SELF_USER_TILE = 80F
private val SELF_VIDEO_TILE_HEIGHT = 250.dp
private val SELF_VIDEO_TILE_WIDTH = 150.dp

@Composable
fun FloatingSelfUserTile(
contentHeight: Dp,
contentWidth: Float,
participant: UICallParticipant,
onSelfUserVideoPreviewCreated: (view: View) -> Unit,
modifier: Modifier = Modifier,
onClearSelfUserVideoPreview: () -> Unit
) {
val density = LocalDensity.current
val contentHeightPx = density.run { (contentHeight).toPx() }

var selfUserTileOffsetX by remember {
mutableStateOf(DEFAULT_OFFSETX_SELF_USER_TILE)
}
var selfUserTileOffsetY by remember {
mutableStateOf(DEFAULT_OFFSETY_SELF_USER_TILE)
}
val selfUserTileOffset by animateOffsetAsState(
targetValue = Offset(selfUserTileOffsetX, selfUserTileOffsetY),
animationSpec = spring(
DAMPING_RATIO_MEDIUM_BOUNCY,
stiffness = STIFFNESS_MEDIUM_LOW
),
label = "selfUserTileOffset"
)

Card(
border = BorderStroke(1.dp, colorsScheme().uncheckedColor),
shape = RoundedCornerShape(dimensions().corner6x),
modifier = modifier
.height(SELF_VIDEO_TILE_HEIGHT)
.width(SELF_VIDEO_TILE_WIDTH)
.offset { IntOffset(selfUserTileOffset.x.toInt(), selfUserTileOffset.y.toInt()) }
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
selfUserTileOffsetX =
if (selfUserTileOffsetX - 150f > -(contentWidth / 2)) {
DEFAULT_OFFSETX_SELF_USER_TILE
} else {
-contentWidth + SELF_VIDEO_TILE_WIDTH.toPx() - DEFAULT_OFFSETX_SELF_USER_TILE
}
selfUserTileOffsetY =
if (selfUserTileOffsetY + 250f > (contentHeightPx / 2)) {
contentHeightPx - SELF_VIDEO_TILE_HEIGHT.toPx() - DEFAULT_OFFSETY_SELF_USER_TILE
} else {
DEFAULT_OFFSETY_SELF_USER_TILE
}
}
) { change, dragAmount ->
change.consume()
val newOffsetX = (selfUserTileOffsetX + dragAmount.x)
.coerceAtLeast(
-contentHeightPx - SELF_VIDEO_TILE_WIDTH.toPx()
)
.coerceAtMost(-50f)

val newOffsetY = (selfUserTileOffsetY + dragAmount.y)
.coerceAtLeast(50f)
.coerceAtMost(
contentHeightPx - SELF_VIDEO_TILE_HEIGHT.toPx()
)

selfUserTileOffsetX = newOffsetX
selfUserTileOffsetY = newOffsetY
}
}
) {
ParticipantTile(
participantTitleState = participant,
isSelfUser = true,
shouldFillSelfUserCameraPreview = true,
isSelfUserMuted = participant.isMuted,
isSelfUserCameraOn = participant.isCameraOn,
onSelfUserVideoPreviewCreated = onSelfUserVideoPreviewCreated,
onClearSelfUserVideoPreview = onClearSelfUserVideoPreview,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
Expand Down Expand Up @@ -92,11 +93,13 @@ fun ParticipantTile(
isSelfUserCameraOn: Boolean,
onSelfUserVideoPreviewCreated: (view: View) -> Unit,
modifier: Modifier = Modifier,
shouldFill: Boolean = true,
shouldFillSelfUserCameraPreview: Boolean = false,
shouldFillOthersVideoPreview: Boolean = true,
isZoomingEnabled: Boolean = false,
onClearSelfUserVideoPreview: () -> Unit
) {
val alpha = if (participantTitleState.hasEstablishedAudio) ContentAlpha.high else ContentAlpha.medium
val alpha =
if (participantTitleState.hasEstablishedAudio) ContentAlpha.high else ContentAlpha.medium
Surface(
modifier = modifier
.thenIf(participantTitleState.isSpeaking, activeSpeakerBorderModifier),
Expand All @@ -118,7 +121,8 @@ fun ParticipantTile(
end.linkTo(parent.end)
bottom.linkTo(bottomRow.top)
width = Dimension.fillToConstraints.atMost(maxAvatarSize)
height = Dimension.fillToConstraints.atMost(maxAvatarSize + activeSpeakerBorderPadding)
height =
Dimension.fillToConstraints.atMost(maxAvatarSize + activeSpeakerBorderPadding)
},
avatar = UserAvatarData(
asset = participantTitleState.avatar,
Expand All @@ -129,6 +133,7 @@ fun ParticipantTile(
if (isSelfUser) {
CameraPreview(
isCameraOn = isSelfUserCameraOn,
shouldFill = shouldFillSelfUserCameraPreview,
onSelfUserVideoPreviewCreated = onSelfUserVideoPreviewCreated,
onClearSelfUserVideoPreview = onClearSelfUserVideoPreview
)
Expand All @@ -138,7 +143,7 @@ fun ParticipantTile(
clientId = participantTitleState.clientId,
isCameraOn = participantTitleState.isCameraOn,
isSharingScreen = participantTitleState.isSharingScreen,
shouldFill = shouldFill,
shouldFill = shouldFillOthersVideoPreview,
isZoomingEnabled = isZoomingEnabled
)
}
Expand All @@ -164,7 +169,8 @@ fun ParticipantTile(
}
}

private fun Modifier.thenIf(condition: Boolean, other: Modifier): Modifier = if (condition) this.then(other) else this
private fun Modifier.thenIf(condition: Boolean, other: Modifier): Modifier =
if (condition) this.then(other) else this

private val activeSpeakerBorderModifier
@Composable get() = Modifier
Expand Down Expand Up @@ -216,7 +222,10 @@ private fun BottomRow(
layout(constraints.maxWidth, usernamePlaceable.height) {
muteIconPlaceable?.placeRelative(0, 0)
if (usernamePlaceable.width < constraints.maxWidth - 2 * muteIconWidth) { // can fit in center
usernamePlaceable.placeRelative((constraints.maxWidth - usernamePlaceable.width) / 2, 0)
usernamePlaceable.placeRelative(
(constraints.maxWidth - usernamePlaceable.width) / 2,
0
)
} else { // needs to take all remaining space
usernamePlaceable.placeRelative(muteIconWidth, 0)
}
Expand Down Expand Up @@ -248,6 +257,7 @@ private fun CleanUpRendererIfNeeded(videoRenderer: VideoRenderer) {
private fun CameraPreview(
isCameraOn: Boolean,
onSelfUserVideoPreviewCreated: (view: View) -> Unit,
shouldFill: Boolean = false,
onClearSelfUserVideoPreview: () -> Unit
) {
var isCameraStopped by remember { mutableStateOf(isCameraOn) }
Expand All @@ -259,7 +269,7 @@ private fun CameraPreview(
val videoPreview = remember {
CameraPreviewBuilder(context)
.setBackgroundColor(backgroundColor)
.shouldFill(false)
.shouldFill(shouldFill)
.build()
}
AndroidView(
Expand Down Expand Up @@ -311,6 +321,7 @@ private fun OthersVideoRenderer(

AndroidView(
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
size = it
}
Expand Down Expand Up @@ -339,7 +350,8 @@ private fun OthersVideoRenderer(
val frameLayout = FrameLayout(it)
frameLayout.addView(videoRenderer)
frameLayout
})
}
)
}
}

Expand Down Expand Up @@ -369,7 +381,8 @@ private fun UsernameTile(
hasEstablishedAudio: Boolean,
modifier: Modifier = Modifier,
) {
val color = if (isSpeaking) colorsScheme().primary else colorsScheme().callingParticipantNameBackground
val color =
if (isSpeaking) colorsScheme().primary else colorsScheme().callingParticipantNameBackground
val nameLabelColor =
when {
isSpeaking -> colorsScheme().onPrimary
Expand Down Expand Up @@ -478,7 +491,11 @@ fun PreviewParticipantConnecting() = WireTheme {
@PreviewMultipleThemes
@Composable
fun PreviewParticipantLongNameConnecting() = WireTheme {
PreviewParticipantTile(shape = PreviewTileShape.Regular, hasEstablishedAudio = false, longName = true)
PreviewParticipantTile(
shape = PreviewTileShape.Regular,
hasEstablishedAudio = false,
longName = true
)
}

@PreviewMultipleThemes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.wire.android.BuildConfig
import com.wire.android.ui.calling.model.UICallParticipant
import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList
import com.wire.android.ui.calling.ongoing.fullscreen.SelectedParticipant
Expand All @@ -60,6 +61,7 @@ fun VerticalCallingPager(
isSelfUserMuted: Boolean,
isSelfUserCameraOn: Boolean,
contentHeight: Dp,
contentWidth: Int,
onSelfVideoPreviewCreated: (view: View) -> Unit,
onSelfClearVideoPreview: () -> Unit,
requestVideoStreams: (participants: List<UICallParticipant>) -> Unit,
Expand All @@ -80,12 +82,18 @@ fun VerticalCallingPager(
modifier = Modifier.fillMaxSize()
) { pageIndex ->
if (participants.isNotEmpty()) {

val participantsChunkedList = remember(participants) {
participants.chunked(MAX_TILES_PER_PAGE)
// if PiP is enabled and more than one participant is present,
// we need to remove the first participant(self user) from the list
val newParticipants = if (BuildConfig.PICTURE_IN_PICTURE_ENABLED && participants.size > 1) {
participants.subList(1, participants.size)
} else {
participants
}
val participantsChunkedList = remember(newParticipants) {
newParticipants.chunked(MAX_TILES_PER_PAGE)
}
val participantsWithCameraOn by rememberUpdatedState(participants.count { it.isCameraOn })
val participantsWithScreenShareOn by rememberUpdatedState(participants.count { it.isSharingScreen })
val participantsWithCameraOn by rememberUpdatedState(newParticipants.count { it.isCameraOn })
val participantsWithScreenShareOn by rememberUpdatedState(newParticipants.count { it.isSharingScreen })

if (participantsChunkedList[pageIndex].size <= MAX_ITEMS_FOR_HORIZONTAL_VIEW) {
CallingHorizontalView(
Expand Down Expand Up @@ -143,6 +151,16 @@ fun VerticalCallingPager(
)
}
}
if (BuildConfig.PICTURE_IN_PICTURE_ENABLED && participants.size > 1) {
FloatingSelfUserTile(
modifier = Modifier.align(Alignment.TopEnd),
contentHeight = contentHeight,
contentWidth = contentWidth.toFloat(),
participant = participants.first(),
onSelfUserVideoPreviewCreated = onSelfVideoPreviewCreated,
onClearSelfUserVideoPreview = onSelfClearVideoPreview
)
}
}
}
}
Expand All @@ -164,6 +182,7 @@ private fun PreviewVerticalCallingPager(participants: List<UICallParticipant>) {
isSelfUserMuted = false,
isSelfUserCameraOn = false,
contentHeight = 800.dp,
contentWidth = 300,
onSelfVideoPreviewCreated = {},
onSelfClearVideoPreview = {},
requestVideoStreams = {},
Expand All @@ -174,7 +193,11 @@ private fun PreviewVerticalCallingPager(participants: List<UICallParticipant>) {
@PreviewMultipleThemes
@Composable
fun PreviewVerticalCallingPagerHorizontalView() = WireTheme {
PreviewVerticalCallingPager(participants = buildPreviewParticipantsList(MAX_ITEMS_FOR_HORIZONTAL_VIEW))
PreviewVerticalCallingPager(
participants = buildPreviewParticipantsList(
MAX_ITEMS_FOR_HORIZONTAL_VIEW
)
)
}

@PreviewMultipleThemes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import com.wire.android.BuildConfig
import com.wire.android.ui.calling.model.UICallParticipant
import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList
import com.wire.android.ui.calling.ongoing.fullscreen.SelectedParticipant
Expand Down Expand Up @@ -83,7 +84,7 @@ fun GroupCallGrid(
// since we are getting participants by chunk of 8 items,
// we need to check that we are on first page for self user
val isSelfUser = remember(pageIndex, participants.first()) {
pageIndex == 0 && participants.first() == participant
pageIndex == 0 && participants.first() == participant && !BuildConfig.PICTURE_IN_PICTURE_ENABLED
}

ParticipantTile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import com.wire.android.BuildConfig
import com.wire.android.ui.calling.model.UICallParticipant
import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList
import com.wire.android.ui.calling.ongoing.fullscreen.SelectedParticipant
Expand Down Expand Up @@ -70,7 +71,7 @@ fun CallingHorizontalView(
// since we are getting participants by chunk of 8 items,
// we need to check that we are on first page for self user
val isSelfUser = remember(pageIndex, participants.first()) {
pageIndex == 0 && participants.first() == participant
pageIndex == 0 && participants.first() == participant && !BuildConfig.PICTURE_IN_PICTURE_ENABLED
}
ParticipantTile(
modifier = Modifier
Expand Down
Loading

0 comments on commit 2c3822a

Please sign in to comment.