From 7a362e76ea4cc34da0807a961d681b19da550c27 Mon Sep 17 00:00:00 2001 From: rahulsainani Date: Thu, 23 Mar 2023 17:15:52 +0100 Subject: [PATCH] Bug 1824134 - Add support for clickable linkified text in OnboardingPage.kt --- .../fenix/onboarding/view/OnboardingPage.kt | 109 +++++++++++++++--- .../onboarding/view/OnboardingPageState.kt | 11 ++ 2 files changed, 104 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt index 1f966dc62..0741f5014 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -24,6 +25,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.mozilla.fenix.R @@ -38,18 +42,24 @@ import org.mozilla.fenix.theme.FirefoxTheme */ private const val IMAGE_HEIGHT_RATIO = 0.4f +/** + * The tag used for links in the text for annotated strings. + */ +private const val URL_TAG = "URL_TAG" + /** * A composable for displaying onboarding page content. * * @param pageState [OnboardingPageState] The page content that's displayed. - * @param onDismiss Invoked when the user clicks the close button. * @param modifier The modifier to be applied to the Composable. + * @param onDismiss Invoked when the user clicks the close button. This defaults to null. When null, + * it doesn't show the close button. */ @Composable fun OnboardingPage( pageState: OnboardingPageState, - onDismiss: () -> Unit, modifier: Modifier = Modifier, + onDismiss: (() -> Unit)? = null, ) { BoxWithConstraints( modifier = Modifier @@ -65,15 +75,19 @@ fun OnboardingPage( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { - IconButton( - onClick = onDismiss, - modifier = Modifier.align(Alignment.End), - ) { - Icon( - painter = painterResource(id = R.drawable.mozac_ic_close), - contentDescription = stringResource(R.string.onboarding_home_content_description_close_button), - tint = FirefoxTheme.colors.iconPrimary, - ) + if (onDismiss != null) { + IconButton( + onClick = onDismiss, + modifier = Modifier.align(Alignment.End), + ) { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_close), + contentDescription = stringResource(R.string.onboarding_home_content_description_close_button), + tint = FirefoxTheme.colors.iconPrimary, + ) + } + } else { + Spacer(Modifier) } Column( @@ -98,11 +112,9 @@ fun OnboardingPage( Spacer(modifier = Modifier.height(16.dp)) - Text( - text = pageState.description, - color = FirefoxTheme.colors.textSecondary, - textAlign = TextAlign.Center, - style = FirefoxTheme.typography.body2, + DescriptionText( + description = pageState.description, + linkTextState = pageState.linkTextState, ) } @@ -131,6 +143,71 @@ fun OnboardingPage( } } +@Composable +private fun DescriptionText( + description: String, + linkTextState: LinkTextState?, +) { + if (linkTextState != null) { + LinkText( + text = description, + linkTextState = linkTextState, + ) + } else { + Text( + text = description, + color = FirefoxTheme.colors.textSecondary, + textAlign = TextAlign.Center, + style = FirefoxTheme.typography.body2, + ) + } +} + +/** + * A composable for displaying text that contains a clickable link text. + * + * @param text The complete text. + * @param linkTextState The clickable part of the text. + */ +@Composable +private fun LinkText( + text: String, + linkTextState: LinkTextState, +) { + val annotatedString = buildAnnotatedString { + val startIndex = text.indexOf(linkTextState.text) + val endIndex = startIndex + linkTextState.text.length + append(text) + addStyle( + style = SpanStyle(color = FirefoxTheme.colors.textAccent), + start = startIndex, + end = endIndex, + ) + + addStringAnnotation( + tag = URL_TAG, + annotation = linkTextState.url, + start = startIndex, + end = endIndex, + ) + } + + ClickableText( + text = annotatedString, + style = FirefoxTheme.typography.body2.copy( + textAlign = TextAlign.Center, + color = FirefoxTheme.colors.textSecondary, + ), + onClick = { + val range: AnnotatedString.Range? = + annotatedString.getStringAnnotations(URL_TAG, it, it).firstOrNull() + range?.let { stringAnnotation -> + linkTextState.onClick(stringAnnotation.item) + } + }, + ) +} + @LightDarkPreview @Composable private fun OnboardingPagePreview() { diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt index 08447a2f4..990b3af04 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt @@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes * @param image [DrawableRes] displayed on the page. * @param title [String] title of the page. * @param description [String] description of the page. + * @param linkTextState [LinkTextState] part of description text with a link. * @param primaryButton [Action] action for the primary button. * @param secondaryButton [Action] action for the secondary button. * @param onRecordImpressionEvent Callback for recording impression event. @@ -20,11 +21,21 @@ data class OnboardingPageState( @DrawableRes val image: Int, val title: String, val description: String, + val linkTextState: LinkTextState? = null, val primaryButton: Action, val secondaryButton: Action? = null, val onRecordImpressionEvent: () -> Unit, ) +/** + * Model containing link text, url and action. + */ +data class LinkTextState( + val text: String, + val url: String, + val onClick: (String) -> Unit, +) + /** * Model containing text and action for a button. */