Add round checkbox composable.

Adds `RoundCheckbox` composable, which is styled to match the appearance of the other view checkboxes used in the app.
This commit is contained in:
Jeffrey Starke
2025-05-05 15:43:39 -04:00
committed by Michelle Tang
parent b79ec79644
commit 9867fa3f50
12 changed files with 142 additions and 14 deletions

View File

@@ -70,10 +70,10 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
import org.thoughtcrime.securesms.components.compose.ConnectivityWarningBottomSheet
import org.thoughtcrime.securesms.components.compose.DeviceSpecificNotificationBottomSheet
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Companion.manageSubscriptions
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment

View File

@@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.components
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components
package org.thoughtcrime.securesms.components.compose
import android.content.DialogInterface
import android.os.Bundle

View File

@@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.components
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
/**
* A custom circular [Checkbox] that can be toggled between checked an unchecked states.
*
* @param checked Indicates whether the checkbox is checked or not.
* @param onCheckedChange A callback function invoked when this checkbox is clicked.
* @param modifier The [Modifier] to be applied to this checkbox.
*/
@Composable
fun RoundCheckbox(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val contentDescription = if (checked) {
stringResource(R.string.SignalCheckbox_accessibility_checked_description)
} else {
stringResource(R.string.SignalCheckbox_accessibility_unchecked_description)
}
Box(
modifier = modifier
.padding(12.dp)
.size(24.dp)
.aspectRatio(1f)
.border(
width = 1.5.dp,
color = if (checked) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.outline
},
shape = CircleShape
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onCheckedChange(!checked) },
onClickLabel = stringResource(R.string.SignalCheckbox_accessibility_on_click_label)
)
.semantics(mergeDescendants = true) {
this.role = Role.Checkbox
this.contentDescription = contentDescription
}
) {
AnimatedVisibility(
visible = checked,
enter = fadeIn(animationSpec = tween(durationMillis = 150)) + scaleIn(initialScale = 1.20f, animationSpec = tween(durationMillis = 500)),
exit = fadeOut(animationSpec = tween(durationMillis = 300)) + scaleOut(targetScale = 0.50f, animationSpec = tween(durationMillis = 600))
) {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle_solid_24),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
@SignalPreview
@Composable
private fun RoundCheckboxCheckedPreview() = SignalTheme {
RoundCheckbox(checked = true, onCheckedChange = {})
}
@SignalPreview
@Composable
private fun RoundCheckboxUncheckedPreview() = SignalTheme {
RoundCheckbox(checked = false, onCheckedChange = {})
}

View File

@@ -28,7 +28,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -83,6 +82,7 @@ import org.signal.core.util.getLength
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.compose.RoundCheckbox
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.DialogState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
import org.thoughtcrime.securesms.compose.ComposeFragment
@@ -629,7 +629,7 @@ fun MediaList(
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if (selectionState.selecting) {
Checkbox(
RoundCheckbox(
checked = selectionState.selected.contains(attachment.id),
onCheckedChange = { selected ->
selectionState = selectionState.copy(selected = if (selected) selectionState.selected + attachment.id else selectionState.selected - attachment.id)

View File

@@ -122,7 +122,6 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.AnimatingToggle
import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar
import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.InputAwareConstraintLayout
import org.thoughtcrime.securesms.components.InputPanel
@@ -130,6 +129,7 @@ import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.SendButton
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel

View File

@@ -98,7 +98,7 @@ import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
import org.thoughtcrime.securesms.banner.banners.UsernameOutOfSyncBanner;
import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SignalProgressDialog;
import org.thoughtcrime.securesms.components.menu.ActionItem;

View File

@@ -36,7 +36,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;

View File

@@ -47,8 +47,8 @@ import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentSaver
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs

View File

@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -37,6 +36,7 @@ import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.nullIfBlank
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.compose.RoundCheckbox
import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressIndicator
import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressState
import org.thoughtcrime.securesms.compose.GlideImage
@@ -158,14 +158,14 @@ fun InstalledStickerPackRow(
color = if (selected) SignalTheme.colors.colorSurface2 else MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(18.dp)
)
.padding(vertical = 10.dp)
.padding(horizontal = 4.dp, vertical = 10.dp)
) {
AnimatedVisibility(
visible = multiSelectEnabled,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) {
Checkbox(
RoundCheckbox(
checked = selected,
onCheckedChange = { onSelectionToggle(pack) },
modifier = Modifier.padding(end = 8.dp)

View File

@@ -8498,5 +8498,12 @@
<!-- Accessibility label for a button displayed in the toolbar to return to the previous screen. -->
<string name="DefaultTopAppBar__navigate_up_content_description">Navigate up</string>
<!-- Accessibility label describing the action that occurs when clicking a checkbox. -->
<string name="SignalCheckbox_accessibility_on_click_label">Toggle</string>
<!-- Accessibility label describing a checked checkbox. -->
<string name="SignalCheckbox_accessibility_checked_description">Ticked</string>
<!-- Accessibility label describing an unchecked checkbox. -->
<string name="SignalCheckbox_accessibility_unchecked_description">Unticked</string>
<!-- EOF -->
</resources>