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
|
var shader: BitmapShader? = null
|
||||||
|
|
||||||
private val WIDTH = if (Util.isLowMemory(AppDependencies.application)) 50.dp else 100.dp
|
private val WIDTH = if (isLowMemory()) 50.dp else 100.dp
|
||||||
private val HEIGHT = if (Util.isLowMemory(AppDependencies.application)) 20.dp else 40.dp
|
private val HEIGHT = if (isLowMemory()) 20.dp else 40.dp
|
||||||
private val PARTICLES_PER_PIXEL = if (Util.isLowMemory(AppDependencies.application)) 0.001f else 0.002f
|
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 shaderBitmap: Bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ALPHA_8)
|
||||||
private var bufferBitmap: 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