Convert AddAllowedMembersFragment to compose.

This commit is contained in:
Alex Hart
2025-08-19 13:17:30 -03:00
committed by Jeffrey Starke
parent 958dde0f6e
commit ecddf34083
9 changed files with 542 additions and 178 deletions

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.ui.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
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.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
/**
* Utilizes a circular reveal animation to display and hide the content given.
* When the content is hidden via settings [isLoading] to true, we display a
* circular progress indicator.
*
* This component will automatically size itself according to the content passed
* in via [content]
*/
@Composable
fun CircularProgressWrapper(
isLoading: Boolean,
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
) {
var size by remember { mutableStateOf(IntSize.Zero) }
val dpSize = with(LocalDensity.current) {
DpSize(size.width.toDp(), size.height.toDp())
}
AnimatedVisibility(
visible = isLoading,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(dpSize)
) {
CircularProgressIndicator()
}
}
AnimatedVisibility(
visible = !isLoading,
enter = EnterTransition.None,
exit = ExitTransition.None
) {
val visibility = transition.animateFloat(
transitionSpec = { tween(durationMillis = 400, easing = LinearOutSlowInEasing) },
label = "CircularProgressWrapper-Visibility"
) { state ->
if (state == EnterExitState.Visible) 1f else 0f
}
Box(
modifier = Modifier
.onSizeChanged { s ->
size = s
}
.circularReveal(visibility)
) {
content()
}
}
}
}
@SignalPreview
@Composable
fun CircularProgressWrapperPreview() {
var isLoading by remember {
mutableStateOf(false)
}
LaunchedEffect(isLoading) {
if (isLoading) {
delay(3.seconds)
isLoading = false
}
}
Previews.Preview {
CircularProgressWrapper(
isLoading = isLoading,
content = {
Buttons.LargeTonal(onClick = {
isLoading = true
}) {
Text(text = "Next")
}
}
)
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.ui.compose
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import kotlin.math.sqrt
/**
* Circle Reveal Modifiers found here:
* https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
*
* A modifier that clips the composable content using a circular shape. The radius of the circle
* will be determined by the [transitionProgress].
*
* The values of the progress should be between 0 and 1.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
*/
fun Modifier.circularReveal(
transitionProgress: State<Float>,
revealFrom: Offset = Offset(0.5f, 0.5f)
): Modifier {
return drawWithCache {
val path = Path()
val center = revealFrom.mapTo(size)
val radius = calculateRadius(revealFrom, size)
path.addOval(Rect(center, radius * transitionProgress.value))
onDrawWithContent {
clipPath(path) { this@onDrawWithContent.drawContent() }
}
}
}
private fun Offset.mapTo(size: Size): Offset {
return Offset(x * size.width, y * size.height)
}
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
val x = (if (x > 0.5f) x else 1 - x) * size.width
val y = (if (y > 0.5f) y else 1 - y) * size.height
sqrt(x * x + y * y)
}

View File

@@ -156,6 +156,7 @@ object Rows {
onCheckChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
label: String? = null,
icon: ImageVector? = null,
textColor: Color = MaterialTheme.colorScheme.onSurface,
isLoading: Boolean = false
) {
@@ -168,6 +169,15 @@ object Rows {
.padding(defaultPadding()),
verticalAlignment = CenterVertically
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null
)
Spacer(modifier = Modifier.width(24.dp))
}
TextAndLabel(
text = text,
label = label,