Clickable Span not being set for multiple words - android

I don't know if there is an exact solution to my problem on SO, but I have been stuck in this for a long time and need to unblock myself.
I have a string "0000 111 222", and I need to set a link to it. My code to set it is like this.
spanManager.SpannableBuilder(text, link)
.setColorSpan(ContextCompat.getColor(containerView.context, R.color.blue))
.setFontSpan(Typeface.BOLD)
.setClickableSpan(object : ClickableSpan() {
override fun onClick(textView: View) {
onClicked.value = action
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
})
.build()
where link and text both are "0000 111 222".
Now the
setClickableSpan(object: ClickableSpan)
is defined as
fun setClickableSpan(clickableSpan: ClickableSpan): SpannableBuilder {
for (span in spans) {
spannable.setSpan(clickableSpan, span.start, span.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
}
The variable set span is set by SpannableBuilder(text, link) constructor which is like this
fun getSpans(fullText: String, subText: String): Set<Span> {
val spans = HashSet<Span>()
if (fullText.isEmpty() || subText.isEmpty()) {
return spans
}
val ft = fullText.toLowerCase()
val st = subText.trim().toLowerCase()
val fullSplit = ft.split(" ")
val subSplit = st.split(" ")
var startPosition = 0
for (full in fullSplit) {
for (sub in subSplit) {
val subTrim = sub.trim()
if (!full.contains(subTrim)) {
continue
}
val index = startPosition + full.indexOf(subTrim)
val span = Span.create(index, index + subTrim.length)
spans.add(span)
}
startPosition += full.length + 1
}
return spans
}
It basically splices the link variable across space delimiters. So the function
setClickableSpan(object: ClickableSpan)
has size of spans as 3 for "0000 111 222". setSpan is a function defined SpannableStringInternal library.
Now the problem is that the link is being set only for "0000" and not the whole string. Whereas color and font span is able to get set for the whole string. I can not find the cause for this behaviour. If anybody can point out what is wrong here and what could be the probably fix, I'd be grateful.

Related

change value dispayed with onValueSelected - MPAndroidChart

I show percentage values in my pieChart. I want to change those values to real amounts when the user clicks on the slice. Is that possible by using onValueSelected()? I don't want to use IMarker as it's a bit complicated for me. I tried doing this but it didn't work:
override fun onValueSelected(e: Entry?, h: Highlight?) {
val amount = (e as PieEntry).value
e.y = amount
}
You can do this by setting a unique index attribute for each PieEntry, using a OnChartValueSelectedListener to keep track of which item is selected, and a ValueFormatter to choose how to render the item's value text based on whether it is the selected item or not.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val chart = findViewById<PieChart>(R.id.chart)
val myData = listOf(10f,20f,50f)
// Step 1: Create a list of PieEntry objects that uses
// the source data index as its "data" attribute
val entries = myData.mapIndexed { i, v -> PieEntry(v, i)}
val dataset = PieDataSet(entries, "Values")
dataset.isHighlightEnabled = true
dataset.valueTextSize = 14f
dataset.colors = listOf(
Color.parseColor("#FF32DA64"),
Color.parseColor("#FF32DAD4"),
Color.parseColor("#FFB853F2")
)
// Step 2: Create a value formatter that compares the pie entry data
// (aka index in the original list) with the selected index to choose
// what value to show
val myValueFormatter = object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry?): String {
val idx = pieEntry?.data as? Int ?: -2
return if (idx == selectedIndex ) {
// Show the value for the selected item
"$value"
}
else {
// Show percentage for all the rest
"${value / myData.sum() * 100f} %"
}
}
}
dataset.valueFormatter = myValueFormatter
// Step 3: Add a value selected listener to change the selected index
chart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
selectedIndex = h?.x?.roundToInt() ?: -1
}
override fun onNothingSelected() {
selectedIndex = -1
}
})
chart.data = PieData(dataset)
chart.description.isEnabled = false
chart.legend.isEnabled = false
}
Which looks like (de-selected on the left, selected on the right):

how to handle new line in word count?

i'm referring this answer https://stackoverflow.com/a/42802060/12428090
basically my goal is to handle word count of 100 words
the abve solution works fine for typed 100 words and even handles copy paste very well
but it doesnt handles the new line case
suppose in entered word or copy pasted word contains new line then the word count is returning incorrect
following is the code please help out
override fun onTextChanged(s: CharSequence, start: Int, before: Int,
count: Int) {
val wordsLength: Int = countWords(s.toString()) // words.length;
limitmoderator.text = "$wordsLength/$MAX_WORDS"
val yourText: String =
moderator_intro.getText().toString().replace(160.toChar().toString(), " ")
if (yourText.split("\\s+".toRegex()).size > MAX_WORDS) {
var space = 0
var length = 0
for (i in 0 until yourText.length) {
if (yourText[i] == ' ') {
space++
if (space >= MAX_WORDS) {
length = i
break
}
}
}
if (length > 1) {
moderator_intro.getText()
.delete(length, yourText.length) // deleting last exceeded words
setCharLimit(moderator_intro, length - 1) //limit edit text
}
} else {
removeFilter(moderator_intro)
}}
private fun countWords(s: String): Int {
val trim = s.trim()
return if (trim.isEmpty()) 0 else trim.split("\\s+".toRegex()).size
// separate string around spaces
}
private var filter: InputFilter? = null
private fun setCharLimit(et: EditText, max: Int) {
filter = LengthFilter(max)
et.filters = arrayOf<InputFilter>(filter as LengthFilter)
}
private fun removeFilter(et: EditText) {
if (filter != null) {
et.filters = arrayOfNulls(0)
filter = null
}
}
so i have tried rplacing the "\n" in the text but it doesnt seems to be handling the case properly
any help will be appreciated
thanks in advance
Here's a different strategy than the one from the question you linked. Notice I'm using afterTextChanged and not onTextChanged!
I'm manually counting words to get the character index of the first whitespace after the last allowable word. That way I don't have to trim and then use Regex, and then try to figure out the index offset of that Regex. Then instead of applying a temporary filter, I directly cut the end of the Editable off.
editText.setSelection is to keep the cursor from jumping to the beginning.
override fun afterTextChanged(s: Editable) {
var previousWasWhitespace = true
var i = 0
var wordCount = 0
for (c in s) {
val whitespace = c.isWhitespace()
if (whitespace && !previousWasWhitespace && ++wordCount == MAX_WORDS) {
break
}
previousWasWhitespace = whitespace
i++
}
if (i < s.length) {
s.delete(i, s.length)
editText.setSelection(i)
}
}
You could write a regular expression to match the text that you want to keep and remove the rest. In this case you want match (non-whitespace+)(whitespace*) maximum 100 times.
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
val input = moderator_intro.getText().toString()
val maxWords = 100
val whiteSpace = "\\p{javaWhitespace}\u00A0\u2007\u202F" //All white space characters
val keepRegex = Regex("[$whiteSpace]*([^$whiteSpace]+[$whiteSpace]*){1,${maxWords.toString()}}")
val allowedText = keepRegex.find(input)?.value ?: ""
val wordAmount = allowedText.split(Regex("[$whiteSpace]+")).filter { it.isNotBlank() }.size
val trailingWhiteSpace = Regex("[$whiteSpace]+$")
if(wordAmount == maxWords && allowedText.contains(trailingWhiteSpace)) {
val newText = allowedText.replace(trailingWhiteSpace, "")
moderator_intro.getText().delete(allowedText.length, input.length)
setCharLimit(moderator_intro, newText.length)
} else {
removeFilter(moderator_intro)
}
}

How to apply a mask date (mm/dd/yyyy) in TextField with Jetpack Compose?

I have a TextField in which there cannot be more than 10 characters, and the user is required to enter date in the format "mm/dd/yyyy". Whenever user types first 2 characters I append "/", when the user types next 2 characters I append "/" again.
I did the following to achieve this:
var maxCharDate = 10
TextField(
value = query2,
onValueChange = {
if (it.text.length <= maxCharDate) {
if (it.text.length == 2 || it.text.length == 5)
query2 = TextFieldValue(it.text + "/", selection = TextRange(it.text.length+1))
else
query2 = it
}
emailErrorVisible.value = false
},
label = {
Text(
"Date of Birth (mm/dd/yyyy)",
color = colorResource(id = R.color.bright_green),
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = with(LocalDensity.current) { dimensionResource(id = R.dimen._12ssp).toSp() })
},
.
.
.
It's working except that the appended "/" doesn't get deleted on pressing backspace, while other characters do get deleted.
How do I make it such that "/" is deleted too on pressing backspace?
You can do something different using the onValueChange to define a max number of characters and using visualTransformation to display your favorite format without changing the value in TextField.
val maxChar = 8
TextField(
singleLine = true,
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
},
visualTransformation = DateTransformation()
)
where:
class DateTransformation() : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return dateFilter(text)
}
}
fun dateFilter(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 8) text.text.substring(0..7) else text.text
var out = ""
for (i in trimmed.indices) {
out += trimmed[i]
if (i % 2 == 1 && i < 4) out += "/"
}
val numberOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 1) return offset
if (offset <= 3) return offset +1
if (offset <= 8) return offset +2
return 10
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <=2) return offset
if (offset <=5) return offset -1
if (offset <=10) return offset -2
return 8
}
}
return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}
The / is being deleted but as soon as you delete, the length of the text becomes 2 or 5. So it checks the condition,
if (it.text.length == 2 || it.text.length == 5)
Since the condition is true now, the / appends again into the text. So it seems like it is not at all being deleted.
One way to solve this is by storing the previous text length and checking if the text length now is greater than the previous text length.
To achieve this, declare a variable below maxCharDate as
var previousTextLength = 0
And change the nested if condition to,
if ((it.text.length == 2 || it.text.length == 5) && it.text.length > previousTextLength)
And at last update the previousTextLength variable. Below the emailErrorVisible.value = false add
previousTextLength = it.text.length;
Implementation of VisualTranformation that accepts any type of mask for Jetpack Compose TextField:
class MaskVisualTransformation(private val mask: String) : VisualTransformation {
private val specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
override fun filter(text: AnnotatedString): TransformedText {
var out = ""
var maskIndex = 0
text.forEach { char ->
while (specialSymbolsIndices.contains(maskIndex)) {
out += mask[maskIndex]
maskIndex++
}
out += char
maskIndex++
}
return TransformedText(AnnotatedString(out), offsetTranslator())
}
private fun offsetTranslator() = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
val offsetValue = offset.absoluteValue
if (offsetValue == 0) return 0
var numberOfHashtags = 0
val masked = mask.takeWhile {
if (it == '#') numberOfHashtags++
numberOfHashtags < offsetValue
}
return masked.length + 1
}
override fun transformedToOriginal(offset: Int): Int {
return mask.take(offset.absoluteValue).count { it == '#' }
}
}
}
How to use it:
#Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.length <= DATE_LENGTH) {
date = it
}
},
visualTransformation = MaskVisualTransformation(DATE_MASK)
)
}
object DateDefaults {
const val DATE_MASK = "##/##/####"
const val DATE_LENGTH = 8 // Equals to "##/##/####".count { it == '#' }
}
I would suggest not only a date mask, but a simpler and generic solution for inputs masking.
A general formatter interface in order to implement any kind of mask.
interface MaskFormatter {
fun format(textToFormat: String): String
}
Implement our own formatters.
object DateFormatter : MaskFormatter {
override fun format(textToFormat: String): String {
TODO("Format '01212022' into '01/21/2022'")
}
}
object CreditCardFormatter : MaskFormatter {
override fun format(textToFormat: String): String {
TODO("Format '1234567890123456' into '1234 5678 9012 3456'")
}
}
And finally use this generic extension function for transforming your text field inputs and you won't need to care about the offsets at all.
internal fun MaskFormatter.toVisualTransformation(): VisualTransformation =
VisualTransformation {
val output = format(it.text)
TransformedText(
AnnotatedString(output),
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int = output.length
override fun transformedToOriginal(offset: Int): Int = it.text.length
}
)
}
Some example usages:
// Date Example
private const val MAX_DATE_LENGTH = 8
#Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.matches("^\\d{0,$MAX_DATE_LENGTH}\$".toRegex())) {
date = it
}
},
visualTransformation = DateFormatter.toVisualTransformation()
)
}
// Credit Card Example
private const val MAX_CREDIT_CARD_LENGTH = 16
#Composable
fun CreditCardTextField() {
var creditCard by remember { mutableStateOf("") }
TextField(
value = creditCard,
onValueChange = {
if (it.matches("^\\d{0,$MAX_CREDIT_CARD_LENGTH}\$".toRegex())) {
creditCard = it
}
},
visualTransformation = CreditCardFormatter.toVisualTransformation()
)
}
It is because you are checking for the length of the string. Whenever the length is two, you insert a slash. Hence the slash gets deleted, and re-inserted.
Why don't you just create three TextFields and insert Slashes as Texts in between. Such logic can be very hard to perfect. Keen users can use it to crash your app, and also devs can insert malicious stuff, and exploit this flaw because the handling logic can have loopholes as well, so... It is better in my opinion to just go with the simplest (and what I think is more elegant) way of constructing.

Android implement text completion on swipe

How can I implement text completion,Like Gmail's smart compose?
I've an edit text where the user enters server address and I want to detect when they start typing the domain suffix and suggest completion.
Thanks.
First you need an algorithm to get suggestion from a given dictionary.
I've created a simple class named SuggestionManager to get suggestion from a given dictionary for a string input. Instead of returning the full match, it'll only return the remaining part of the given input. Below given a simple unit test along with full source code of the class. You can also go here to run the test online.
SuggestionManager.kt
class SuggestionManager(private val dictionary: Array<String>) {
companion object {
private val WORD_SPLIT_REGEX = Regex("[^A-Za-z0-9'\\-]")
/**
* To get reversed list
*/
private fun getReversedList(list: List<String>): MutableSet<String> {
val reversed = mutableSetOf<String>()
for (item in list.withIndex()) {
if (item.index != 0) {
val rev = list.subList(list.size - item.index, list.size).joinToString(" ")
reversed.add(rev)
}
}
// finally, add the full string
reversed.add(list.joinToString(" "))
return reversed
}
}
fun getSuggestionFor(_text: String?): String? {
var text = _text
// empty text
if (text.isNullOrBlank()) {
return null
}
// Getting last line only
if (text.contains("\n")) {
text = text.split("\n").last()
if (text.trim().isEmpty()) {
return null
}
}
// Splitting words by space
val words = text.split(WORD_SPLIT_REGEX).filter { it.isNotBlank() }
// Getting last word
val lastWord = if (text.endsWith(" ")) "${words.last()} " else words.last()
// Matching if last word exist in any dictionary
val suggestions = mutableSetOf<String>()
for (dicItem in dictionary) {
if (dicItem.contains(lastWord, true)) {
// Storing founded matches
suggestions.add(dicItem)
}
}
// Reverse ordering split-ed words
val pyramidWords = getReversedList(words)
val matchList = mutableListOf<String>()
for (pw in pyramidWords) {
for (sug in suggestions) {
// Storing suggestions starts with reversed word
if (sug.startsWith(pw, true)) {
matchList.add("$pw:$sug")
}
}
}
// Checking if second level match is not empty
if (matchList.isNotEmpty()) {
// Ordering by matched reversed word - (largest key first)
matchList.sortBy { -it.split(":")[0].length }
// Looping through in ascending order
for (m in matchList) {
val match = m.split(":")
val selPw: String = match[0]
var selSug: String = match.subList(1, match.size).joinToString(":")
// trimming to
selSug = selSug.replace(selPw, "", true)
if (text.endsWith(" ")) {
selSug = selSug.trim()
}
return selSug
}
}
return null
}
}
Unit Test
class SuggestionManagerUrlTest {
private val suggestionManager by lazy {
val dictionary = arrayOf(
"google.com",
"facebook.com",
"gmail.com",
"yahoo.com",
"192.168.354.45"
)
SuggestionManager(dictionary)
}
#Test
fun test() {
// null of empty and null input
assertNull(suggestionManager.getSuggestionFor(null))
assertNull(suggestionManager.getSuggestionFor(""))
// matching
assertEquals("ogle.com", suggestionManager.getSuggestionFor("go"))
assertEquals("book.com", suggestionManager.getSuggestionFor("face"))
assertEquals(".168.354.45", suggestionManager.getSuggestionFor("192"))
// no match
assertNull(suggestionManager.getSuggestionFor("somesite"))
}
}
Then, you'll have to set text in EditText with two colors. One for input and other for the suggestion. You may use the Html.fromHtml method to do this.
val text = "<font color=#cc0029>$input</font> <font color=#ffcc00>$suggestion</font>";
yourEditText.setText(Html.fromHtml(text));
Combining these two aspects, you can create a custom EditText.

Android - Fill different colors between two lines using MPAndroidChart

following this (Android - Fill the color between two lines using MPAndroidChart) answer I was able to fill with color the space between two lines using AndroidMPChart library.
But now I want to customize the filling color to have:
the areas above the boundarySet filled with blue color;
the areas below the boundarySet filled with green color.
Like in the following screenshot (please note that the blue line is a lineSet, so it could be that it is not a limit line):
I would like to customize the line color of the chart, setting it as the filling:
blue color for the line above the boundarySet;
green color for the line below the boundary set.
Is it possible?
I'm not able to find anything similar in the examples using MPAndroidChart.
Thank you!
you can try to override the drawLinear in LineChartRender
This has worked for me:
dataSet.setFillFormatter(new DefaultFillFormatter() {
#Override
public float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider) {
return 22500;// its value of midel Y line
}
});
I've done this with this custom chart render) I use colors from line here, but you can use other colors. Change your colors here :drawFilledPath(c, filled, dataSet.colors[index], dataSet.fillAlpha)
class CustomLineChartRender(
lineDataProvider: LineDataProvider,
chartAnimator: ChartAnimator,
port: ViewPortHandler
)
: LineChartRenderer(lineDataProvider, chartAnimator, port) {
override fun drawLinearFill(
c: Canvas?,
dataSet: ILineDataSet?,
trans: Transformer?,
bounds: XBounds?
) {
val filled = mGenerateFilledPathBuffer
val startingIndex = bounds!!.min
val endingIndex = bounds!!.range + bounds!!.min
val indexInterval = 1
var currentStartIndex = 0
var currentEndIndex = indexInterval
var iterations = 0
do {
currentStartIndex = startingIndex + iterations * indexInterval
currentEndIndex = currentStartIndex + indexInterval
currentEndIndex = if (currentEndIndex > endingIndex) endingIndex else currentEndIndex
if (currentStartIndex <= currentEndIndex) {
generateFilledPath(dataSet!!, currentStartIndex, currentEndIndex, filled)
trans!!.pathValueToPixel(filled)
val drawable = dataSet.fillDrawable
if (drawable != null) {
drawFilledPath(c, filled, drawable)
} else {
val index = startingIndex + iterations
drawFilledPath(c, filled, dataSet.colors[index], dataSet.fillAlpha)
}
}
iterations++
} while (currentStartIndex <= currentEndIndex)
}
private fun generateFilledPath(
dataSet: ILineDataSet,
startIndex: Int,
endIndex: Int,
outputPath: Path
) {
val fillMin = dataSet.fillFormatter.getFillLinePosition(dataSet, mChart)
val phaseY = mAnimator.phaseY
val isDrawSteppedEnabled = dataSet.mode == LineDataSet.Mode.STEPPED
outputPath.reset()
val entry = dataSet.getEntryForIndex(startIndex)
outputPath.moveTo(entry.x, fillMin)
outputPath.lineTo(entry.x, entry.y * phaseY)
// create a new path
var currentEntry: Entry? = null
var previousEntry = entry
for (x in startIndex + 1..endIndex) {
currentEntry = dataSet.getEntryForIndex(x)
if (isDrawSteppedEnabled) {
outputPath.lineTo(currentEntry.x, previousEntry!!.y * phaseY)
}
outputPath.lineTo(currentEntry.x, currentEntry.y * phaseY)
previousEntry = currentEntry
}
// close up
if (currentEntry != null) {
outputPath.lineTo(currentEntry.x, fillMin)
}
outputPath.close()
}
}

Categories

Resources