How adding ImageSpan in jetpack compose Text - android

As we know, AnnotatedString in JetpackCompose has provided some API of Android's SpannedString.
but I didn't find any way/workaround to inline ImageSpan to a Text (except using AndroidView)

Putting images inside text can be done using AnnotatedString and inlineContent parameter of Text Composable.
Inside buildAnnotatedString { ... } we need to define some id for our inline content using appendInlineContent(id = ...)
Then in Text Composable in inlineContent parameter we provide a map matching this id to InlineTextContent() object.
You can basically put any content there as long as you can define its size up-front in Placeholder.
Here is how it looks with an Image put in the middle of the text:
val annotatedString = buildAnnotatedString {
append("This is text ")
appendInlineContent(id = "imageId")
append(" with a call icon")
}
val inlineContentMap = mapOf(
"imageId" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Image(
imageVector = Icons.Default.Call,
modifier = Modifier.fillMaxSize(),
contentDescription = ""
)
}
)
Text(annotatedString, inlineContent = inlineContentMap)

Related

Showing a text field in the app bar in Jetpack Compose with Material3

Many Android apps feature a search box in the app bar / toolbar, that looks somewhat like this screenshot:
How do I recreate something similar in Jetpack Compose + Material3? I would like the text field to have these features:
support showing a hint when the content string is empty (e.g. "Type your search...")
auto-focus on the first composition, so that the keyboard is opened
put the cursor at the end of the content string on the first composition
I tried to reproduce the same behavior in Jetpack Compose + Material3 by putting a TextField in the title of a CenterAlignedTopAppBar but the result does not look good. The text field uses the whole height of the app bar and has a grey background, and both of these things look odd.
I came up with a AppBarTextField after some engineering, see the code below. I had to use the lower-level BasicTextField since the normal TextField is not customizable enough. The code having to do with theming and color was copied directly from TextField's implementation, so that the theme's customizations apply normally to the components of the text field.
The parameters the AppBarTextField composable accepts are:
value: the content string to show in the text field
onValueChange: new values are passed here (remember to update value!)
hint: the hint to show when the text field is empty
modifier, keyboardOptions and keyboardActions: they are passed directly to BasicTextField and they behave the same as they would in a normal TextField. If you need to customize other TextField parameters just add them to the function signature and then pass them to BasicTextField.
The requested features are implemented:
the focus acquisition was achieved with a SideEffect, so that it would only happen on the first composition
putting the cursor at the end on the first composition required using a TextFieldValue
the strange-looking background is not present anymore, since no .background() modifier is present (while it is in the normal TextField)
the hint was added using by passing a placeholder to TextFieldDecorationBox in the decorationBox parameter (note that this was also possible with TextField)
TextFieldDecorationBox's padding is also now only 4dp. Padding was added here (and not with a modifier on BasicTextField) since otherwise the bottom line indicator (which is, instead, displayed using the .indicatorLine() modifier) would not be shown correctly.
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun AppBarTextField(
value: String,
onValueChange: (String) -> Unit,
hint: String,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
val interactionSource = remember { MutableInteractionSource() }
val textStyle = LocalTextStyle.current
// make sure there is no background color in the decoration box
val colors = TextFieldDefaults.textFieldColors(containerColor = Color.Unspecified)
// If color is not provided via the text style, use content color as a default
val textColor = textStyle.color.takeOrElse {
MaterialTheme.colorScheme.onSurface
}
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor, lineHeight = 50.sp))
// request focus when this composable is first initialized
val focusRequester = FocusRequester()
SideEffect {
focusRequester.requestFocus()
}
// set the correct cursor position when this composable is first initialized
var textFieldValue by remember {
mutableStateOf(TextFieldValue(value, TextRange(value.length)))
}
textFieldValue = textFieldValue.copy(text = value) // make sure to keep the value updated
CompositionLocalProvider(
LocalTextSelectionColors provides LocalTextSelectionColors.current
) {
BasicTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
// remove newlines to avoid strange layout issues, and also because singleLine=true
onValueChange(it.text.replace("\n", ""))
},
modifier = modifier
.fillMaxWidth()
.heightIn(32.dp)
.indicatorLine(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors
)
.focusRequester(focusRequester),
textStyle = mergedTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = true,
decorationBox = { innerTextField ->
// places text field with placeholder and appropriate bottom padding
TextFieldDefaults.TextFieldDecorationBox(
value = value,
visualTransformation = VisualTransformation.None,
innerTextField = innerTextField,
placeholder = { Text(text = hint) },
singleLine = true,
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors,
contentPadding = PaddingValues(bottom = 4.dp)
)
}
)
}
}
Here is an example usage:
var value by rememberSaveable { mutableStateOf("initial content") }
CenterAlignedTopAppBar(
title = {
AppBarTextField(
value = value,
onValueChange = { newValue -> value = newValue },
hint = "A hint..."
)
},
navigationIcon = /* the back icon */,
actions = /* the search icon */
)

Make composable wrap content - Jetpack Compose

I am trying to make the ImageComposable wrap its height and width according to its content, along with the two Text composable, align to the bottom of Assemble composable. Following is the code for that:
#Composable
fun ImageComposable(url:String){
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current).data(url).apply{
placeholder(drawableResId = R.drawable.ic_broken_pic)
}.build()
)
Image(painter = painter, contentDescription = null, Modifier.padding(2.dp).border(width = 2.dp, shape = CircleShape, color = MaterialTheme.colors.onPrimary)
}
#Composable
fun Assemble(url:String){
Column (modifier = Modifier.fillMaxWidth().height(400.dp).background(MaterialTheme.colors.primary)
.padding(16.dp), verticalArrangement = Arrangement.Bottom) {
ImageComposable(url)
Text(text = "title")
Text(text = "Body")
}
}
but the ImageComposable ends up taking all the height and width of the Assemble composable and I am not able to see the two Text composables that I added in the column. So I am confused as to what is the exact problem here. I thought at least it should show the ImageComposable along with the two Text composable but it is not happening.
I am using coil image loading library here for parsing the image from url. For now in testing, I am passing url as an Empty String. Hence I am calling the composable as:
Assemble("")
I didn't find any document that would help me understand this behavior. So I wanted to know the reason to this problem and possible solutions to overcome it.
You can explicitly specify the height of each component:
fun ImageComposable(modifier: Modifier = Modifier, url: String){
//...
Image(modifier = modifier, //...
}
Column(//..
ImageComposable(modifier = Modifier.height(200.dp)//...
Text(modifier = Modifier.height(50.dp)//...
Text(modifier = Modifier.height(150.dp)//...
}
Or you can specify a fraction of the maximum height it will take up:
fun ImageComposable(modifier: Modifier = Modifier, url: String){
//...
Image(modifier = modifier, //...
}
Column(//..
ImageComposable(modifier = Modifier.fillMaxHeight(0.75f)//...
Text(modifier = Modifier.fillMaxHeight(0.1f)//...
Text(modifier = Modifier.fillMaxHeight(0.15f)//...
}
You can also try playing with the weight modifier:
fun ImageComposable(modifier: Modifier = Modifier, url: String){
//...
Image(modifier = modifier, //...
}
Column(//..
ImageComposable(modifier = Modifier.weight(1f)//...
Text(modifier = Modifier.weight(1f, fill = false)//...
Text(modifier = Modifier.weight(1f, fill = false)//...
}
It would be easier to solve your problem if there would be a sketch of what you want to achieve.
Nevertheless, I hope I can help:
It looks like the issue you are facing can be handled by Intrinsic measurements in Compose layouts.
The column measures each child individually without the dimension of your text constraining the image size. For this Intrinsics can be used.
Intrinsics lets you query children before they're actually measured.
For example, if you ask the minIntrinsicHeight of a Text with infinite width, it'll return the height of the Text as if the text was drawn in a single line.
By using IntrinsicSize.Max for the width of the Assemble composable like this:
#Composable
fun Assemble(url: String) {
Column(
modifier = Modifier
.width(IntrinsicSize.Max)
.background(MaterialTheme.colors.primary)
.padding(16.dp), verticalArrangement = Arrangement.Bottom
) {
ImageComposable(url)
Text(text = "title")
Text(text = "Body")
}
}
you can can create a layout like this:
(Please note that I am using a local drawable here)
You can now see the 2 texts and the width of the image is adjusted to the width of the texts.
Using Intrinsics to measure children in dependance to each other should help you to achieve what you wanted.
Please let me know if this layout does not meet your expectations.

Accompanist FlowRow : is it possible to scroll down automatically in order to show an element?

In my Jetpack Compose project, one of my components uses a FlowRow from Accompanist.
But I don't know how to make the FlowRow scroll to a given "node".
Here the relevant code from my #Composable:
sealed class MovesNavigatorElement(open val text: String)
data class MoveNumber(override val text: String) : MovesNavigatorElement(text)
data class HalfMoveSAN(override val text: String) : MovesNavigatorElement(text)
#Composable
fun MovesNavigator(modifier: Modifier = Modifier, elements: Array<MovesNavigatorElement>, mustBeVisibleByDefaultElementIndex: Int) {
val vertScrollState = rememberScrollState()
FlowRow(
modifier = modifier
.background(color = Color.Yellow.copy(alpha = 0.3f))
.verticalScroll(vertScrollState),
mainAxisSpacing = 10.dp,
crossAxisSpacing = 15.dp,
) {
elements.map {
Text(text = it.text, fontSize = 34.sp, color = Color.Blue, style= MaterialTheme.typography.body1)
}
}
}
Where you can see that I declare the "nodes" of the FlowRow as a list : the parameter elements. Also I'm using a ScrollState in the local variable vertScrollState.
But, let's say that I want to make it scroll to elements[30] : how should I do that ? Given that mustBeVisibleByDefaultElementIndex is the index of the element that must be visible by default. I mean, when composition occurs. But the user can change the position later of course.
In other words :
At composition : the element whose index is given is made visible
Then, before any other composition occurs of course, the user can scroll it with the scrollbar.
You just need to use the method parameter,
val state = rememberScrollState(initial = mustBeVisibleByDefault)
Better press Ctrl + P to see all possible combinations before going to even the web.
I kept this in case if anyone finds it helpful bizarrely:-
The ScrollState exposes scrollTo and animateScrollTo methods. You can easily use them to achieve the desired result. Refer to the docs
Here how I manage to solve (partially) my issue :
use a plain ScrollState instead of rememberScrollState() : because even if I set scroll to a fixed value at recomposition, the user still will be able to move it. So, no need to "cache" the scroll value
use sp to px conversion for the scroll amount which is expected a value in pixels, and an hard-coded division amount of the given index (that's why it is only partially solved)
Which led me to the following :
sealed class MovesNavigatorElement(open val text: String)
data class MoveNumber(override val text: String) : MovesNavigatorElement(text)
data class HalfMoveSAN(override val text: String) : MovesNavigatorElement(text)
#Composable
fun MovesNavigator(modifier: Modifier = Modifier, elements: Array<MovesNavigatorElement>, mustBeVisibleByDefaultElementIndex: Int = 0) {
val lineHeightPixels = with(LocalDensity.current) {34.sp.toPx()}
val scrollAmount = ((mustBeVisibleByDefaultElementIndex / 6) * lineHeightPixels).toInt()
val vertScrollState = ScrollState(scrollAmount)
FlowRow(
modifier = modifier
.background(color = Color.Yellow.copy(alpha = 0.3f))
.verticalScroll(vertScrollState),
mainAxisSpacing = 8.dp,
) {
elements.map {
Text(text = it.text, fontSize = 34.sp, color = Color.Blue, style= MaterialTheme.typography.body1)
}
}
}
But still have to test on several devices.

How i can pass the icon in this compossable function ? So that i can reuse it

#Composable
fun LoginMethod(icon: Icon , text : String) {
Row(
modifier = Modifier
.padding(8.dp)
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(6.dp)
)
) {
Image(imageVector = icon , contentDescription ="")
Spacer(modifier = Modifier.height(4.dp))
Text(text = text)
}
}
You can use something like:
#Composable
fun LoginMethod(icon: ImageVector, text : String) {
/* your code */
}
and then call it with:
LoginMethod( Icons.Filled.Add, "title" )
If you wish to access the icon anywhere, store it in a viewmodel. You cannot re-reference the Composable from anywhere like views, but as far as the usage INSIDE the scope of the Composable is concerned, you can use the passed-in icon anywhere inside it. You must store it for use outside

AutoLink for Android Compose Text

Is there any way to use android:autoLink feature on JetPack Compose Text?
I know, that it is maybe not "declarative way" for using this feature in one simple tag/modifier, but maybe there is some easy way for this?
For styling text I can use this way
val apiString = AnnotatedString.Builder("API provided by")
apiString.pushStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
)
)
apiString.append("https://example.com")
Text(text = apiString.toAnnotatedString())
But, how can I manage clicks here? And would be great, if I programatically say what behaviour I expect from the system (email, phone, web, etc). Like it. works with TextView.
Thank you
We can achieve Linkify kind of TextView in Android Compose like this example below,
#Composable
fun LinkifySample() {
val uriHandler = UriHandlerAmbient.current
val layoutResult = remember {
mutableStateOf<TextLayoutResult?>(null)
}
val text = "API provided by"
val annotatedString = annotatedString {
pushStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
)
)
append(text)
addStringAnnotation(
tag = "URL",
annotation = "https://example.com",
start = 0,
end = text.length
)
}
Text(
fontSize = 16.sp,
text = annotatedString, modifier = Modifier.tapGestureFilter { offsetPosition ->
layoutResult.value?.let {
val position = it.getOffsetForPosition(offsetPosition)
annotatedString.getStringAnnotations(position, position).firstOrNull()
?.let { result ->
if (result.tag == "URL") {
uriHandler.openUri(result.item)
}
}
}
},
onTextLayout = { layoutResult.value = it }
)
}
In the above example, we can see we give the text and also we use addStringAnnotation to set the tag. And using tapGestureFilter, we can get the clicked annotation.
Finally using UriHandlerAmbient.current we can open the link like email, phone, or web.
Reference : https://www.hellsoft.se/rendering-markdown-with-jetpack-compose/
The most important part of jetpack compose is the compatibility with native android components.
Create a component that use TextView and use it:
#Composable
fun DefaultLinkifyText(modifier: Modifier = Modifier, text: String?) {
val context = LocalContext.current
val customLinkifyTextView = remember {
TextView(context)
}
AndroidView(modifier = modifier, factory = { customLinkifyTextView }) { textView ->
textView.text = text ?: ""
LinkifyCompat.addLinks(textView, Linkify.ALL)
Linkify.addLinks(textView, Patterns.PHONE,"tel:",
Linkify.sPhoneNumberMatchFilter, Linkify.sPhoneNumberTransformFilter)
textView.movementMethod = LinkMovementMethod.getInstance()
}
}
How to use:
DefaultLinkifyText(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
text = "6999999 and https://stackoverflow.com/ works fine"
)

Categories

Resources