diff --git a/readmore-foundation/api/current.api b/readmore-foundation/api/current.api index 3cb41fc..312a7ac 100644 --- a/readmore-foundation/api/current.api +++ b/readmore-foundation/api/current.api @@ -2,8 +2,8 @@ package com.webtoonscorp.android.readmore.foundation { public final class BasicReadMoreTextKt { + method @androidx.compose.runtime.Composable public static void BasicReadMoreText(androidx.compose.ui.text.AnnotatedString text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1 onTextLayout, optional boolean softWrap, optional java.util.Map inlineContent, optional String readMoreText, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); method @androidx.compose.runtime.Composable public static void BasicReadMoreText(String text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1 onTextLayout, optional boolean softWrap, optional String readMoreText, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); - method @androidx.compose.runtime.Composable public static void BasicReadMoreText(androidx.compose.ui.text.AnnotatedString text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1 onTextLayout, optional boolean softWrap, optional String readMoreText, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); } @kotlin.RequiresOptIn(message="This API is experimental and is likely to change or to be removed in the future.") @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalReadMoreApi { diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png new file mode 100644 index 0000000..d5e6fcb Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png new file mode 100644 index 0000000..f58c6a9 Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.default_none.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.default_none.png new file mode 100644 index 0000000..a601595 Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.default_none.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png new file mode 100644 index 0000000..73f8b7b Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png new file mode 100644 index 0000000..a601595 Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png new file mode 100644 index 0000000..f58c6a9 Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png new file mode 100644 index 0000000..d5e6fcb Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png differ diff --git a/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png new file mode 100644 index 0000000..73f8b7b Binary files /dev/null and b/readmore-foundation/screenshots/BasicReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png differ diff --git a/readmore-foundation/src/main/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreText.kt b/readmore-foundation/src/main/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreText.kt index 598ec4f..e5c754d 100644 --- a/readmore-foundation/src/main/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreText.kt +++ b/readmore-foundation/src/main/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreText.kt @@ -15,12 +15,14 @@ */ package com.webtoonscorp.android.readmore.foundation +import android.annotation.SuppressLint import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.InlineTextContent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -31,6 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextMeasurer @@ -132,6 +135,8 @@ public fun BasicReadMoreText( * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, * [readMoreOverflow] and TextAlign may have unexpected effects. + * @param inlineContent A map store composables that replaces certain ranges of the text. It's used + * to insert composables into text layout. Check [InlineTextContent] for more information. * @param readMoreText The read more text to be displayed in the collapsed state. * @param readMoreMaxLines An optional maximum number of lines for the text to span, wrapping if * necessary. If the text exceeds the given number of lines, it will be truncated according to @@ -154,6 +159,7 @@ public fun BasicReadMoreText( style: TextStyle = TextStyle.Default, onTextLayout: (TextLayoutResult) -> Unit = {}, softWrap: Boolean = true, + inlineContent: Map = mapOf(), readMoreText: String = "", readMoreMaxLines: Int = 2, readMoreOverflow: ReadMoreTextOverflow = ReadMoreTextOverflow.Ellipsis, @@ -171,6 +177,7 @@ public fun BasicReadMoreText( style = style, onTextLayout = onTextLayout, softWrap = softWrap, + inlineContent = inlineContent, readMoreText = readMoreText, readMoreMaxLines = readMoreMaxLines, readMoreOverflow = readMoreOverflow, @@ -188,6 +195,7 @@ public fun BasicReadMoreText( private const val ReadMoreTag = "read_more" private const val ReadLessTag = "read_less" +@SuppressLint("UnusedBoxWithConstraintsScope") @Composable private fun CoreReadMoreText( text: AnnotatedString, @@ -198,6 +206,7 @@ private fun CoreReadMoreText( style: TextStyle = TextStyle.Default, onTextLayout: (TextLayoutResult) -> Unit = {}, softWrap: Boolean = true, + inlineContent: Map = mapOf(), readMoreText: String = "", readMoreMaxLines: Int = 2, readMoreOverflow: ReadMoreTextOverflow = ReadMoreTextOverflow.Ellipsis, @@ -304,6 +313,7 @@ private fun CoreReadMoreText( overflow = TextOverflow.Ellipsis, softWrap = softWrap, maxLines = if (expanded) Int.MAX_VALUE else readMoreMaxLines, + inlineContent = inlineContent, ) val constraints = Constraints(maxWidth = constraints.maxWidth) @@ -317,6 +327,7 @@ private fun CoreReadMoreText( text, readMoreMaxLines, softWrap, + inlineContent, ) { state.applyCollapsedText( textMeasurer = textMeasurer, @@ -328,6 +339,7 @@ private fun CoreReadMoreText( text = text, readMoreMaxLines = readMoreMaxLines, softWrap = softWrap, + inlineContent = inlineContent, ) } } @@ -346,7 +358,7 @@ private class ReadMoreState { var collapsedText: AnnotatedString get() = _collapsedText - internal set(value) { + private set(value) { if (value != _collapsedText) { _collapsedText = value if (DebugLog) { @@ -368,6 +380,7 @@ private class ReadMoreState { text: AnnotatedString, readMoreMaxLines: Int, softWrap: Boolean, + inlineContent: Map, ) { val overflowTextWidth = if (overflowText.isNotEmpty()) { textMeasurer.measure( @@ -392,6 +405,7 @@ private class ReadMoreState { overflow = TextOverflow.Clip, softWrap = softWrap, constraints = constraints, + placeholders = extractPlaceholders(text, inlineContent), ) val clipTextCount = textLayout.getLineEnd(lineIndex = textLayout.lineCount - 1) @@ -410,6 +424,7 @@ private class ReadMoreState { text = subText, style = style, softWrap = softWrap, + placeholders = extractPlaceholders(subText, inlineContent), ).size.width }, ) @@ -452,6 +467,40 @@ private class ReadMoreState { return replacedCount } + /** + * Converts AnnotatedString and inlineContent map to placeholders for use with TextMeasurer. + * + * @param text The annotated string containing inline content annotations + * @param inlineContent Map of inline content IDs to their corresponding InlineTextContent + * @return List of Range objects representing the inline content positions + */ + private fun extractPlaceholders( + text: AnnotatedString, + inlineContent: Map, + ): List> { + if (inlineContent.isEmpty()) { + return emptyList() + } + + // Get all string annotations with the "androidx.compose.foundation.text.inlineContent" tag + val inlineContentAnnotations = text.getStringAnnotations( + tag = "androidx.compose.foundation.text.inlineContent", + start = 0, + end = text.length, + ) + + // Map each annotation to a Range if it exists in the inlineContent map + return inlineContentAnnotations.mapNotNull { annotation -> + inlineContent[annotation.item]?.let { content -> + AnnotatedString.Range( + item = content.placeholder, + start = annotation.start, + end = annotation.end, + ) + } + } + } + override fun toString(): String { return "ReadMoreState(" + "collapsedText=$collapsedText" + diff --git a/readmore-foundation/src/test/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreTextScreenshotTest.kt b/readmore-foundation/src/test/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreTextScreenshotTest.kt index 99e0552..4be4184 100644 --- a/readmore-foundation/src/test/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreTextScreenshotTest.kt +++ b/readmore-foundation/src/test/java/com/webtoonscorp/android/readmore/foundation/BasicReadMoreTextScreenshotTest.kt @@ -15,14 +15,27 @@ */ package com.webtoonscorp.android.readmore.foundation +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString @@ -31,6 +44,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.captureRoboImage @@ -117,6 +131,102 @@ internal class BasicReadMoreTextScreenshotTest( readMoreText = "Read more", readLessText = "Read less", ), + AnnotatedStringScreenshotTestCase( + name = "InlineTextContent", + text = buildAnnotatedString { + appendInlineContent("start") + append("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + appendInlineContent("middle") + append("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + appendInlineContent("end") + }, + inlineContent = mapOf( + "start" to InlineTextContent( + Placeholder( + width = 55.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + val shape = RoundedCornerShape(4.dp) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(end = 5.dp) + .fillMaxSize() + .background(color = Color.Red, shape = shape) + .clip(shape = shape), + ) { + BasicText( + text = "START", + style = TextStyle.Default.copy( + color = Color.White, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ), + ) + } + }, + "middle" to InlineTextContent( + Placeholder( + width = 70.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + val shape = RoundedCornerShape(4.dp) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(horizontal = 5.dp) + .fillMaxSize() + .background(color = Color.Blue, shape = shape) + .clip(shape = shape), + ) { + BasicText( + text = "MIDDLE", + style = TextStyle.Default.copy( + color = Color.White, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ), + ) + } + }, + "end" to InlineTextContent( + Placeholder( + width = 35.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + val shape = RoundedCornerShape(4.dp) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(start = 5.dp) + .fillMaxSize() + .background(color = Color.Green, shape = shape) + .clip(shape = shape), + ) { + BasicText( + text = "END", + style = TextStyle.Default.copy( + color = Color.White, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ), + ) + } + }, + ), + maxLines = 4, + readMoreText = "Read more", + readLessText = "Read less", + ), ) } } @@ -292,12 +402,14 @@ internal class BasicReadMoreTextScreenshotTest( is AnnotatedStringScreenshotTestCase -> { BasicReadMoreText( text = testCase.text, + inlineContent = testCase.inlineContent, expanded = expanded, style = TextStyle.Default.copy( fontSize = 15.sp, fontStyle = FontStyle.Normal, lineHeight = 22.sp, ), + readMoreMaxLines = testCase.maxLines, readMoreText = readMoreText, readMoreStyle = SpanStyle( color = Color.Blue, @@ -340,6 +452,8 @@ internal data class StringScreenshotTestCase( internal data class AnnotatedStringScreenshotTestCase( override val name: String, val text: AnnotatedString, + val inlineContent: Map = mapOf(), + val maxLines: Int = 2, override val readMoreText: String, override val readLessText: String, override val isRtl: Boolean = false, diff --git a/readmore-material/api/current.api b/readmore-material/api/current.api index c612f53..1974173 100644 --- a/readmore-material/api/current.api +++ b/readmore-material/api/current.api @@ -2,8 +2,8 @@ package com.webtoonscorp.android.readmore.material { public final class ReadMoreTextKt { + method @androidx.compose.runtime.Composable public static void ReadMoreText(androidx.compose.ui.text.AnnotatedString text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional boolean softWrap, optional kotlin.jvm.functions.Function1 onTextLayout, optional java.util.Map inlineContent, optional androidx.compose.ui.text.TextStyle style, optional String readMoreText, optional long readMoreColor, optional long readMoreFontSize, optional androidx.compose.ui.text.font.FontStyle? readMoreFontStyle, optional androidx.compose.ui.text.font.FontWeight? readMoreFontWeight, optional androidx.compose.ui.text.font.FontFamily? readMoreFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readMoreTextDecoration, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional long readLessColor, optional long readLessFontSize, optional androidx.compose.ui.text.font.FontStyle? readLessFontStyle, optional androidx.compose.ui.text.font.FontWeight? readLessFontWeight, optional androidx.compose.ui.text.font.FontFamily? readLessFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readLessTextDecoration, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); method @androidx.compose.runtime.Composable public static void ReadMoreText(String text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional boolean softWrap, optional kotlin.jvm.functions.Function1 onTextLayout, optional androidx.compose.ui.text.TextStyle style, optional String readMoreText, optional long readMoreColor, optional long readMoreFontSize, optional androidx.compose.ui.text.font.FontStyle? readMoreFontStyle, optional androidx.compose.ui.text.font.FontWeight? readMoreFontWeight, optional androidx.compose.ui.text.font.FontFamily? readMoreFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readMoreTextDecoration, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional long readLessColor, optional long readLessFontSize, optional androidx.compose.ui.text.font.FontStyle? readLessFontStyle, optional androidx.compose.ui.text.font.FontWeight? readLessFontWeight, optional androidx.compose.ui.text.font.FontFamily? readLessFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readLessTextDecoration, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); - method @androidx.compose.runtime.Composable public static void ReadMoreText(androidx.compose.ui.text.AnnotatedString text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional boolean softWrap, optional kotlin.jvm.functions.Function1 onTextLayout, optional androidx.compose.ui.text.TextStyle style, optional String readMoreText, optional long readMoreColor, optional long readMoreFontSize, optional androidx.compose.ui.text.font.FontStyle? readMoreFontStyle, optional androidx.compose.ui.text.font.FontWeight? readMoreFontWeight, optional androidx.compose.ui.text.font.FontFamily? readMoreFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readMoreTextDecoration, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional long readLessColor, optional long readLessFontSize, optional androidx.compose.ui.text.font.FontStyle? readLessFontStyle, optional androidx.compose.ui.text.font.FontWeight? readLessFontWeight, optional androidx.compose.ui.text.font.FontFamily? readLessFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readLessTextDecoration, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); } } diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png new file mode 100644 index 0000000..d30fca5 Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png new file mode 100644 index 0000000..e6cad8f Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.default_none.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.default_none.png new file mode 100644 index 0000000..7a452a9 Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.default_none.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png new file mode 100644 index 0000000..1b674e1 Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png new file mode 100644 index 0000000..7a452a9 Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png new file mode 100644 index 0000000..e6cad8f Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png new file mode 100644 index 0000000..d30fca5 Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png differ diff --git a/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png new file mode 100644 index 0000000..1b674e1 Binary files /dev/null and b/readmore-material/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png differ diff --git a/readmore-material/src/main/java/com/webtoonscorp/android/readmore/material/ReadMoreText.kt b/readmore-material/src/main/java/com/webtoonscorp/android/readmore/material/ReadMoreText.kt index be4608d..056d03b 100644 --- a/readmore-material/src/main/java/com/webtoonscorp/android/readmore/material/ReadMoreText.kt +++ b/readmore-material/src/main/java/com/webtoonscorp/android/readmore/material/ReadMoreText.kt @@ -16,6 +16,7 @@ package com.webtoonscorp.android.readmore.material import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalTextStyle @@ -276,6 +277,8 @@ public fun ReadMoreText( * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw selection around the text. + * @param inlineContent A map store composables that replaces certain ranges of the text. It's used + * to insert composables into text layout. Check [InlineTextContent] for more information. * @param style Style configuration for the text such as color, font, line height etc. * @param readMoreText The read more text to be displayed in the collapsed state. * @param readMoreColor [Color] to apply to the read more text. If [Color.Unspecified], and [style] @@ -331,6 +334,7 @@ public fun ReadMoreText( lineHeight: TextUnit = TextUnit.Unspecified, softWrap: Boolean = true, onTextLayout: (TextLayoutResult) -> Unit = {}, + inlineContent: Map = mapOf(), style: TextStyle = LocalTextStyle.current, readMoreText: String = "", readMoreColor: Color = Color.Unspecified, @@ -405,6 +409,7 @@ public fun ReadMoreText( style = mergedStyle, onTextLayout = onTextLayout, softWrap = softWrap, + inlineContent = inlineContent, readMoreText = readMoreText, readMoreMaxLines = readMoreMaxLines, readMoreOverflow = readMoreOverflow, diff --git a/readmore-material/src/test/java/com/webtoonscorp/android/readmore/material/ReadMoreTextScreenshotTest.kt b/readmore-material/src/test/java/com/webtoonscorp/android/readmore/material/ReadMoreTextScreenshotTest.kt index 1629139..d8f189f 100644 --- a/readmore-material/src/test/java/com/webtoonscorp/android/readmore/material/ReadMoreTextScreenshotTest.kt +++ b/readmore-material/src/test/java/com/webtoonscorp/android/readmore/material/ReadMoreTextScreenshotTest.kt @@ -15,14 +15,26 @@ */ package com.webtoonscorp.android.readmore.material +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle @@ -30,6 +42,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.captureRoboImage @@ -116,6 +129,105 @@ internal class ReadMoreTextScreenshotTest( readMoreText = "Read more", readLessText = "Read less", ), + AnnotatedStringScreenshotTestCase( + name = "InlineTextContent", + text = buildAnnotatedString { + appendInlineContent("start") + append("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + appendInlineContent("middle") + append("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + appendInlineContent("end") + }, + inlineContent = mapOf( + "start" to InlineTextContent( + Placeholder( + width = 55.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.Red, + contentColor = Color.White, + modifier = Modifier + .padding(end = 5.dp) + .fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = "START", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } + }, + "middle" to InlineTextContent( + Placeholder( + width = 70.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.Blue, + contentColor = Color.White, + modifier = Modifier + .padding(horizontal = 5.dp) + .fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = "MIDDLE", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } + }, + "end" to InlineTextContent( + Placeholder( + width = 35.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.Green, + contentColor = Color.White, + modifier = Modifier + .padding(start = 5.dp) + .fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = "END", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } + }, + ), + maxLines = 4, + readMoreText = "Read more", + readLessText = "Read less", + ), ) } } @@ -285,10 +397,12 @@ internal class ReadMoreTextScreenshotTest( is AnnotatedStringScreenshotTestCase -> { ReadMoreText( text = testCase.text, + inlineContent = testCase.inlineContent, expanded = expanded, fontSize = 15.sp, fontStyle = FontStyle.Normal, lineHeight = 22.sp, + readMoreMaxLines = testCase.maxLines, readMoreText = readMoreText, readMoreColor = Color.Blue, readMoreFontSize = 14.sp, @@ -327,6 +441,8 @@ internal data class StringScreenshotTestCase( internal data class AnnotatedStringScreenshotTestCase( override val name: String, val text: AnnotatedString, + val inlineContent: Map = mapOf(), + val maxLines: Int = 2, override val readMoreText: String, override val readLessText: String, override val isRtl: Boolean = false, diff --git a/readmore-material3/api/current.api b/readmore-material3/api/current.api index 44ecba0..888543e 100644 --- a/readmore-material3/api/current.api +++ b/readmore-material3/api/current.api @@ -2,8 +2,8 @@ package com.webtoonscorp.android.readmore.material3 { public final class ReadMoreTextKt { + method @androidx.compose.runtime.Composable public static void ReadMoreText(androidx.compose.ui.text.AnnotatedString text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional boolean softWrap, optional kotlin.jvm.functions.Function1 onTextLayout, optional java.util.Map inlineContent, optional androidx.compose.ui.text.TextStyle style, optional String readMoreText, optional long readMoreColor, optional long readMoreFontSize, optional androidx.compose.ui.text.font.FontStyle? readMoreFontStyle, optional androidx.compose.ui.text.font.FontWeight? readMoreFontWeight, optional androidx.compose.ui.text.font.FontFamily? readMoreFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readMoreTextDecoration, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional long readLessColor, optional long readLessFontSize, optional androidx.compose.ui.text.font.FontStyle? readLessFontStyle, optional androidx.compose.ui.text.font.FontWeight? readLessFontWeight, optional androidx.compose.ui.text.font.FontFamily? readLessFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readLessTextDecoration, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); method @androidx.compose.runtime.Composable public static void ReadMoreText(String text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional boolean softWrap, optional kotlin.jvm.functions.Function1 onTextLayout, optional androidx.compose.ui.text.TextStyle style, optional String readMoreText, optional long readMoreColor, optional long readMoreFontSize, optional androidx.compose.ui.text.font.FontStyle? readMoreFontStyle, optional androidx.compose.ui.text.font.FontWeight? readMoreFontWeight, optional androidx.compose.ui.text.font.FontFamily? readMoreFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readMoreTextDecoration, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional long readLessColor, optional long readLessFontSize, optional androidx.compose.ui.text.font.FontStyle? readLessFontStyle, optional androidx.compose.ui.text.font.FontWeight? readLessFontWeight, optional androidx.compose.ui.text.font.FontFamily? readLessFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readLessTextDecoration, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); - method @androidx.compose.runtime.Composable public static void ReadMoreText(androidx.compose.ui.text.AnnotatedString text, boolean expanded, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1? onExpandedChange, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional boolean softWrap, optional kotlin.jvm.functions.Function1 onTextLayout, optional androidx.compose.ui.text.TextStyle style, optional String readMoreText, optional long readMoreColor, optional long readMoreFontSize, optional androidx.compose.ui.text.font.FontStyle? readMoreFontStyle, optional androidx.compose.ui.text.font.FontWeight? readMoreFontWeight, optional androidx.compose.ui.text.font.FontFamily? readMoreFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readMoreTextDecoration, optional int readMoreMaxLines, optional int readMoreOverflow, optional androidx.compose.ui.text.SpanStyle readMoreStyle, optional String readLessText, optional long readLessColor, optional long readLessFontSize, optional androidx.compose.ui.text.font.FontStyle? readLessFontStyle, optional androidx.compose.ui.text.font.FontWeight? readLessFontWeight, optional androidx.compose.ui.text.font.FontFamily? readLessFontFamily, optional androidx.compose.ui.text.style.TextDecoration? readLessTextDecoration, optional androidx.compose.ui.text.SpanStyle readLessStyle, optional int toggleArea); } } diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png new file mode 100644 index 0000000..d487497 Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_collapsed.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png new file mode 100644 index 0000000..47c68e0 Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.all_expanded.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.default_none.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.default_none.png new file mode 100644 index 0000000..97f096f Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.default_none.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png new file mode 100644 index 0000000..7467316 Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.expanded_none.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png new file mode 100644 index 0000000..97f096f Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_collapsed.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png new file mode 100644 index 0000000..47c68e0 Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readLessText_expanded.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png new file mode 100644 index 0000000..d487497 Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_collapsed.png differ diff --git a/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png new file mode 100644 index 0000000..7467316 Binary files /dev/null and b/readmore-material3/screenshots/ReadMoreTextScreenshotTest.InlineTextContent.readMoreText_expanded.png differ diff --git a/readmore-material3/src/main/java/com/webtoonscorp/android/readmore/material3/ReadMoreText.kt b/readmore-material3/src/main/java/com/webtoonscorp/android/readmore/material3/ReadMoreText.kt index 3b36524..62ec4cf 100644 --- a/readmore-material3/src/main/java/com/webtoonscorp/android/readmore/material3/ReadMoreText.kt +++ b/readmore-material3/src/main/java/com/webtoonscorp/android/readmore/material3/ReadMoreText.kt @@ -16,6 +16,7 @@ package com.webtoonscorp.android.readmore.material3 import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -270,6 +271,8 @@ public fun ReadMoreText( * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw selection around the text. + * @param inlineContent A map store composables that replaces certain ranges of the text. It's used + * to insert composables into text layout. Check [InlineTextContent] for more information. * @param style Style configuration for the text such as color, font, line height etc. * @param readMoreText The read more text to be displayed in the collapsed state. * @param readMoreColor [Color] to apply to the read more text. If [Color.Unspecified], and [style] @@ -325,6 +328,7 @@ public fun ReadMoreText( lineHeight: TextUnit = TextUnit.Unspecified, softWrap: Boolean = true, onTextLayout: (TextLayoutResult) -> Unit = {}, + inlineContent: Map = mapOf(), style: TextStyle = LocalTextStyle.current, readMoreText: String = "", readMoreColor: Color = Color.Unspecified, @@ -399,6 +403,7 @@ public fun ReadMoreText( style = mergedStyle, onTextLayout = onTextLayout, softWrap = softWrap, + inlineContent = inlineContent, readMoreText = readMoreText, readMoreMaxLines = readMoreMaxLines, readMoreOverflow = readMoreOverflow, diff --git a/readmore-material3/src/test/java/com/webtoonscorp/android/readmore/material3/ReadMoreTextScreenshotTest.kt b/readmore-material3/src/test/java/com/webtoonscorp/android/readmore/material3/ReadMoreTextScreenshotTest.kt index edc02bc..53a4eb2 100644 --- a/readmore-material3/src/test/java/com/webtoonscorp/android/readmore/material3/ReadMoreTextScreenshotTest.kt +++ b/readmore-material3/src/test/java/com/webtoonscorp/android/readmore/material3/ReadMoreTextScreenshotTest.kt @@ -15,14 +15,26 @@ */ package com.webtoonscorp.android.readmore.material3 +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle @@ -30,6 +42,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.captureRoboImage @@ -116,6 +129,105 @@ internal class ReadMoreTextScreenshotTest( readMoreText = "Read more", readLessText = "Read less", ), + AnnotatedStringScreenshotTestCase( + name = "InlineTextContent", + text = buildAnnotatedString { + appendInlineContent("start") + append("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + appendInlineContent("middle") + append("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + appendInlineContent("end") + }, + inlineContent = mapOf( + "start" to InlineTextContent( + Placeholder( + width = 55.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.Red, + contentColor = Color.White, + modifier = Modifier + .padding(end = 5.dp) + .fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = "START", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } + }, + "middle" to InlineTextContent( + Placeholder( + width = 70.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.Blue, + contentColor = Color.White, + modifier = Modifier + .padding(horizontal = 5.dp) + .fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = "MIDDLE", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } + }, + "end" to InlineTextContent( + Placeholder( + width = 35.sp, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.Green, + contentColor = Color.White, + modifier = Modifier + .padding(start = 5.dp) + .fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = "END", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } + }, + ), + maxLines = 4, + readMoreText = "Read more", + readLessText = "Read less", + ), ) } } @@ -285,10 +397,12 @@ internal class ReadMoreTextScreenshotTest( is AnnotatedStringScreenshotTestCase -> { ReadMoreText( text = testCase.text, + inlineContent = testCase.inlineContent, expanded = expanded, fontSize = 15.sp, fontStyle = FontStyle.Normal, lineHeight = 22.sp, + readMoreMaxLines = testCase.maxLines, readMoreText = readMoreText, readMoreColor = Color.Blue, readMoreFontSize = 14.sp, @@ -327,6 +441,8 @@ internal data class StringScreenshotTestCase( internal data class AnnotatedStringScreenshotTestCase( override val name: String, val text: AnnotatedString, + val inlineContent: Map = mapOf(), + val maxLines: Int = 2, override val readMoreText: String, override val readLessText: String, override val isRtl: Boolean = false, diff --git a/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/foundation/BasicReadMoreTextDemo.kt b/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/foundation/BasicReadMoreTextDemo.kt index 8a94b79..07c6b7d 100644 --- a/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/foundation/BasicReadMoreTextDemo.kt +++ b/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/foundation/BasicReadMoreTextDemo.kt @@ -17,13 +17,19 @@ package com.webtoonscorp.android.readmore.sample.compose.foundation import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme @@ -36,11 +42,17 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle @@ -95,6 +107,8 @@ fun BasicReadMoreTextDemo() { Divider() Item_CustomText() Divider() + Item_InlineTextContent() + Divider() Item_RTL() Divider() Item_Emoji() @@ -361,6 +375,122 @@ private fun Item_CustomText() { } } +@Composable +private fun Item_InlineTextContent() { + val start = "start" + val middle = "middle" + val end = "end" + val annotatedDescription = buildAnnotatedString { + appendInlineContent(start) + append("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + appendInlineContent(middle) + append("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + appendInlineContent(end) + } + val (expanded, onExpandedChange) = rememberSaveable { mutableStateOf(false) } + Column { + Text( + text = stringResource(id = R.string.title_inline_content_text_compose), + modifier = Modifier + .fillMaxWidth() + .padding(start = 18.dp, end = 18.dp, top = 16.dp), + color = MaterialTheme.colors.onSurface, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ) + val density = LocalDensity.current + BasicReadMoreText( + text = annotatedDescription, + expanded = expanded, + onExpandedChange = onExpandedChange, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(start = 18.dp, top = 5.dp, end = 18.dp, bottom = 18.dp), + style = TextStyle.Default.copy( + color = MaterialTheme.colors.onSurface, + fontSize = 15.sp, + fontStyle = FontStyle.Normal, + lineHeight = 22.sp, + ), + inlineContent = mapOf( + start to InlineTextContent( + Placeholder( + width = with(density) { 55.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "START", + color = Color.Red, + modifier = Modifier.padding(end = 5.dp), + ) + }, + middle to InlineTextContent( + Placeholder( + width = with(density) { 70.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "MIDDLE", + color = Color.Blue, + modifier = Modifier.padding(horizontal = 5.dp), + ) + }, + end to InlineTextContent( + Placeholder( + width = with(density) { 35.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "END", + color = Color.Green, + modifier = Modifier.padding(start = 5.dp), + ) + }, + ), + readMoreMaxLines = 3, + readMoreText = stringResource(id = R.string.read_more), + readMoreStyle = SpanStyle( + color = MaterialTheme.colors.secondary, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + ), + readLessText = stringResource(id = R.string.read_less), + ) + } +} + +@Composable +private fun Badge( + text: String, + color: Color, + modifier: Modifier = Modifier, + contentColor: Color = Color.White, + shape: Shape = RoundedCornerShape(4.dp), +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxSize() + .background(color = color, shape = shape) + .clip(shape = shape), + ) { + BasicText( + text = text, + style = TextStyle.Default.copy( + color = contentColor, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ), + ) + } +} + @Composable private fun Item_RTL() { val (expanded, onExpandedChange) = rememberSaveable { mutableStateOf(false) } diff --git a/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material/ReadMoreTextDemo.kt b/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material/ReadMoreTextDemo.kt index a714bf2..da947a7 100644 --- a/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material/ReadMoreTextDemo.kt +++ b/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material/ReadMoreTextDemo.kt @@ -18,16 +18,21 @@ package com.webtoonscorp.android.readmore.sample.compose.material import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.rememberScaffoldState @@ -36,11 +41,16 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString @@ -91,6 +101,8 @@ fun ReadMoreTextDemo() { Divider() Item_CustomText() Divider() + Item_InlineTextContent() + Divider() Item_RTL() Divider() Item_Emoji() @@ -337,6 +349,119 @@ private fun Item_CustomText() { } } +@Composable +private fun Item_InlineTextContent() { + val start = "start" + val middle = "middle" + val end = "end" + val annotatedDescription = buildAnnotatedString { + appendInlineContent(start) + append("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + appendInlineContent(middle) + append("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + appendInlineContent(end) + } + val (expanded, onExpandedChange) = rememberSaveable { mutableStateOf(false) } + Column { + Text( + text = stringResource(id = R.string.title_inline_content_text_compose), + modifier = Modifier + .fillMaxWidth() + .padding(start = 18.dp, end = 18.dp, top = 16.dp), + color = MaterialTheme.colors.onSurface, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ) + val density = LocalDensity.current + ReadMoreText( + text = annotatedDescription, + expanded = expanded, + onExpandedChange = onExpandedChange, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(start = 18.dp, top = 5.dp, end = 18.dp, bottom = 18.dp), + color = MaterialTheme.colors.onSurface, + fontSize = 15.sp, + fontStyle = FontStyle.Normal, + lineHeight = 22.sp, + inlineContent = mapOf( + start to InlineTextContent( + Placeholder( + width = with(density) { 55.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "START", + color = Color.Red, + modifier = Modifier.padding(end = 5.dp), + ) + }, + middle to InlineTextContent( + Placeholder( + width = with(density) { 70.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "MIDDLE", + color = Color.Blue, + modifier = Modifier.padding(horizontal = 5.dp), + ) + }, + end to InlineTextContent( + Placeholder( + width = with(density) { 35.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "END", + color = Color.Green, + modifier = Modifier.padding(start = 5.dp), + ) + }, + ), + readMoreMaxLines = 3, + readMoreText = stringResource(id = R.string.read_more), + readMoreColor = MaterialTheme.colors.secondary, + readMoreFontSize = 14.sp, + readMoreFontWeight = FontWeight.Bold, + readLessText = stringResource(id = R.string.read_less), + ) + } +} + +@Composable +private fun Badge( + text: String, + color: Color, + modifier: Modifier = Modifier, + contentColor: Color = Color.White, + shape: Shape = RoundedCornerShape(4.dp), +) { + Surface( + shape = shape, + color = color, + contentColor = contentColor, + modifier = modifier.fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = text, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } +} + @Composable private fun Item_RTL() { val (expanded, onExpandedChange) = rememberSaveable { mutableStateOf(false) } diff --git a/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material3/ReadMoreTextDemo.kt b/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material3/ReadMoreTextDemo.kt index fd09bd1..2359289 100644 --- a/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material3/ReadMoreTextDemo.kt +++ b/sample/src/main/java/com/webtoonscorp/android/readmore/sample/compose/material3/ReadMoreTextDemo.kt @@ -18,12 +18,16 @@ package com.webtoonscorp.android.readmore.sample.compose.material3 import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -32,6 +36,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -42,13 +47,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString @@ -109,6 +119,8 @@ fun ReadMoreTextDemo() { HorizontalDivider() Item_CustomText() HorizontalDivider() + Item_InlineTextContent() + HorizontalDivider() Item_RTL() HorizontalDivider() Item_Emoji() @@ -355,6 +367,119 @@ private fun Item_CustomText() { } } +@Composable +private fun Item_InlineTextContent() { + val start = "start" + val middle = "middle" + val end = "end" + val annotatedDescription = buildAnnotatedString { + appendInlineContent(start) + append("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + appendInlineContent(middle) + append("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + appendInlineContent(end) + } + val (expanded, onExpandedChange) = rememberSaveable { mutableStateOf(false) } + Column { + Text( + text = stringResource(id = R.string.title_inline_content_text_compose), + modifier = Modifier + .fillMaxWidth() + .padding(start = 18.dp, end = 18.dp, top = 16.dp), + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ) + val density = LocalDensity.current + ReadMoreText( + text = annotatedDescription, + expanded = expanded, + onExpandedChange = onExpandedChange, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(start = 18.dp, top = 5.dp, end = 18.dp, bottom = 18.dp), + color = MaterialTheme.colorScheme.onSurface, + fontSize = 15.sp, + fontStyle = FontStyle.Normal, + lineHeight = 22.sp, + inlineContent = mapOf( + start to InlineTextContent( + Placeholder( + width = with(density) { 55.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "START", + color = Color.Red, + modifier = Modifier.padding(end = 5.dp), + ) + }, + middle to InlineTextContent( + Placeholder( + width = with(density) { 70.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "MIDDLE", + color = Color.Blue, + modifier = Modifier.padding(horizontal = 5.dp), + ) + }, + end to InlineTextContent( + Placeholder( + width = with(density) { 35.dp.toSp() }, + height = 15.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Badge( + text = "END", + color = Color.Green, + modifier = Modifier.padding(start = 5.dp), + ) + }, + ), + readMoreMaxLines = 3, + readMoreText = stringResource(id = R.string.read_more), + readMoreColor = MaterialTheme.colorScheme.secondary, + readMoreFontSize = 14.sp, + readMoreFontWeight = FontWeight.Bold, + readLessText = stringResource(id = R.string.read_less), + ) + } +} + +@Composable +private fun Badge( + text: String, + color: Color, + modifier: Modifier = Modifier, + contentColor: Color = Color.White, + shape: Shape = RoundedCornerShape(4.dp), +) { + Surface( + shape = shape, + color = color, + contentColor = contentColor, + modifier = modifier.fillMaxSize(), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Text( + text = text, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 14.sp, + ) + } + } +} + @Composable private fun Item_RTL() { val (expanded, onExpandedChange) = rememberSaveable { mutableStateOf(false) } diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index ca76cc3..ee5ff35 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Witness what the gods doโ€ฆafter dark. The friendships and the lies, the gossip and the wild parties, and of course, forbidden love. Because it turns out, the gods arenโ€™t so different from us after all, especially when it comes to their problems. Stylish and immersive, this is one of mythologyโ€™s greatest stories -- The Taking of Persephone -- as itโ€™s never been told before. Custom Text (SpannedString) Custom Text (AnnotatedString) + InlineContent Text (AnnotatedString) Emoji ๐Ÿ˜€๐Ÿ˜ƒ๐Ÿ˜„๐Ÿ˜๐Ÿ˜†๐Ÿ˜…๐Ÿ˜‚๐Ÿคฃ๐Ÿฅฒโ˜บ๏ธ.๐Ÿ˜Š๐Ÿ˜‡๐Ÿ™‚๐Ÿ™ƒ๐Ÿ˜‰๐Ÿ˜Œ๐Ÿ˜๐Ÿฅฐ๐Ÿ‘ช๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ.๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ.๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฆ.๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘ฉโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ˜˜๐Ÿ˜—๐Ÿ˜™๐Ÿ˜š๐Ÿ˜‹๐Ÿ˜›.๐Ÿ˜๐Ÿ˜œ๐Ÿคช๐Ÿคจ๐Ÿง๐Ÿค“๐Ÿ˜Ž๐Ÿฅธ๐Ÿคฉ๐Ÿฅณ.๐Ÿ˜๐Ÿ˜’๐Ÿ˜ž๐Ÿ˜”๐Ÿ˜Ÿ๐Ÿ˜•๐Ÿ™โ˜น๏ธ๐Ÿ˜ฃ๐Ÿ˜–.๐Ÿ˜ซ๐Ÿ˜ฉ๐Ÿฅบ๐Ÿ˜ข๐Ÿ˜ญ๐Ÿ˜ค๐Ÿ˜ ๐Ÿ˜ก๐Ÿคฌ๐Ÿคฏ๐Ÿ˜ณ๐Ÿฅต๐Ÿฅถ๐Ÿ˜ฑ๐Ÿ˜จ๐Ÿ˜ฐ๐Ÿ˜ฅ๐Ÿ˜“๐Ÿค—๐Ÿค”๐Ÿคญ๐Ÿคซ๐Ÿคฅ๐Ÿ˜ถ๐Ÿ˜๐Ÿ˜‘๐Ÿ˜ฌ๐Ÿ™„๐Ÿ˜ฏ๐Ÿ˜ฆ๐Ÿ˜ง๐Ÿ˜ฎ๐Ÿ˜ฒ๐Ÿฅฑ๐Ÿ˜ด๐Ÿคค๐Ÿ˜ช๐Ÿ˜ต๐Ÿค๐Ÿฅด๐Ÿคข๐Ÿคฎ๐Ÿคง๐Ÿ˜ท๐Ÿค’๐Ÿค•๐Ÿค‘๐Ÿค ๐Ÿ˜ˆ๐Ÿ‘ฟ๐Ÿ‘น๐Ÿ‘บ๐Ÿคก๐Ÿ’ฉ๐Ÿ‘ป๐Ÿ’€โ˜ ๏ธ๐Ÿ‘ฝ๐Ÿ‘พ๐Ÿค–๐ŸŽƒ๐Ÿ˜บ๐Ÿ˜ธ๐Ÿ˜น๐Ÿ˜ป๐Ÿ˜ผ๐Ÿ˜ฝ๐Ÿ™€๐Ÿ˜ฟ๐Ÿ˜พ Hyperlink