mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 02:58:45 +00:00
Add support for displaying spoiler text in compose.
This commit is contained in:
committed by
jeffrey-signal
parent
b3b934e009
commit
7c9aa3de72
@@ -28,9 +28,18 @@ object SpoilerPaint {
|
||||
*/
|
||||
var shader: BitmapShader? = null
|
||||
|
||||
private val WIDTH = if (Util.isLowMemory(AppDependencies.application)) 50.dp else 100.dp
|
||||
private val HEIGHT = if (Util.isLowMemory(AppDependencies.application)) 20.dp else 40.dp
|
||||
private val PARTICLES_PER_PIXEL = if (Util.isLowMemory(AppDependencies.application)) 0.001f else 0.002f
|
||||
private val WIDTH = if (isLowMemory()) 50.dp else 100.dp
|
||||
private val HEIGHT = if (isLowMemory()) 20.dp else 40.dp
|
||||
private val PARTICLES_PER_PIXEL = if (isLowMemory()) 0.001f else 0.002f
|
||||
|
||||
private fun isLowMemory(): Boolean {
|
||||
return try {
|
||||
Util.isLowMemory(AppDependencies.application)
|
||||
} catch (e: Throwable) {
|
||||
// In preview mode or when AppDependencies is not initialized, default to low memory mode
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private var shaderBitmap: Bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ALPHA_8)
|
||||
private var bufferBitmap: Bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ALPHA_8)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.thoughtcrime.securesms.components.spoiler.compose
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerPaint
|
||||
|
||||
/**
|
||||
* Annotation tag used to mark spoiler text ranges in an AnnotatedString.
|
||||
*/
|
||||
const val SPOILER_ANNOTATION_TAG = "spoiler"
|
||||
|
||||
/**
|
||||
* Modifier that draws the spoiler effect over specific text regions.
|
||||
*
|
||||
* @param spoilerState State holder for revealed spoilers
|
||||
* @param annotatedString The text with spoiler annotations
|
||||
* @param textLayoutResult The result of text layout measurement
|
||||
* @param textColor Color to tint the particles
|
||||
*/
|
||||
fun Modifier.drawSpoilers(
|
||||
spoilerState: SpoilerState,
|
||||
annotatedString: AnnotatedString,
|
||||
textLayoutResult: TextLayoutResult?,
|
||||
textColor: Color
|
||||
): Modifier = this.then(
|
||||
Modifier.drawWithCache {
|
||||
val paint = Paint()
|
||||
val colorFilter = android.graphics.PorterDuffColorFilter(
|
||||
textColor.toArgb(),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
|
||||
val layout = textLayoutResult ?: return@onDrawWithContent
|
||||
|
||||
// Get all spoiler annotations
|
||||
val spoilerAnnotations = annotatedString.getStringAnnotations(
|
||||
tag = SPOILER_ANNOTATION_TAG,
|
||||
start = 0,
|
||||
end = annotatedString.length
|
||||
)
|
||||
|
||||
if (spoilerAnnotations.isEmpty()) {
|
||||
return@onDrawWithContent
|
||||
}
|
||||
|
||||
val shader = SpoilerPaint.shader
|
||||
|
||||
drawIntoCanvas { canvas ->
|
||||
val nativeCanvas = canvas.nativeCanvas
|
||||
|
||||
// Update paint properties for this draw
|
||||
if (shader != null) {
|
||||
paint.shader = shader
|
||||
paint.colorFilter = colorFilter
|
||||
} else {
|
||||
paint.shader = null
|
||||
paint.color = android.graphics.Color.TRANSPARENT
|
||||
}
|
||||
|
||||
for (annotation in spoilerAnnotations) {
|
||||
if (spoilerState.isRevealed(annotation.item)) {
|
||||
continue
|
||||
}
|
||||
|
||||
val start = annotation.start
|
||||
val end = annotation.end
|
||||
|
||||
if (start >= end) {
|
||||
continue
|
||||
}
|
||||
|
||||
val startLine = layout.getLineForOffset(start)
|
||||
val endLine = layout.getLineForOffset(end)
|
||||
|
||||
if (startLine == endLine) {
|
||||
val left = layout.getHorizontalPosition(start, true)
|
||||
val right = layout.getHorizontalPosition(end, true)
|
||||
val top = layout.getLineTop(startLine)
|
||||
val bottom = layout.getLineBottom(startLine)
|
||||
|
||||
nativeCanvas.drawRect(
|
||||
left.coerceAtMost(right),
|
||||
top,
|
||||
left.coerceAtLeast(right),
|
||||
bottom,
|
||||
paint
|
||||
)
|
||||
} else {
|
||||
val firstLineLeft = layout.getHorizontalPosition(start, true)
|
||||
val firstLineRight = if (layout.getParagraphDirection(startLine) == androidx.compose.ui.text.style.ResolvedTextDirection.Rtl) {
|
||||
layout.getLineLeft(startLine)
|
||||
} else {
|
||||
layout.getLineRight(startLine)
|
||||
}
|
||||
nativeCanvas.drawRect(
|
||||
firstLineLeft.coerceAtMost(firstLineRight),
|
||||
layout.getLineTop(startLine),
|
||||
firstLineLeft.coerceAtLeast(firstLineRight),
|
||||
layout.getLineBottom(startLine),
|
||||
paint
|
||||
)
|
||||
|
||||
for (line in startLine + 1 until endLine) {
|
||||
nativeCanvas.drawRect(
|
||||
layout.getLineLeft(line),
|
||||
layout.getLineTop(line),
|
||||
layout.getLineRight(line),
|
||||
layout.getLineBottom(line),
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
val lastLineLeft = if (layout.getParagraphDirection(endLine) == androidx.compose.ui.text.style.ResolvedTextDirection.Rtl) {
|
||||
layout.getLineRight(endLine)
|
||||
} else {
|
||||
layout.getLineLeft(endLine)
|
||||
}
|
||||
val lastLineRight = layout.getHorizontalPosition(end, true)
|
||||
nativeCanvas.drawRect(
|
||||
lastLineLeft.coerceAtMost(lastLineRight),
|
||||
layout.getLineTop(endLine),
|
||||
lastLineLeft.coerceAtLeast(lastLineRight),
|
||||
layout.getLineBottom(endLine),
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Find which spoiler annotation (if any) contains the given offset in the text.
|
||||
*/
|
||||
fun AnnotatedString.findSpoilerAt(offset: Int): AnnotatedString.Range<String>? {
|
||||
return getStringAnnotations(SPOILER_ANNOTATION_TAG, offset, offset).firstOrNull()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.components.spoiler.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
/**
|
||||
* State holder for managing which spoilers have been revealed in Compose.
|
||||
* Similar to the View-based SpoilerAnnotation, but uses Compose state management.
|
||||
*/
|
||||
@Stable
|
||||
class SpoilerState {
|
||||
private var revealedSpoilers by mutableStateOf(setOf<String>())
|
||||
|
||||
/**
|
||||
* Check if a spoiler with the given ID has been revealed.
|
||||
*/
|
||||
fun isRevealed(spoilerId: String): Boolean {
|
||||
return spoilerId in revealedSpoilers
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal a spoiler with the given ID.
|
||||
*/
|
||||
fun reveal(spoilerId: String) {
|
||||
revealedSpoilers = revealedSpoilers + spoilerId
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all revealed spoilers.
|
||||
*/
|
||||
fun reset() {
|
||||
revealedSpoilers = emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember a [SpoilerState] instance.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSpoilerState(): SpoilerState {
|
||||
return remember { SpoilerState() }
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.thoughtcrime.securesms.components.spoiler.compose
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerPaint
|
||||
|
||||
/**
|
||||
* A Text composable that supports spoiler annotations with particle effects.
|
||||
*
|
||||
* Text ranges marked with [SPOILER_ANNOTATION_TAG] annotations will be rendered with
|
||||
* an animated particle effect until revealed by tapping.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* val spoilerState = rememberSpoilerState()
|
||||
* val text = buildAnnotatedString {
|
||||
* append("This is normal text. ")
|
||||
* withAnnotation(SPOILER_ANNOTATION_TAG, "spoiler-1") {
|
||||
* append("This is a spoiler!")
|
||||
* }
|
||||
* append(" More normal text.")
|
||||
* }
|
||||
* SpoilerText(
|
||||
* text = text,
|
||||
* spoilerState = spoilerState
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @param text The annotated string with optional spoiler annotations
|
||||
* @param spoilerState State holder for managing revealed spoilers
|
||||
* @param modifier Modifier to be applied to the Text
|
||||
* @param color Color to apply to the text
|
||||
* @param fontSize Font size to apply to the text
|
||||
* @param textAlign Text alignment
|
||||
* @param textDecoration Text decoration
|
||||
* @param overflow How to handle text overflow
|
||||
* @param softWrap Whether the text should break at soft line breaks
|
||||
* @param maxLines Maximum number of lines
|
||||
* @param minLines Minimum number of lines
|
||||
* @param onTextLayout Callback for text layout results
|
||||
* @param style Text style to apply
|
||||
* @param inlineContent Map of inline content for the text
|
||||
*/
|
||||
@Composable
|
||||
fun SpoilerText(
|
||||
text: AnnotatedString,
|
||||
spoilerState: SpoilerState,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
textAlign: TextAlign? = null,
|
||||
textDecoration: TextDecoration? = null,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
inlineContent: Map<String, InlineTextContent> = mapOf()
|
||||
) {
|
||||
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
var frameTime by remember { mutableLongStateOf(0L) }
|
||||
|
||||
// Get all spoiler annotations
|
||||
val spoilerAnnotations = remember(text) {
|
||||
text.getStringAnnotations(SPOILER_ANNOTATION_TAG, 0, text.length)
|
||||
}
|
||||
|
||||
// Check if there are any unrevealed spoilers
|
||||
val hasUnrevealedSpoilers = remember(spoilerAnnotations, spoilerState, frameTime) {
|
||||
spoilerAnnotations.any { !spoilerState.isRevealed(it.item) }
|
||||
}
|
||||
|
||||
// Animate when there are unrevealed spoilers
|
||||
LaunchedEffect(hasUnrevealedSpoilers) {
|
||||
if (hasUnrevealedSpoilers) {
|
||||
while (true) {
|
||||
withFrameNanos { nanos ->
|
||||
SpoilerPaint.update()
|
||||
frameTime = nanos
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val originalTextColor = if (color != Color.Unspecified) {
|
||||
color
|
||||
} else if (style.color != Color.Unspecified) {
|
||||
style.color
|
||||
} else {
|
||||
LocalContentColor.current
|
||||
}
|
||||
|
||||
val displayText = remember(text, spoilerAnnotations, spoilerState, frameTime) {
|
||||
AnnotatedString.Builder(text).apply {
|
||||
for (annotation in spoilerAnnotations) {
|
||||
if (!spoilerState.isRevealed(annotation.item)) {
|
||||
addStyle(
|
||||
style = androidx.compose.ui.text.SpanStyle(color = Color.Transparent),
|
||||
start = annotation.start,
|
||||
end = annotation.end
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toAnnotatedString()
|
||||
}
|
||||
|
||||
Text(
|
||||
text = displayText,
|
||||
modifier = modifier
|
||||
.drawSpoilers(
|
||||
spoilerState = spoilerState,
|
||||
annotatedString = text,
|
||||
textLayoutResult = textLayoutResult,
|
||||
textColor = originalTextColor
|
||||
)
|
||||
.pointerInput(text, spoilerState) {
|
||||
detectTapGestures { offset ->
|
||||
val layout = textLayoutResult ?: return@detectTapGestures
|
||||
val position = layout.getOffsetForPosition(offset)
|
||||
val spoiler = text.findSpoilerAt(position)
|
||||
if (spoiler != null && !spoilerState.isRevealed(spoiler.item)) {
|
||||
spoilerState.reveal(spoiler.item)
|
||||
}
|
||||
}
|
||||
},
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
textAlign = textAlign,
|
||||
textDecoration = textDecoration,
|
||||
overflow = overflow,
|
||||
softWrap = softWrap,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onTextLayout = { result ->
|
||||
textLayoutResult = result
|
||||
onTextLayout(result)
|
||||
},
|
||||
style = style,
|
||||
inlineContent = inlineContent
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Text composable that supports spoiler annotations, using a simple string with builder.
|
||||
*
|
||||
* @param text The plain text to display
|
||||
* @param spoilerState State holder for managing revealed spoilers
|
||||
* @param modifier Modifier to be applied to the Text
|
||||
* @param color Color to apply to the text
|
||||
* @param fontSize Font size to apply to the text
|
||||
* @param textAlign Text alignment
|
||||
* @param textDecoration Text decoration
|
||||
* @param overflow How to handle text overflow
|
||||
* @param softWrap Whether the text should break at soft line breaks
|
||||
* @param maxLines Maximum number of lines
|
||||
* @param minLines Minimum number of lines
|
||||
* @param onTextLayout Callback for text layout results
|
||||
* @param style Text style to apply
|
||||
*/
|
||||
@Composable
|
||||
fun SpoilerText(
|
||||
text: String,
|
||||
spoilerState: SpoilerState,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
textAlign: TextAlign? = null,
|
||||
textDecoration: TextDecoration? = null,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
SpoilerText(
|
||||
text = AnnotatedString(text),
|
||||
spoilerState = spoilerState,
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
textAlign = textAlign,
|
||||
textDecoration = textDecoration,
|
||||
overflow = overflow,
|
||||
softWrap = softWrap,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onTextLayout = onTextLayout,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package org.thoughtcrime.securesms.components.spoiler.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun SingleSpoilerPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
val text = buildAnnotatedString {
|
||||
append("This is some normal text and ")
|
||||
val start = length
|
||||
append("this part is hidden")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "spoiler-1", start, length)
|
||||
append(" until you tap it!")
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MultipleSpoilersPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
val text = buildAnnotatedString {
|
||||
append("The answer to question 1 is ")
|
||||
val start1 = length
|
||||
append("42")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "answer-1", start1, length)
|
||||
append(" and the answer to question 2 is ")
|
||||
val start2 = length
|
||||
append("the meaning of life")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "answer-2", start2, length)
|
||||
append(".")
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MultilineSpoilerPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
val text = buildAnnotatedString {
|
||||
append("Here's a really long spoiler: ")
|
||||
val start = length
|
||||
append("This is a very long piece of text that will definitely span multiple lines when displayed in a narrow container. It contains important plot details that you might not want to see until you're ready!")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "long-spoiler", start, length)
|
||||
append(" Tap to reveal.")
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun StyledSpoilersPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
val text = buildAnnotatedString {
|
||||
append("Normal text, ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("bold text, ")
|
||||
}
|
||||
val start1 = length
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp)) {
|
||||
append("bold spoiler")
|
||||
}
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "styled-spoiler-1", start1, length)
|
||||
append(", and ")
|
||||
val start2 = length
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Light, fontSize = 14.sp)) {
|
||||
append("light spoiler")
|
||||
}
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "styled-spoiler-2", start2, length)
|
||||
append(".")
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ColoredSpoilersPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
val text = buildAnnotatedString {
|
||||
append("This text has ")
|
||||
val start1 = length
|
||||
withStyle(SpanStyle(color = Color.Red)) {
|
||||
append("red spoiler")
|
||||
}
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "red-spoiler", start1, length)
|
||||
append(" and ")
|
||||
val start2 = length
|
||||
withStyle(SpanStyle(color = Color.Blue)) {
|
||||
append("blue spoiler")
|
||||
}
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "blue-spoiler", start2, length)
|
||||
append(".")
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun SharedStatePreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
|
||||
val text1 = buildAnnotatedString {
|
||||
append("First message with ")
|
||||
val start = length
|
||||
append("shared spoiler")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "shared-spoiler", start, length)
|
||||
}
|
||||
|
||||
val text2 = buildAnnotatedString {
|
||||
append("Second message with the same ")
|
||||
val start = length
|
||||
append("shared spoiler")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "shared-spoiler", start, length)
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text1,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
SpoilerText(
|
||||
text = text2,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun EntireMessageSpoilerPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
val text = buildAnnotatedString {
|
||||
val start = length
|
||||
append("This entire message is a spoiler! You need to tap anywhere on it to reveal the content.")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "entire-message", start, length)
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
SpoilerText(
|
||||
text = text,
|
||||
spoilerState = spoilerState,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun DifferentSizesPreview() {
|
||||
val spoilerState = rememberSpoilerState()
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SpoilerText(
|
||||
text = buildAnnotatedString {
|
||||
append("Small text with ")
|
||||
val start = length
|
||||
append("small spoiler")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "small", start, length)
|
||||
},
|
||||
spoilerState = spoilerState,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
|
||||
SpoilerText(
|
||||
text = buildAnnotatedString {
|
||||
append("Medium text with ")
|
||||
val start = length
|
||||
append("medium spoiler")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "medium", start, length)
|
||||
},
|
||||
spoilerState = spoilerState,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
|
||||
SpoilerText(
|
||||
text = buildAnnotatedString {
|
||||
append("Large text with ")
|
||||
val start = length
|
||||
append("large spoiler")
|
||||
addStringAnnotation(SPOILER_ANNOTATION_TAG, "large", start, length)
|
||||
},
|
||||
spoilerState = spoilerState,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user