Is it possible use text with annotation? - android

Is it possible use text (not string resource) with annotation?
For example: val text = "Open the page <annotation clickable="page">123</annotation>"
I want get spans from this text like in this article:
val annotations = spannedString.getSpans(
0,
spannedString.length,
android.text.Annotation::class.java
)

Is it possible use text (not string resource) with annotation? For example: val text = "Open the page <annotation clickable="page">123</annotation>"
Yes it's possible by setting the annotations to portions of a SpannableString using setSpan & marking the span with the Annotation class:
val spannedString = SpannableString("Open the page 123")
spannedString.setSpan(Annotation("key", "clickable"), 14, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
val annotations = spannedString.getSpans(
0,
spannedString.length,
android.text.Annotation::class.java
)

Related

How to change styles for all numbers in string in Kotlin

How to change styles for all numbers in strings (from strings.xml) to (small) and (subscript) and (color.blue)? and where put that in recyclerView adapter (all strings in Array)?
Like this:
If you are using Kotlin, use regex to replace all the numbers with HTML formatting.
var text = "This is an example of text[1] formatting."
"\\[[0-9]+\\]".toRegex().findAll(text)
.flatMap { it.groupValues }
.forEach {
val num = it.drop(1).dropLast(1)
text = text.replace(it, "<sup>[$num]</sup>")
}
Then use tvMyTextView.setText(Html.fromHtml(text))

how to assert that text not contains specific characters in android jetpack compose testing?

I'm trying to write some test cases for my compose functions.
I have an outlined Text field with a maximum value of 16 characters.
So I want to test this feature. Here is the test:
#Test
fun checkMaxTaxCodeLength_16Character() {
val taxCode = composeRule.onNodeWithTag(testTag = AUTHENTICATION_SCREEN_TAX_CODE_EDIT_TEXT)
for (i in 'A'..'Z')
taxCode.performTextInput(i.toString())
taxCode.assertTextEquals("ABCDEFGHIJKLMNOP")
}
But although I can see the input is correct, the test fails, and it seems assertTextEquals doesn't work correctly. So:
first of all, what am I doing wrong?
Second, is there any way to, instead of checking the equality, check the text does not contain specific characters?
here is the code of text field:
OutlinedTextField(
value = state.taxCode,
maxLines = 1,
onValueChange = { string ->
viewModel.onEvent(
AuthenticationEvent.TaxCodeChanged(string)
)
},
label = {
Text(text = stringResource(id = R.string.tax_code))
},
modifier = Modifier
.fillMaxWidth()
.testTag(TestingConstant.AUTHENTICATION_SCREEN_TAX_CODE_EDIT_TEXT)
)
The maximum length is handled in the view model. If the user adds more characters than 16, the view model won't update the state and keep the old value.
first of all, what am I doing wrong?
assertTextEquals() takes the value of Text and EditableText in your semantics node combines them and then does a check against the values you pass in. The order does not matter, just make sure to pass in the value of the Text as one of the arguments.
val mNode = composeTestRule.onNodeWithText("Email"))
mNode.performTextInput("test#mail.com")
mNode.assertTextEquals("Email", "test#mail.com")
Please note the text Email is the label for the textfield composable.
To get the semantic information about your nodes you can have
#Test
fun print_semantics_tree() {
composeTestRule.onRoot(useUnmergedTree = true).printToLog(TAG)
}
For the TAG you can use any string. After running the above test you can search the logcat with the specified TAG. You should see something like
|-Node #3 at (l=155.0, t=105.0, r=925.0, b=259.0)px
| Focused = 'false'
| ImeAction = 'Default'
| EditableText = 'test#mail.com'
| TextSelectionRange = 'TextRange(0, 0)'
| Text = '[Email]'
| Actions = [RequestFocus, GetTextLayoutResult, SetText, SetSelection,
OnClick, OnLongClick, PasteText]
Please note you can also obtain the semantics node object with an index operation rather than iterating through all the values.
val value = fetchSemanticsNode().config[EditableText]
assertEquals("test#mail.com", value.toString())
Ok, still, the problem is open, but I achieved what I wanted another way. I used semantic nodes to get what is in edit text and compared it with what it should be:
#Test
fun checkMaxTaxCodeLength_16Character() {
val taxCode = composeRule.onNodeWithTag(testTag = AUTHENTICATION_SCREEN_TAX_CODE_EDIT_TEXT)
for (i in 'A'..'Z')
taxCode.performTextInput(i.toString())
for ((key,value) in taxCode.fetchSemanticsNode().config)
if (key.name =="EditableText")
assertEquals("ABCDEFGHIJKLMNOP",value.toString())
}

Compose TextField shows wierd behavior

I am trying to implement a TextField which inputs an amount and formats it as soon as it is typed and also limits it to 100,000.
#Composable
fun MainScreen(
viewModel: MyViewModel
) {
val uiState by viewModel.uiState.collectAsState()
Column {
AmountSection(
uiState.amount,
viewModel::updateAmount
)
Text(text = viewModel.logs)
}
}
#Composable
fun AmountSection(
amount: TextFieldValue,
updateAmount: (TextFieldValue) -> Unit
) {
BasicTextField(
value = amount,
onValueChange = updateAmount,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number
)
)
MyViewModel:
class MyViewModel: ViewModel() {
private val _uiState = MutableStateFlow(MyUiState())
val uiState: StateFlow<MyUiState> = _uiState
var logs by mutableStateOf("")
var text = ""
fun updateAmount(amount: TextFieldValue) {
val formattedAmount: String = amount.text.getFormattedAmount()
text += "input = ${amount.text}\n"
text += "output = $formattedAmount \n"
logs = text
_uiState.update {
it.copy(amount = TextFieldValue(formattedAmount, TextRange(formattedAmount.length))
}
}
}
data class MyUiState(val amount: TextFieldValue = TextFieldValue())
(logs and text are just for logging purpose. Was finding it difficult to share the logcat output so presented it this way)
Result:
When I press 6, the input is "12,3456" which is expected (ignore the currency)
My getFormattedAmount() function removes the last six as ( 123456 > 100000). It outputs "12,345" which is also correct. "12,345" is what gets displayed on the screen.
But when I press 7, I get the input "12,34567". Where did that 6 come from?? It was not in uiState.amount.
(Please ignore the last output line. getFormattedAmount only removes the last character if the amount exceeds the limit and it gave wrong output because it didn't expect that input)
I feel that I making some really silly mistake here and would be really thankful if somecome could help me find that out.
Edit based on the edit:-
From the looks of the question, it isn't much clear what you wish to achieve here, but this is my deduction -
You just want a TextField that allows numbers to be input, but only up to a maximum value (VALUE, not characters). When a digit-press by a user leads to the value exceeding your max value, you want that digit to be not entered of course, but you wish to reflect no changes at all in this case, i.e., the field value should remain intact.
Based on the above deduction, here is an example:-
First of all, f your uiState variable. I'm keeping it simple for the sake of clarity.
class VM: ViewModel(){
var fieldValue by mutableStateOf("")
fun onFieldUpdate(newValue){
if(newValue.toDouble() > 999999999999999)
return
else
fieldValue = newValue
}
}
#Composable
fun CrazyField(fieldValue: String, onFieldUpdate: (String) -> Unit){
TextField(value = fieldValue, onValueChange = onFieldUpdate)
}
Do not comment further without actually running this.
Original answer:-
Use a doubles parser.
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = {
if (it.toDouble() <= 100000)
text = it //Based on your use-case - it won't cut off text or limit the amount of characters
else text = it.subString(0,7) //In-case of pasting or fast typing
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Found this comment on Compose slack channel by Sean.
As a model, you should assume that the keyboard may make aribitrary and large
edits each onValueChange. This may happen, for example, if the user uses
autocorrect, replaces a word with an emoji, or other smart editing features. To
correctly handle this, write any transformation logic with the assumption that
the current text passed to onValueChange is unrelated to the previous or next
values that will be passed to onValueChange.
So this is some issue with TextField & IME relationship.
I rewrote my getFormattedAmount function to format the given string without any assumptions (earlier it was assuming that amount is formatted till the last second character). Everything seems fixed now.

Android data binding TextView value of String with fall back to #StringRes

context: data binding with a ViewModel, which gets data from a remote source in the form of JSON. I want to display a textual value from that JSON in a TextView, but if the data is absent in the JSON, I want to fall back to a string defined in strings.xml.
android:text="#{viewModel.theText}"
How I currently solved it is with a custom binding adapter that accepts an Any, and checks if the value is an Int or String:
app:anyText="#{viewModel.theText}". The viewModel has something like val theText = json.nullableString ?: R.string.placeholder.
I'm guessing that this is a problem more people deal with, and I was hoping if someone knows a more elegant solution.
You could provide Application context to your ViewModel or Resources and then do something like this:
val theText = json.nullableString ?: resources.getString(R.string.placeholder)
The other option would be keep using binding adapter like you do but I would wrap text input in another object like this:
data class TextWrapper(
val text: String?,
#StringRes val default: Int
)
#BindingAdapter("anyText")
fun TextView.setAnyText(textWrapper: TextWrapper) {
text = textWrapper.text ?: context.getString(textWrapper.default)
}
val theText = TextWrapper(text = json.nullableString, default = R.string.placeholder)
You do not need an adapter to handle this use Null coalescing operator operator ?? in xml.
Try below code:
android:text="#{viewModel.theText?? #string/your_default_text}"
Use case :
The null coalescing operator (??) chooses the left operand if it isn't null or the right if the former is null.
P.S: lean more about DB and expressions here-> https://developer.android.com/topic/libraries/data-binding/expressions

How to Convert androidx.compose.ui.graphics.Color to android.graphics.Color (int)

How to convert from Compose Color to Android Color Int?
I am using this code for the moment and it seems to be working, I can't seem to find a function to get the Int value of the color
Color.rgb(color.red.toInt(), color.green.toInt(), color.blue.toInt())
where Color.rgb is a function android.graphics.Color that returns an Integer color and color variable is just a Compose Color !
Since the float one requires higher API
Linked : How to convert android.graphics.Color to androidx.compose.ui.graphics.Color
You can use the toArgb() method
Converts this color to an ARGB color int. A color int is always in the
sRGB color space
Something like:
//Compose Color androidx.compose.ui.graphics
val Teal200 = Color(0xFFBB86FC)
//android.graphics.Color
val color = Teal200.toArgb()
You can also use something like:
//android.graphics.Color
val color = android.graphics.Color.argb(
Teal200.toArgb().alpha,
Teal200.toArgb().red,
Teal200.toArgb().green,
Teal200.toArgb().blue
)
The Android Compose Color class now has function toArgb(), which converts to Color Int.
The function documentation says:
Converts this color to an ARGB color int. A color int is always in the sRGB color space. This implies a color space conversion is applied if needed.
-> No need for a custom conversion function anymore.
Worked for me convert to Argb, then convert to string:
fun Int.hexToString() = String.format("#%06X", 0xFFFFFF and this)
fun getStringColor(color: Color): String {
return color.toArgb().hexToString()
}
fun someOperation() {
val color = Color.White //compose color
val colorAsString = getStringColor(color)
print(colorAsString)
}
// Result will be: "#FFFFFF"
Here's a spin on the answer from #gabriele with some Kotlin sugar:
//Compose Color androidx.compose.ui.graphics
val Teal200 = Color(0xFFBB86FC)
fun Color.toAGColor() = toArgb().run { android.graphics.Color.argb(alpha, red, green, blue) }
//android.graphics.Color
val color = Teal200.toAGColor()
What worked for me is:
color.hashCode()

Categories

Resources