mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Implement new call grid that has animations.
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.AllNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Animation constants for CallGrid
|
||||
*/
|
||||
private object CallGridDefaults {
|
||||
const val ANIMATION_DURATION_MS = 300L
|
||||
|
||||
private val DefaultEasing = CubicBezierEasing(0.42f, 0f, 0.58f, 1f)
|
||||
|
||||
val positionAnimationSpec: FiniteAnimationSpec<IntOffset> = tween(
|
||||
durationMillis = ANIMATION_DURATION_MS.toInt(),
|
||||
easing = DefaultEasing
|
||||
)
|
||||
|
||||
val alphaAnimationSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = ANIMATION_DURATION_MS.toInt(),
|
||||
easing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1f)
|
||||
)
|
||||
|
||||
val sizeAnimationSpec: FiniteAnimationSpec<IntSize> = tween(
|
||||
durationMillis = ANIMATION_DURATION_MS.toInt(),
|
||||
easing = DefaultEasing
|
||||
)
|
||||
|
||||
val dpAnimationSpec: FiniteAnimationSpec<Dp> = tween(
|
||||
durationMillis = ANIMATION_DURATION_MS.toInt(),
|
||||
easing = DefaultEasing
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a specific grid layout
|
||||
*/
|
||||
@Immutable
|
||||
data class GridConfig(
|
||||
val rows: Int,
|
||||
val columns: Int,
|
||||
val itemsInLastRow: Int,
|
||||
val outerPadding: Dp,
|
||||
val innerSpacing: Dp,
|
||||
val cornerRadius: Dp,
|
||||
val aspectRatio: Float?,
|
||||
val lastColumnSpansFullHeight: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents a calculated cell position and size in the grid
|
||||
*/
|
||||
@Immutable
|
||||
data class GridCell(
|
||||
val index: Int,
|
||||
val x: Float,
|
||||
val y: Float,
|
||||
val width: Float,
|
||||
val height: Float
|
||||
)
|
||||
|
||||
/**
|
||||
* Internal helper for grid layout parameters
|
||||
*/
|
||||
private data class GridLayoutParams(
|
||||
val rows: Int,
|
||||
val cols: Int,
|
||||
val lastRowItems: Int,
|
||||
val lastColSpans: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Strategy for determining grid configuration based on device size
|
||||
*/
|
||||
sealed class CallGridStrategy(val maxTiles: Int) {
|
||||
abstract fun getConfig(count: Int): GridConfig
|
||||
|
||||
class SmallPortrait : CallGridStrategy(6) {
|
||||
override fun getConfig(count: Int): GridConfig {
|
||||
val (rows, cols, lastRowItems) = when (count) {
|
||||
1 -> GridLayoutParams(1, 1, 1)
|
||||
2 -> GridLayoutParams(2, 1, 1)
|
||||
3 -> GridLayoutParams(2, 2, 1)
|
||||
4 -> GridLayoutParams(2, 2, 2)
|
||||
5 -> GridLayoutParams(3, 2, 1)
|
||||
else -> GridLayoutParams(3, 2, 2)
|
||||
}
|
||||
return GridConfig(
|
||||
rows = rows,
|
||||
columns = cols,
|
||||
itemsInLastRow = lastRowItems,
|
||||
outerPadding = if (count == 1) 0.dp else 16.dp,
|
||||
innerSpacing = if (count == 1) 0.dp else 12.dp,
|
||||
cornerRadius = if (count == 1) 0.dp else 32.dp,
|
||||
aspectRatio = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SmallLandscape : CallGridStrategy(6) {
|
||||
override fun getConfig(count: Int): GridConfig {
|
||||
// For 5 items: 2 rows, 3 columns, with the last column (5th item) spanning full height
|
||||
val (rows, cols, lastRowItems, lastColSpans) = when (count) {
|
||||
1 -> GridLayoutParams(1, 1, 1)
|
||||
2 -> GridLayoutParams(1, 2, 2)
|
||||
3 -> GridLayoutParams(2, 2, 1)
|
||||
4 -> GridLayoutParams(2, 2, 2)
|
||||
5 -> GridLayoutParams(2, 3, 1, lastColSpans = true)
|
||||
else -> GridLayoutParams(2, 3, 3)
|
||||
}
|
||||
return GridConfig(
|
||||
rows = rows,
|
||||
columns = cols,
|
||||
itemsInLastRow = lastRowItems,
|
||||
outerPadding = if (count == 1) 0.dp else 16.dp,
|
||||
innerSpacing = if (count == 1) 0.dp else 12.dp,
|
||||
cornerRadius = if (count == 1) 0.dp else 32.dp,
|
||||
aspectRatio = null,
|
||||
lastColumnSpansFullHeight = lastColSpans
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Medium : CallGridStrategy(9) {
|
||||
override fun getConfig(count: Int): GridConfig {
|
||||
val (rows, cols, lastRowItems) = when (count) {
|
||||
1 -> GridLayoutParams(1, 1, 1)
|
||||
2 -> GridLayoutParams(2, 1, 1)
|
||||
3 -> GridLayoutParams(2, 2, 1)
|
||||
4 -> GridLayoutParams(2, 2, 2)
|
||||
5 -> GridLayoutParams(3, 2, 1)
|
||||
6 -> GridLayoutParams(3, 2, 2)
|
||||
7 -> GridLayoutParams(3, 3, 1)
|
||||
8 -> GridLayoutParams(3, 3, 2)
|
||||
else -> GridLayoutParams(3, 3, 3)
|
||||
}
|
||||
return GridConfig(
|
||||
rows = rows,
|
||||
columns = cols,
|
||||
itemsInLastRow = lastRowItems,
|
||||
outerPadding = 24.dp,
|
||||
innerSpacing = 12.dp,
|
||||
cornerRadius = 32.dp,
|
||||
aspectRatio = if (count == 1) 9f / 16f else 5f / 4f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Large : CallGridStrategy(12) {
|
||||
override fun getConfig(count: Int): GridConfig {
|
||||
val (rows, cols, lastRowItems) = when (count) {
|
||||
1 -> GridLayoutParams(1, 1, 1)
|
||||
2 -> GridLayoutParams(1, 2, 2)
|
||||
3 -> GridLayoutParams(1, 3, 3)
|
||||
4 -> GridLayoutParams(2, 2, 2)
|
||||
5 -> GridLayoutParams(2, 3, 2)
|
||||
6 -> GridLayoutParams(2, 3, 3)
|
||||
7 -> GridLayoutParams(2, 4, 3)
|
||||
8 -> GridLayoutParams(2, 4, 4)
|
||||
9 -> GridLayoutParams(3, 4, 1)
|
||||
10 -> GridLayoutParams(3, 4, 2)
|
||||
11 -> GridLayoutParams(3, 4, 3)
|
||||
else -> GridLayoutParams(3, 4, 4)
|
||||
}
|
||||
return GridConfig(
|
||||
rows = rows,
|
||||
columns = cols,
|
||||
itemsInLastRow = lastRowItems,
|
||||
outerPadding = 24.dp,
|
||||
innerSpacing = 12.dp,
|
||||
cornerRadius = 32.dp,
|
||||
aspectRatio = if (count == 1) 9f / 16f else 5f / 4f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers the appropriate CallGridStrategy based on current window size
|
||||
*/
|
||||
@Composable
|
||||
fun rememberCallGridStrategy(): CallGridStrategy {
|
||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||
|
||||
return remember(windowSizeClass) {
|
||||
val isWidthExpanded = windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND)
|
||||
val isWidthMedium = windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
|
||||
val isHeightMedium = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)
|
||||
|
||||
when {
|
||||
isWidthExpanded && isHeightMedium -> CallGridStrategy.Large()
|
||||
isWidthMedium && isHeightMedium -> CallGridStrategy.Medium()
|
||||
!isHeightMedium -> CallGridStrategy.SmallLandscape()
|
||||
else -> CallGridStrategy.SmallPortrait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate grid cell positions and sizes.
|
||||
*
|
||||
* When aspectRatio is specified: Items are packed together with only inner spacing
|
||||
* between them, and the entire group is centered in the container.
|
||||
*
|
||||
* When aspectRatio is null (compact mode): Items fill the available space, with
|
||||
* partial rows stretching to fill the full width.
|
||||
*
|
||||
* When lastColumnSpansFullHeight is true: The last item occupies the rightmost column
|
||||
* and spans the full height (all rows).
|
||||
*/
|
||||
private fun calculateGridCells(
|
||||
config: GridConfig,
|
||||
containerWidth: Float,
|
||||
containerHeight: Float,
|
||||
itemCount: Int
|
||||
): List<GridCell> {
|
||||
if (itemCount == 0) return emptyList()
|
||||
|
||||
val padding = config.outerPadding.value
|
||||
val spacing = config.innerSpacing.value
|
||||
val availableWidth = containerWidth - (padding * 2)
|
||||
val availableHeight = containerHeight - (padding * 2)
|
||||
|
||||
if (config.lastColumnSpansFullHeight && itemCount > 1) {
|
||||
return calculateGridCellsWithSpanningColumn(
|
||||
config = config,
|
||||
availableWidth = availableWidth,
|
||||
availableHeight = availableHeight,
|
||||
padding = padding,
|
||||
spacing = spacing,
|
||||
itemCount = itemCount
|
||||
)
|
||||
}
|
||||
|
||||
val maxCellWidth = (availableWidth - (spacing * (config.columns - 1))) / config.columns
|
||||
val maxCellHeight = (availableHeight - (spacing * (config.rows - 1))) / config.rows
|
||||
|
||||
val (itemWidth, itemHeight) = if (config.aspectRatio != null) {
|
||||
val targetAspectRatio = config.aspectRatio
|
||||
val cellAspectRatio = maxCellWidth / maxCellHeight
|
||||
|
||||
if (cellAspectRatio > targetAspectRatio) {
|
||||
val constrainedWidth = maxCellHeight * targetAspectRatio
|
||||
constrainedWidth to maxCellHeight
|
||||
} else {
|
||||
val constrainedHeight = maxCellWidth / targetAspectRatio
|
||||
maxCellWidth to constrainedHeight
|
||||
}
|
||||
} else {
|
||||
maxCellWidth to maxCellHeight
|
||||
}
|
||||
|
||||
val totalGridWidth = (config.columns * itemWidth) + ((config.columns - 1) * spacing)
|
||||
val totalGridHeight = (config.rows * itemHeight) + ((config.rows - 1) * spacing)
|
||||
|
||||
val gridStartX = padding + (availableWidth - totalGridWidth) / 2
|
||||
val gridStartY = padding + (availableHeight - totalGridHeight) / 2
|
||||
|
||||
val cells = mutableListOf<GridCell>()
|
||||
|
||||
var index = 0
|
||||
for (row in 0 until config.rows) {
|
||||
val isLastRow = row == config.rows - 1
|
||||
val itemsInThisRow = if (isLastRow) config.itemsInLastRow else config.columns
|
||||
val remainingItems = itemCount - index
|
||||
|
||||
if (remainingItems <= 0) break
|
||||
|
||||
val actualItemsInRow = min(itemsInThisRow, remainingItems)
|
||||
val isPartialRow = actualItemsInRow < config.columns
|
||||
|
||||
// Stretch items in partial rows to fill width (compact mode only)
|
||||
val cellWidthForRow = if (config.aspectRatio == null && isPartialRow) {
|
||||
(totalGridWidth - (spacing * (actualItemsInRow - 1))) / actualItemsInRow
|
||||
} else {
|
||||
itemWidth
|
||||
}
|
||||
|
||||
val rowWidth = (actualItemsInRow * cellWidthForRow) + ((actualItemsInRow - 1) * spacing)
|
||||
val rowXOffset = (totalGridWidth - rowWidth) / 2
|
||||
|
||||
for (col in 0 until actualItemsInRow) {
|
||||
val x = gridStartX + rowXOffset + col * (cellWidthForRow + spacing)
|
||||
val y = gridStartY + row * (itemHeight + spacing)
|
||||
|
||||
cells.add(
|
||||
GridCell(
|
||||
index = index,
|
||||
x = x,
|
||||
y = y,
|
||||
width = cellWidthForRow,
|
||||
height = itemHeight
|
||||
)
|
||||
)
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
return cells
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate grid cells when the last column spans full height.
|
||||
* Layout: Regular grid on the left, with the last item taking the rightmost column
|
||||
* and spanning all rows.
|
||||
*/
|
||||
private fun calculateGridCellsWithSpanningColumn(
|
||||
config: GridConfig,
|
||||
availableWidth: Float,
|
||||
availableHeight: Float,
|
||||
padding: Float,
|
||||
spacing: Float,
|
||||
itemCount: Int
|
||||
): List<GridCell> {
|
||||
val cells = mutableListOf<GridCell>()
|
||||
|
||||
val columnsForRegularItems = config.columns - 1
|
||||
val regularItemCount = itemCount - 1
|
||||
|
||||
val cellWidth = (availableWidth - (spacing * (config.columns - 1))) / config.columns
|
||||
val cellHeight = (availableHeight - (spacing * (config.rows - 1))) / config.rows
|
||||
|
||||
val totalGridWidth = (config.columns * cellWidth) + ((config.columns - 1) * spacing)
|
||||
val totalGridHeight = (config.rows * cellHeight) + ((config.rows - 1) * spacing)
|
||||
|
||||
val gridStartX = padding + (availableWidth - totalGridWidth) / 2
|
||||
val gridStartY = padding + (availableHeight - totalGridHeight) / 2
|
||||
|
||||
// Place regular items in column-major order (fills columns top-to-bottom, left-to-right)
|
||||
var index = 0
|
||||
for (col in 0 until columnsForRegularItems) {
|
||||
for (row in 0 until config.rows) {
|
||||
if (index >= regularItemCount) break
|
||||
|
||||
val x = gridStartX + col * (cellWidth + spacing)
|
||||
val y = gridStartY + row * (cellHeight + spacing)
|
||||
|
||||
cells.add(
|
||||
GridCell(
|
||||
index = index,
|
||||
x = x,
|
||||
y = y,
|
||||
width = cellWidth,
|
||||
height = cellHeight
|
||||
)
|
||||
)
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
// Spanning item takes full height
|
||||
val spanningX = gridStartX + columnsForRegularItems * (cellWidth + spacing)
|
||||
val spanningY = gridStartY
|
||||
val spanningHeight = totalGridHeight
|
||||
|
||||
cells.add(
|
||||
GridCell(
|
||||
index = regularItemCount,
|
||||
x = spanningX,
|
||||
y = spanningY,
|
||||
width = cellWidth,
|
||||
height = spanningHeight
|
||||
)
|
||||
)
|
||||
|
||||
return cells
|
||||
}
|
||||
|
||||
/**
|
||||
* An animated grid layout for call participants.
|
||||
*
|
||||
* Features:
|
||||
* - Smooth position animations when items move
|
||||
* - Fade-in animation for new items (after position animations complete)
|
||||
* - Crossfade for swapped items (same position, different participant)
|
||||
* - Device-aware grid configurations
|
||||
*
|
||||
* @param items List of items to display, each with a stable key
|
||||
* @param modifier Modifier for the grid container
|
||||
* @param itemKey Function to extract a stable key from each item
|
||||
* @param content Composable content for each item
|
||||
*/
|
||||
@Composable
|
||||
fun <T> CallGrid(
|
||||
items: List<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
itemKey: (T) -> Any,
|
||||
content: @Composable (item: T, modifier: Modifier) -> Unit
|
||||
) {
|
||||
val strategy = rememberCallGridStrategy()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val positionAnimatables: SnapshotStateMap<Any, Animatable<IntOffset, *>> = remember { mutableStateMapOf() }
|
||||
val sizeAnimatables: SnapshotStateMap<Any, Animatable<IntSize, *>> = remember { mutableStateMapOf() }
|
||||
val alphaAnimatables: SnapshotStateMap<Any, Animatable<Float, *>> = remember { mutableStateMapOf() }
|
||||
val knownKeys = remember { mutableSetOf<Any>() }
|
||||
|
||||
val displayCount = min(items.size, strategy.maxTiles)
|
||||
val displayItems = items.take(displayCount)
|
||||
val config = remember(strategy, displayCount) { strategy.getConfig(displayCount) }
|
||||
|
||||
val animatedCornerRadius by animateDpAsState(
|
||||
targetValue = config.cornerRadius,
|
||||
animationSpec = CallGridDefaults.dpAnimationSpec,
|
||||
label = "cornerRadius"
|
||||
)
|
||||
|
||||
val currentKeys = displayItems.map { itemKey(it) }.toSet()
|
||||
val newKeys = currentKeys - knownKeys
|
||||
val hasExistingItems = knownKeys.isNotEmpty()
|
||||
|
||||
newKeys.forEach { key ->
|
||||
if (hasExistingItems) {
|
||||
alphaAnimatables[key] = Animatable(0f)
|
||||
}
|
||||
knownKeys.add(key)
|
||||
}
|
||||
|
||||
val removedKeys = knownKeys - currentKeys
|
||||
removedKeys.forEach { key ->
|
||||
positionAnimatables.remove(key)
|
||||
sizeAnimatables.remove(key)
|
||||
alphaAnimatables.remove(key)
|
||||
knownKeys.remove(key)
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val containerWidthPx = constraints.maxWidth.toFloat()
|
||||
val containerHeightPx = constraints.maxHeight.toFloat()
|
||||
|
||||
val cells = remember(config, containerWidthPx, containerHeightPx, displayCount) {
|
||||
calculateGridCells(
|
||||
config = config,
|
||||
containerWidth = containerWidthPx,
|
||||
containerHeight = containerHeightPx,
|
||||
itemCount = displayCount
|
||||
)
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
Layout(
|
||||
content = {
|
||||
displayItems.forEachIndexed { index, item ->
|
||||
val itemKeyValue = itemKey(item)
|
||||
key(itemKeyValue) {
|
||||
val alpha = alphaAnimatables[itemKeyValue]?.value ?: 1f
|
||||
val animatedSize = sizeAnimatables[itemKeyValue]?.value
|
||||
val cell = cells.getOrNull(index)
|
||||
|
||||
if (cell != null) {
|
||||
val widthPx = animatedSize?.width ?: cell.width.roundToInt()
|
||||
val heightPx = animatedSize?.height ?: cell.height.roundToInt()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.layoutId(itemKeyValue)
|
||||
.alpha(alpha)
|
||||
) {
|
||||
content(
|
||||
item,
|
||||
Modifier
|
||||
.size(
|
||||
width = with(density) { widthPx.toDp() },
|
||||
height = with(density) { heightPx.toDp() }
|
||||
)
|
||||
.clip(RoundedCornerShape(animatedCornerRadius))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
displayItems.forEachIndexed { index, item ->
|
||||
val itemKeyValue = itemKey(item)
|
||||
val cell = cells.getOrNull(index) ?: return@forEachIndexed
|
||||
val targetPosition = IntOffset(cell.x.roundToInt(), cell.y.roundToInt())
|
||||
val targetSize = IntSize(cell.width.roundToInt(), cell.height.roundToInt())
|
||||
|
||||
val existingPosition = positionAnimatables[itemKeyValue]
|
||||
if (existingPosition == null) {
|
||||
positionAnimatables[itemKeyValue] = Animatable(targetPosition, IntOffset.VectorConverter)
|
||||
if (hasExistingItems && itemKeyValue in newKeys) {
|
||||
scope.launch {
|
||||
delay(CallGridDefaults.ANIMATION_DURATION_MS)
|
||||
alphaAnimatables[itemKeyValue]?.animateTo(1f, CallGridDefaults.alphaAnimationSpec)
|
||||
}
|
||||
} else {
|
||||
if (alphaAnimatables[itemKeyValue] == null) {
|
||||
alphaAnimatables[itemKeyValue] = Animatable(1f)
|
||||
}
|
||||
}
|
||||
} else if (existingPosition.targetValue != targetPosition) {
|
||||
scope.launch {
|
||||
existingPosition.animateTo(targetPosition, CallGridDefaults.positionAnimationSpec)
|
||||
}
|
||||
}
|
||||
|
||||
val existingSize = sizeAnimatables[itemKeyValue]
|
||||
if (existingSize == null) {
|
||||
sizeAnimatables[itemKeyValue] = Animatable(targetSize, IntSize.VectorConverter)
|
||||
} else if (existingSize.targetValue != targetSize) {
|
||||
scope.launch {
|
||||
existingSize.animateTo(targetSize, CallGridDefaults.sizeAnimationSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val placeables = measurables.mapIndexed { index, measurable ->
|
||||
val itemKeyValue = displayItems.getOrNull(index)?.let { itemKey(it) }
|
||||
val animatedSize = itemKeyValue?.let { sizeAnimatables[it]?.value }
|
||||
val cell = cells.getOrNull(index)
|
||||
|
||||
if (animatedSize != null) {
|
||||
measurable.measure(
|
||||
Constraints.fixed(animatedSize.width, animatedSize.height)
|
||||
)
|
||||
} else if (cell != null) {
|
||||
measurable.measure(
|
||||
Constraints.fixed(
|
||||
cell.width.roundToInt(),
|
||||
cell.height.roundToInt()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
measurable.measure(Constraints())
|
||||
}
|
||||
}
|
||||
|
||||
val keyToPlaceable = measurables.zip(placeables).associate { (measurable, placeable) ->
|
||||
measurable.layoutId to placeable
|
||||
}
|
||||
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
displayItems.forEach { item ->
|
||||
val itemKeyValue = itemKey(item)
|
||||
val placeable = keyToPlaceable[itemKeyValue]
|
||||
val position = positionAnimatables[itemKeyValue]?.value
|
||||
|
||||
if (placeable != null && position != null) {
|
||||
placeable.place(position.x, position.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preview
|
||||
|
||||
@AllNightPreviews
|
||||
@Composable
|
||||
private fun CallGridPreview() {
|
||||
Previews.Preview {
|
||||
var count by remember { mutableStateOf(1) }
|
||||
val items = remember(count) { (1..count).toList() }
|
||||
|
||||
val colors = listOf(
|
||||
Color(0xFF5E97F6),
|
||||
Color(0xFF9CCC65),
|
||||
Color(0xFFFFB74D),
|
||||
Color(0xFFEF5350),
|
||||
Color(0xFFAB47BC),
|
||||
Color(0xFF26A69A),
|
||||
Color(0xFF78909C),
|
||||
Color(0xFFEC407A),
|
||||
Color(0xFF7E57C2),
|
||||
Color(0xFF29B6F6),
|
||||
Color(0xFFD4E157),
|
||||
Color(0xFFFF7043)
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CallGrid(
|
||||
items = items,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
itemKey = { it }
|
||||
) { item, itemModifier ->
|
||||
Box(
|
||||
modifier = itemModifier.background(colors[(item - 1) % colors.size]),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = item.toString(),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.TopStart)
|
||||
) {
|
||||
Button(onClick = { count = max(1, count - 1) }) {
|
||||
Text("-")
|
||||
}
|
||||
Button(onClick = { count = min(12, count + 1) }) {
|
||||
Text("+")
|
||||
}
|
||||
Text(
|
||||
text = "Count: $count",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,32 +5,17 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
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.pager.PagerState
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.movableContentOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.window.core.layout.WindowHeightSizeClass
|
||||
import androidx.window.core.layout.WindowWidthSizeClass
|
||||
import org.signal.core.ui.compose.AllNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
@@ -38,7 +23,6 @@ import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
fun CallParticipantsPager(
|
||||
@@ -50,6 +34,26 @@ fun CallParticipantsPager(
|
||||
return
|
||||
}
|
||||
|
||||
// Use movableContentOf to preserve CallGrid state when switching between
|
||||
// single participant (no pager) and multiple participants (with pager)
|
||||
val callGridContent = remember {
|
||||
movableContentOf { state: CallParticipantsPagerState, mod: Modifier ->
|
||||
CallGrid(
|
||||
items = state.callParticipants,
|
||||
modifier = mod,
|
||||
itemKey = { it.callParticipantId }
|
||||
) { participant, itemModifier ->
|
||||
RemoteParticipantContent(
|
||||
participant = participant,
|
||||
renderInPip = state.isRenderInPip,
|
||||
raiseHandAllowed = false,
|
||||
onInfoMoreInfoClick = null,
|
||||
modifier = itemModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callParticipantsPagerState.callParticipants.size > 1) {
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
@@ -59,10 +63,7 @@ fun CallParticipantsPager(
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
callGridContent(callParticipantsPagerState, Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
1 -> {
|
||||
@@ -77,238 +78,13 @@ fun CallParticipantsPager(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState: CallParticipantsPagerState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val layoutStrategy = rememberRemoteParticipantsLayoutStrategy()
|
||||
val count = min(callParticipantsPagerState.callParticipants.size, layoutStrategy.maxDeviceCount)
|
||||
|
||||
val state = remember(count) {
|
||||
layoutStrategy.buildStateForCount(count)
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.padding(state.outerInsets)
|
||||
) {
|
||||
val width = maxWidth
|
||||
val height = maxHeight
|
||||
|
||||
val nLines = (count + state.maxItemsInEachLine - 1) / state.maxItemsInEachLine
|
||||
|
||||
when (layoutStrategy.lineType) {
|
||||
LayoutStrategyLineType.COLUMN -> {
|
||||
ColumnBasedLayout(
|
||||
containerWidth = width,
|
||||
containerHeight = height,
|
||||
numberOfLines = nLines,
|
||||
numberOfParticipants = count,
|
||||
state = state,
|
||||
callParticipantsPagerState = callParticipantsPagerState
|
||||
)
|
||||
}
|
||||
|
||||
LayoutStrategyLineType.ROW -> {
|
||||
RowBasedLayout(
|
||||
containerWidth = width,
|
||||
containerHeight = height,
|
||||
numberOfLines = nLines,
|
||||
numberOfParticipants = count,
|
||||
state = state,
|
||||
callParticipantsPagerState = callParticipantsPagerState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowBasedLayout(
|
||||
containerWidth: Dp,
|
||||
containerHeight: Dp,
|
||||
numberOfLines: Int,
|
||||
numberOfParticipants: Int,
|
||||
state: RemoteParticipantsLayoutState,
|
||||
callParticipantsPagerState: CallParticipantsPagerState
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(state.innerInsets),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val batches = callParticipantsPagerState.callParticipants
|
||||
.take(numberOfParticipants)
|
||||
.chunked(state.maxItemsInEachLine) {
|
||||
it.reversed()
|
||||
}
|
||||
|
||||
val lastParticipant = batches.last().last()
|
||||
|
||||
batches.forEach { batch ->
|
||||
Column(
|
||||
verticalArrangement = spacedBy(state.innerInsets),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
batch.forEach { participant ->
|
||||
|
||||
AutoSizedParticipant(
|
||||
lineType = LayoutStrategyLineType.ROW,
|
||||
containerWidth = containerWidth,
|
||||
containerHeight = containerHeight,
|
||||
numberOfLines = numberOfLines,
|
||||
numberOfParticipants = numberOfParticipants,
|
||||
isLastParticipant = lastParticipant == participant,
|
||||
isRenderInPip = callParticipantsPagerState.isRenderInPip,
|
||||
state = state,
|
||||
participant = participant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnBasedLayout(
|
||||
containerWidth: Dp,
|
||||
containerHeight: Dp,
|
||||
numberOfLines: Int,
|
||||
numberOfParticipants: Int,
|
||||
state: RemoteParticipantsLayoutState,
|
||||
callParticipantsPagerState: CallParticipantsPagerState
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = spacedBy(state.innerInsets),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val batches = callParticipantsPagerState.callParticipants
|
||||
.take(numberOfParticipants)
|
||||
.chunked(state.maxItemsInEachLine)
|
||||
|
||||
val lastParticipant = batches.last().last()
|
||||
|
||||
batches.forEach { batch ->
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(state.innerInsets),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
batch.forEach { participant ->
|
||||
AutoSizedParticipant(
|
||||
lineType = LayoutStrategyLineType.COLUMN,
|
||||
containerWidth = containerWidth,
|
||||
containerHeight = containerHeight,
|
||||
numberOfLines = numberOfLines,
|
||||
numberOfParticipants = numberOfParticipants,
|
||||
isLastParticipant = lastParticipant == participant,
|
||||
isRenderInPip = callParticipantsPagerState.isRenderInPip,
|
||||
state = state,
|
||||
participant = participant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AutoSizedParticipant(
|
||||
lineType: LayoutStrategyLineType,
|
||||
containerWidth: Dp,
|
||||
containerHeight: Dp,
|
||||
numberOfLines: Int,
|
||||
numberOfParticipants: Int,
|
||||
isLastParticipant: Boolean,
|
||||
isRenderInPip: Boolean,
|
||||
state: RemoteParticipantsLayoutState,
|
||||
participant: CallParticipant
|
||||
) {
|
||||
val maxSize = when (lineType) {
|
||||
LayoutStrategyLineType.COLUMN -> {
|
||||
val itemMaximumHeight = (containerHeight - (state.innerInsets * (numberOfLines - 1))) / numberOfLines.toFloat()
|
||||
val itemMaximumWidth = (containerWidth - (state.innerInsets * (state.maxItemsInEachLine - 1))) / state.maxItemsInEachLine.toFloat()
|
||||
|
||||
DpSize(itemMaximumWidth, itemMaximumHeight)
|
||||
}
|
||||
|
||||
LayoutStrategyLineType.ROW -> {
|
||||
val itemMaximumWidth = (containerWidth - (state.innerInsets * (numberOfLines - 1))) / numberOfLines.toFloat()
|
||||
val itemMaximumHeight = (containerHeight - (state.innerInsets * (state.maxItemsInEachLine - 1))) / state.maxItemsInEachLine.toFloat()
|
||||
|
||||
DpSize(itemMaximumWidth, itemMaximumHeight)
|
||||
}
|
||||
}
|
||||
|
||||
val aspectRatio = state.aspectRatio ?: -1f
|
||||
val sizeModifier = when {
|
||||
aspectRatio > 0f ->
|
||||
Modifier.size(
|
||||
largestRectangleWithAspectRatio(
|
||||
maxSize.width,
|
||||
maxSize.height,
|
||||
aspectRatio
|
||||
)
|
||||
)
|
||||
|
||||
isLastParticipant && numberOfParticipants % 2 == 1 -> Modifier.fillMaxSize()
|
||||
else -> Modifier.size(DpSize(maxSize.width, maxSize.height))
|
||||
}
|
||||
|
||||
RemoteParticipantContent(
|
||||
participant = participant,
|
||||
renderInPip = isRenderInPip,
|
||||
raiseHandAllowed = false,
|
||||
onInfoMoreInfoClick = null,
|
||||
modifier = sizeModifier
|
||||
.clip(RoundedCornerShape(state.cornerRadius))
|
||||
)
|
||||
}
|
||||
|
||||
private fun largestRectangleWithAspectRatio(
|
||||
containerWidth: Dp,
|
||||
containerHeight: Dp,
|
||||
aspectRatio: Float
|
||||
): DpSize {
|
||||
val containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
return if (containerAspectRatio > aspectRatio) {
|
||||
DpSize(
|
||||
width = containerHeight * aspectRatio,
|
||||
height = containerHeight
|
||||
)
|
||||
} else {
|
||||
DpSize(
|
||||
width = containerWidth,
|
||||
height = containerWidth / aspectRatio
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberRemoteParticipantsLayoutStrategy(): RemoteParticipantsLayoutStrategy {
|
||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||
|
||||
return remember(windowSizeClass) {
|
||||
when {
|
||||
windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT -> RemoteParticipantsLayoutStrategy.SmallLandscape()
|
||||
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT -> RemoteParticipantsLayoutStrategy.SmallPortrait()
|
||||
windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM -> RemoteParticipantsLayoutStrategy.Medium()
|
||||
else -> RemoteParticipantsLayoutStrategy.Large()
|
||||
}
|
||||
callGridContent(callParticipantsPagerState, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@AllNightPreviews
|
||||
@Composable
|
||||
private fun CallParticipantsLayoutComponentPreview() {
|
||||
private fun CallParticipantsPagerPreview() {
|
||||
Previews.Preview {
|
||||
val participants = remember {
|
||||
(1..5).map {
|
||||
@@ -334,10 +110,19 @@ private fun CallParticipantsLayoutComponentPreview() {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
CallGrid(
|
||||
items = state.callParticipants,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
itemKey = { it.callParticipantId }
|
||||
) { participant, itemModifier ->
|
||||
RemoteParticipantContent(
|
||||
participant = participant,
|
||||
renderInPip = state.isRenderInPip,
|
||||
raiseHandAllowed = false,
|
||||
onInfoMoreInfoClick = null,
|
||||
modifier = itemModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,76 +134,3 @@ data class CallParticipantsPagerState(
|
||||
val isRenderInPip: Boolean = false,
|
||||
val hideAvatar: Boolean = false
|
||||
)
|
||||
|
||||
private sealed class RemoteParticipantsLayoutStrategy(
|
||||
val maxDeviceCount: Int,
|
||||
val lineType: LayoutStrategyLineType = LayoutStrategyLineType.COLUMN
|
||||
) {
|
||||
|
||||
abstract fun buildStateForCount(count: Int): RemoteParticipantsLayoutState
|
||||
|
||||
class SmallLandscape : RemoteParticipantsLayoutStrategy(6, LayoutStrategyLineType.ROW) {
|
||||
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
|
||||
return RemoteParticipantsLayoutState(
|
||||
outerInsets = if (count < 2) 0.dp else 16.dp,
|
||||
innerInsets = if (count < 2) 0.dp else 12.dp,
|
||||
cornerRadius = if (count < 2) 0.dp else CallScreenMetrics.FocusedRendererCornerSize,
|
||||
maxItemsInEachLine = if (count < 3) 1 else 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SmallPortrait : RemoteParticipantsLayoutStrategy(6) {
|
||||
|
||||
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
|
||||
return RemoteParticipantsLayoutState(
|
||||
outerInsets = if (count < 2) 0.dp else 16.dp,
|
||||
innerInsets = if (count < 2) 0.dp else 12.dp,
|
||||
cornerRadius = if (count < 2) 0.dp else CallScreenMetrics.FocusedRendererCornerSize,
|
||||
maxItemsInEachLine = if (count < 3) 1 else 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Medium : RemoteParticipantsLayoutStrategy(9) {
|
||||
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
|
||||
return RemoteParticipantsLayoutState(
|
||||
outerInsets = 24.dp,
|
||||
innerInsets = 12.dp,
|
||||
cornerRadius = CallScreenMetrics.FocusedRendererCornerSize,
|
||||
aspectRatio = if (count < 2) 9 / 16f else 5 / 4f,
|
||||
maxItemsInEachLine = when {
|
||||
count < 3 -> 1
|
||||
count < 7 -> 2
|
||||
else -> 3
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Large : RemoteParticipantsLayoutStrategy(12) {
|
||||
override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState {
|
||||
return RemoteParticipantsLayoutState(
|
||||
outerInsets = 24.dp,
|
||||
innerInsets = 12.dp,
|
||||
cornerRadius = CallScreenMetrics.FocusedRendererCornerSize,
|
||||
aspectRatio = if (count < 2) 9 / 16f else 5 / 4f,
|
||||
maxItemsInEachLine = when {
|
||||
count < 4 -> 3
|
||||
count == 4 -> 2
|
||||
count < 7 -> 3
|
||||
else -> 4
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class RemoteParticipantsLayoutState(
|
||||
val outerInsets: Dp,
|
||||
val innerInsets: Dp,
|
||||
val cornerRadius: Dp,
|
||||
val maxItemsInEachLine: Int,
|
||||
val aspectRatio: Float? = null
|
||||
)
|
||||
|
||||
@@ -241,7 +241,7 @@ fun CallScreen(
|
||||
|
||||
val selfPipHorizontalPadding = 32.dp
|
||||
val shouldNotApplyBottomPaddingToViewPort = currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
|
||||
val selfPipBottomInset: Dp = if (shouldNotApplyBottomPaddingToViewPort) {
|
||||
val selfPipBottomInset: Dp = if (shouldNotApplyBottomPaddingToViewPort && localRenderState != WebRtcLocalRenderState.SMALLER_RECTANGLE) {
|
||||
val containerWidth = maxWidth
|
||||
val sheetWidth = BottomSheetDefaults.SheetMaxWidth
|
||||
val widthOfPip = rememberSelfPipSize(localRenderState).width
|
||||
@@ -261,7 +261,12 @@ fun CallScreen(
|
||||
0.dp
|
||||
}
|
||||
|
||||
val reactionsAndRaisesHandBottomInset = if (shouldNotApplyBottomPaddingToViewPort) {
|
||||
// Reactions/raised hands need bottom inset to stay above the bottom sheet,
|
||||
// UNLESS the overflow row is present (portrait + large group call), in which case
|
||||
// the reactions sit above the overflow row naturally.
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
val hasOverflowRow = isPortrait && overflowParticipants.size > 1
|
||||
val reactionsAndRaisesHandBottomInset = if (shouldNotApplyBottomPaddingToViewPort && !hasOverflowRow) {
|
||||
padding
|
||||
} else {
|
||||
0.dp
|
||||
@@ -592,7 +597,7 @@ private fun CallScreenPreview() {
|
||||
isMicEnabled = true,
|
||||
displayVideoToggle = true,
|
||||
displayGroupRingingToggle = true,
|
||||
displayStartCallButton = true
|
||||
displayStartCallButton = false
|
||||
),
|
||||
callParticipantsPagerState = CallParticipantsPagerState(
|
||||
callParticipants = participants,
|
||||
@@ -609,7 +614,7 @@ private fun CallScreenPreview() {
|
||||
2
|
||||
)
|
||||
),
|
||||
localRenderState = WebRtcLocalRenderState.FOCUSED,
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE,
|
||||
callScreenDialogType = CallScreenDialogType.NONE,
|
||||
callInfoView = {
|
||||
Text(text = "Call Info View Preview", modifier = Modifier.alpha(it))
|
||||
@@ -635,7 +640,7 @@ private fun CallScreenPreview() {
|
||||
onNavigationClick = {},
|
||||
onLocalPictureInPictureClicked = {},
|
||||
onLocalPictureInPictureFocusClicked = {},
|
||||
overflowParticipants = emptyList(), // participants,
|
||||
overflowParticipants = participants,
|
||||
onControlsToggled = {},
|
||||
reactions = listOf(
|
||||
GroupCallReactionEvent(
|
||||
|
||||
@@ -22,8 +22,8 @@ fun PictureInPictureCallScreen(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
CallParticipantsLayoutComponent(
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
CallGrid(
|
||||
items = callParticipantsPagerState.callParticipants,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
@@ -33,6 +33,15 @@ fun PictureInPictureCallScreen(
|
||||
}
|
||||
},
|
||||
enabled = false
|
||||
)
|
||||
)
|
||||
),
|
||||
itemKey = { it.callParticipantId }
|
||||
) { participant, itemModifier ->
|
||||
RemoteParticipantContent(
|
||||
participant = participant,
|
||||
renderInPip = callParticipantsPagerState.isRenderInPip,
|
||||
raiseHandAllowed = false,
|
||||
onInfoMoreInfoClick = null,
|
||||
modifier = itemModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user