mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-28 07:51:42 +01:00
Fix padding on call controls.
This commit is contained in:
+59
-60
@@ -40,11 +40,13 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.signal.core.ui.compose.AllNightPreviews
|
||||
import org.signal.core.ui.compose.SignalPreviewWrapper
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.ringrtc.GroupCall
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -118,7 +120,8 @@ object RaiseHandSnackbar {
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@AllNightPreviews
|
||||
@Composable
|
||||
private fun RaiseHandSnackbarPreview() {
|
||||
RaiseHand(
|
||||
@@ -139,67 +142,63 @@ private fun RaiseHand(
|
||||
exit = shrinkOut(shrinkTowards = Alignment.CenterEnd) + fadeOut(),
|
||||
modifier = modifier
|
||||
) {
|
||||
SignalTheme(
|
||||
isDarkMode = true
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(shape = RoundedCornerShape(16.dp, 16.dp, 16.dp, 16.dp))
|
||||
.background(SignalTheme.colors.colorSurface1)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(shape = RoundedCornerShape(16.dp, 16.dp, 16.dp, 16.dp))
|
||||
.background(SignalTheme.colors.colorSurface1)
|
||||
.animateContentSize()
|
||||
val boxModifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickable(
|
||||
!state.isExpanded,
|
||||
stringResource(id = R.string.CallOverflowPopupWindow__expand_snackbar_accessibility_label),
|
||||
Role.Button
|
||||
) { setExpanded(true) }
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
modifier = if (state.isExpanded) {
|
||||
boxModifier.fillMaxWidth()
|
||||
} else {
|
||||
boxModifier.wrapContentWidth()
|
||||
}
|
||||
) {
|
||||
val boxModifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickable(
|
||||
!state.isExpanded,
|
||||
stringResource(id = R.string.CallOverflowPopupWindow__expand_snackbar_accessibility_label),
|
||||
Role.Button
|
||||
) { setExpanded(true) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_raise_hand_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
modifier = if (state.isExpanded) {
|
||||
boxModifier.fillMaxWidth()
|
||||
} else {
|
||||
boxModifier.wrapContentWidth()
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_raise_hand_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = getSnackbarText(state),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.weight(1f, fill = state.isExpanded)
|
||||
.wrapContentWidth(Alignment.Start)
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
if (state.isExpanded) {
|
||||
if (state.raisedHands.any { it.sender.isSelf && it.sender.isPrimary }) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
AppDependencies.signalCallManager.raiseHand(false)
|
||||
},
|
||||
modifier = Modifier.wrapContentWidth(Alignment.End)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CallOverflowPopupWindow__lower_hand), maxLines = 1)
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = showCallInfoListener,
|
||||
modifier = Modifier.wrapContentWidth(Alignment.End)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CallOverflowPopupWindow__view), maxLines = 1)
|
||||
}
|
||||
Text(
|
||||
text = getSnackbarText(state),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.weight(1f, fill = state.isExpanded)
|
||||
.wrapContentWidth(Alignment.Start)
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
if (state.isExpanded) {
|
||||
if (state.raisedHands.any { it.sender.isSelf && it.sender.isPrimary }) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
AppDependencies.signalCallManager.raiseHand(false)
|
||||
},
|
||||
modifier = Modifier.wrapContentWidth(Alignment.End)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CallOverflowPopupWindow__lower_hand), maxLines = 1)
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = showCallInfoListener,
|
||||
modifier = Modifier.wrapContentWidth(Alignment.End)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CallOverflowPopupWindow__view), maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+263
-30
@@ -8,26 +8,31 @@ package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.signal.core.ui.compose.NightPreview
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreviewWrapper
|
||||
import org.signal.core.ui.compose.TriggerAlignedPopupState.Companion.popupTrigger
|
||||
import org.signal.core.ui.compose.TriggerAlignedPopupState.Companion.rememberTriggerAlignedPopupState
|
||||
import org.signal.core.ui.compose.spaceBetweenUpTo
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.ToggleButtonOutputState
|
||||
@@ -61,7 +66,8 @@ fun CallControls(
|
||||
modifier = modifier.padding(bottom = bottom)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(20.dp)
|
||||
horizontalArrangement = Arrangement.spaceBetweenUpTo(20.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (callControlsState.displayAudioOutputToggle) {
|
||||
CallAudioToggleButton(
|
||||
@@ -134,39 +140,266 @@ fun CallControls(
|
||||
}
|
||||
}
|
||||
|
||||
@NightPreview
|
||||
/**
|
||||
* The distinct control layouts the call screen can produce, as derived by
|
||||
* [CallControlsState.fromViewModelData] from a given [WebRtcControls]. The rules that separate them:
|
||||
*
|
||||
* - the audio, video, and mic toggles appear in every pre-join and in-call state, except that the audio toggle
|
||||
* drops out once your own camera is on and there's no headset to switch to
|
||||
* - the ring toggle is pre-join only, and only for a group that isn't a call link and that nobody has joined yet
|
||||
* - the overflow button is in-call only, and only once someone else is on the call
|
||||
* - the start call button is pre-join only, and the hangup button replaces it once the call is up
|
||||
*
|
||||
* Note that a connected call link and a connected group produce the same strip -- a call link is a group call with
|
||||
* `isCallLink` set, and that flag only suppresses the ring toggle, which is pre-join only. So the connected case is
|
||||
* covered once, by [GROUP_ONGOING].
|
||||
*/
|
||||
private enum class CallControlsPreviewState(val controls: CallControlsState) {
|
||||
/** 1:1 lobby. No ring toggle (not a group), no hangup yet. */
|
||||
ONE_TO_ONE_PRE_JOIN(
|
||||
CallControlsState(
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.HANDSET,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__start_call
|
||||
)
|
||||
),
|
||||
|
||||
/** The smallest strip: camera on with no headset attached leaves nowhere for the audio toggle to route to. */
|
||||
ONE_TO_ONE_PRE_JOIN_VIDEO(
|
||||
CallControlsState(
|
||||
audioOutput = WebRtcAudioOutput.SPEAKER,
|
||||
displayVideoToggle = true,
|
||||
isVideoEnabled = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__start_call
|
||||
)
|
||||
),
|
||||
|
||||
/** Connected 1:1. Overflow only shows here when screen sharing is enabled remotely. */
|
||||
ONE_TO_ONE_ONGOING(
|
||||
CallControlsState(
|
||||
isBluetoothHeadsetAvailable = true,
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.BLUETOOTH_HEADSET,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
displayAdditionalActions = true,
|
||||
displayEndCallButton = true
|
||||
)
|
||||
),
|
||||
|
||||
/** Group lobby with nobody on the call yet: the only state that offers the ring toggle. */
|
||||
GROUP_PRE_JOIN(
|
||||
CallControlsState(
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.HANDSET,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
displayGroupRingingToggle = true,
|
||||
isGroupRingingEnabled = true,
|
||||
isGroupRingingAllowed = true,
|
||||
isGroupCall = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__start_call
|
||||
)
|
||||
),
|
||||
|
||||
/** Same lobby, but the group is past [RemoteConfig.maxGroupCallRingSize], so the ring toggle is shown disabled. */
|
||||
GROUP_PRE_JOIN_RING_DISALLOWED(
|
||||
CallControlsState(
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.HANDSET,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
displayGroupRingingToggle = true,
|
||||
isGroupCall = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__start_call
|
||||
)
|
||||
),
|
||||
|
||||
/** Group lobby for a call that's already running: the ring toggle is gone and the button reads "Join call". */
|
||||
GROUP_PRE_JOIN_CALL_IN_PROGRESS(
|
||||
CallControlsState(
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.HANDSET,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
isGroupCall = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__join_call
|
||||
)
|
||||
),
|
||||
|
||||
/** Call link lobby. Identical to an empty group lobby except that call links never ring. */
|
||||
CALL_LINK_PRE_JOIN(
|
||||
CallControlsState(
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.HANDSET,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
isGroupCall = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__start_call
|
||||
)
|
||||
),
|
||||
|
||||
/** Connected group or call link. The widest strip. */
|
||||
GROUP_ONGOING(
|
||||
CallControlsState(
|
||||
isWiredHeadsetAvailable = true,
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.WIRED_HEADSET,
|
||||
displayVideoToggle = true,
|
||||
isVideoEnabled = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
isGroupCall = true,
|
||||
displayAdditionalActions = true,
|
||||
displayEndCallButton = true
|
||||
)
|
||||
),
|
||||
|
||||
/** Connected group while sharing your screen, which takes the video toggle away. */
|
||||
GROUP_ONGOING_SCREEN_SHARING(
|
||||
CallControlsState(
|
||||
isEarpieceAvailable = true,
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.SPEAKER,
|
||||
displayVideoToggle = true,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
isGroupCall = true,
|
||||
displayAdditionalActions = true,
|
||||
displayEndCallButton = true,
|
||||
isLocalScreenSharing = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The remembered collaborators [CallControls] needs beyond its state, built to match the given controls so that
|
||||
* the audio picker offers the same devices the strip claims are available.
|
||||
*/
|
||||
@Composable
|
||||
fun CallControlsPreview() {
|
||||
Previews.Preview {
|
||||
CallControls(
|
||||
callControlsState = CallControlsState(
|
||||
displayAudioOutputToggle = true,
|
||||
audioOutput = WebRtcAudioOutput.WIRED_HEADSET,
|
||||
displayMicToggle = true,
|
||||
isMicEnabled = true,
|
||||
displayVideoToggle = true,
|
||||
isVideoEnabled = true,
|
||||
displayGroupRingingToggle = true,
|
||||
isGroupRingingEnabled = true,
|
||||
displayAdditionalActions = true,
|
||||
displayStartCallButton = true,
|
||||
startCallButtonText = R.string.WebRtcCallView__start_call,
|
||||
displayEndCallButton = true
|
||||
),
|
||||
displayVideoTooltip = false,
|
||||
callScreenControlsListener = CallScreenControlsListener.Empty,
|
||||
callScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty,
|
||||
additionalActionsState = AdditionalActionsState(
|
||||
triggerAlignedPopupState = rememberTriggerAlignedPopupState()
|
||||
),
|
||||
audioOutputPickerController = AudioOutputPickerController(
|
||||
outputState = ToggleButtonOutputState(),
|
||||
onSelectedDeviceChanged = {}
|
||||
)
|
||||
private fun rememberPreviewState(controls: CallControlsState): Pair<AdditionalActionsState, AudioOutputPickerController> {
|
||||
val triggerAlignedPopupState = rememberTriggerAlignedPopupState()
|
||||
val outputState = remember(controls) {
|
||||
ToggleButtonOutputState().apply {
|
||||
isEarpieceAvailable = controls.isEarpieceAvailable
|
||||
isWiredHeadsetAvailable = controls.isWiredHeadsetAvailable
|
||||
isBluetoothHeadsetAvailable = controls.isBluetoothHeadsetAvailable
|
||||
setCurrentOutput(controls.audioOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return remember(controls, triggerAlignedPopupState, outputState) {
|
||||
AdditionalActionsState(
|
||||
triggerAlignedPopupState = triggerAlignedPopupState,
|
||||
isGroupCall = controls.isGroupCall,
|
||||
isScreenSharing = controls.isLocalScreenSharing
|
||||
) to AudioOutputPickerController(
|
||||
outputState = outputState,
|
||||
onSelectedDeviceChanged = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallControlsPreview(previewState: CallControlsPreviewState) {
|
||||
val (additionalActionsState, audioOutputPickerController) = rememberPreviewState(previewState.controls)
|
||||
|
||||
CallControls(
|
||||
callControlsState = previewState.controls,
|
||||
displayVideoTooltip = false,
|
||||
callScreenControlsListener = CallScreenControlsListener.Empty,
|
||||
callScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty,
|
||||
additionalActionsState = additionalActionsState,
|
||||
audioOutputPickerController = audioOutputPickerController
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun OneToOnePreJoinCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.ONE_TO_ONE_PRE_JOIN)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun OneToOnePreJoinVideoCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.ONE_TO_ONE_PRE_JOIN_VIDEO)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun OneToOneOngoingCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.ONE_TO_ONE_ONGOING)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun GroupPreJoinCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.GROUP_PRE_JOIN)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun GroupPreJoinRingDisallowedCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.GROUP_PRE_JOIN_RING_DISALLOWED)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun GroupPreJoinCallInProgressCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.GROUP_PRE_JOIN_CALL_IN_PROGRESS)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun CallLinkPreJoinCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.CALL_LINK_PRE_JOIN)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun GroupOngoingCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.GROUP_ONGOING)
|
||||
}
|
||||
|
||||
@PreviewWrapper(SignalPreviewWrapper::class)
|
||||
@NightPreview
|
||||
@Composable
|
||||
private fun GroupOngoingScreenSharingCallControlsPreview() {
|
||||
CallControlsPreview(CallControlsPreviewState.GROUP_ONGOING_SCREEN_SHARING)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callbacks for call controls actions.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
|
||||
/**
|
||||
* Spreads children across the full width like [Arrangement.SpaceBetween], but never opens a gap wider than [max].
|
||||
* Once the gap is capped, the children are centered as a group rather than pinned to the edges, and if there isn't
|
||||
* even room for [max] the gap shrinks to fit instead of overflowing.
|
||||
*/
|
||||
fun Arrangement.spaceBetweenUpTo(max: Dp): Arrangement.Horizontal = object : Arrangement.Horizontal {
|
||||
override val spacing: Dp = max
|
||||
|
||||
override fun Density.arrange(
|
||||
totalSize: Int,
|
||||
sizes: IntArray,
|
||||
layoutDirection: LayoutDirection,
|
||||
outPositions: IntArray
|
||||
) {
|
||||
if (sizes.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val gapCount = sizes.size - 1
|
||||
val slack = totalSize - sizes.sum()
|
||||
val gap = if (gapCount == 0) 0 else (slack / gapCount).coerceIn(0, max.roundToPx())
|
||||
|
||||
var position = ((slack - gap * gapCount) / 2).coerceAtLeast(0)
|
||||
for (index in if (layoutDirection == LayoutDirection.Ltr) sizes.indices else sizes.indices.reversed()) {
|
||||
outPositions[index] = position
|
||||
position += sizes[index] + gap
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user