how to handle new line in word count? - android

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)
}
}

Related

Formating a string from right to left for currency values

I'm working on creating a way to input an amount and format it from left to right with placeholder zeros.
For example, pressing 1 and 2 would show $0.12 pressing 3 would give $1.23. Pressing backspace would give $0.12.
Instead, I am getting $1,0002.00
binding.keypad.btnBackspace.setOnClickListener {
val text = binding.tvTotalValue.text.toString()
if(text.isNotEmpty()) {
binding.tvTotalValue.text = text.drop(1)
}
binding.tvTotalValue.text = ""
}
binding.keypad.onNumberSelected = {
processNewAmount(it.toString())
}
private fun processNewAmount(newValue: String) {
val text = binding.tvTotalValue.text.toString().plus(newValue)
val result = text.replace("[^0-9]".toRegex(), "") // remove any characters
val amount = result.toDouble()
binding.tvTotalValue.text = NumberFormat.getCurrencyInstance().format(amount)
}
What am I doing wrong?
Is there a better way to do this?
I advise keeping a property that stores the entered value without formatting. Each time a number is added, you can add it to this entered number and then format it for the screen. That will be a lot simpler than trying to move/remove existing symbols around in the String.
private var enteredNumber = ""
//...
binding.keypad.btnBackspace.setOnClickListener {
enteredNumber = enteredNumber.dropLast(1)
refreshTotal()
}
binding.keypad.onNumberSelected = {
if(!(it == 0 && enteredNumber == "0")) { // avoid multiple zeros or backspace will act unexpectedly
enteredNumber += it.toString()
refreshTotal()
}
}
//...
private fun refreshTotal() {
val amount = enteredNumber.toDouble() / 100.0
binding.tvTotalValue.text =
NumberFormat.getCurrencyInstance().format(amount)
}

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.

Clickable Span not being set for multiple words

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.

(Android) PhoneNumberFormattingTextWatcher not formatting number if user deletes "-" or "(" or ")"

I am using PhoneNumberFormattingTextWathcer to format the mobile number and it's working fine.
But if I am deleting "-" or "(" or ")", number is not formatting again.
I am using below code:
editPrimarynumber.addTextChangedListener(
PhoneNumberFormattingTextWatcher("US")
)
Deleting numbers from right end, works fine.
Please help to resolve this issue.
Thank you.
Use this Formatter.
editPrimarynumber.addTextChangedListener(PhoneTextFormatter(editPrimarynumber,"(###) ###-####"));
PhoneText Formatter
class PhoneTextFormatter(private val mEditText: EditText, private val mPattern: String) :
TextWatcher {
private val tag = this.javaClass.simpleName
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int
) {
// do nothing
}
override fun onTextChanged(
s: CharSequence,
start: Int,
before: Int,
count: Int
) {
val phone = StringBuilder(s)
Log.d(tag, "join")
if (count > 0 && !isValid(phone.toString())) {
for (i in 0 until phone.length) {
Log.d(tag, String.format("%s", phone))
val c = mPattern[i]
if (c != '#' && c != phone[i]) {
phone.insert(i, c)
}
}
mEditText.setText(phone)
mEditText.setSelection(mEditText.text.length)
}
}
override fun afterTextChanged(s: Editable) { // do nothing
}
private fun isValid(phone: String): Boolean {
for (i in 0 until phone.length) {
val c = mPattern[i]
if (c == '#') continue
if (c != phone[i]) {
return false
}
}
return true
}
init {
//set max length of string
val maxLength = mPattern.length
mEditText.filters = arrayOf<InputFilter>(LengthFilter(maxLength))
}
}

EditText replacing character after appending

I'm trying to make a number look like this
932-874838/9
I did this with my EditText to append the - and / after some spaces
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(text: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onValueChange(s.toString())
}
})
}
private fun onValueChange(value: String) {
mNumberTxtView.text = value
if (value.length == 3) {
mNumberTxtView.append("-")
}
if (value.length == 10) {
mNumberTxtView.append("/")
}
}
When I'm typing like
932
it automatically appends the - , and that works, but after it appends the - and if I type another number it replaces the - with that number instead of continuing, so it becomes 932- at first but when trying to put another number,
9328
it gets replaced like that removing the appended -
I think the problem is inside the onValueChange() method
onValueChange should be like this:
var test: StringBuilder = StringBuilder()
var lastValue: String = ""
fun onValueChange(value: String) {
if(lastValue.length > value.length) {
test.deleteCharAt(test.lastIndex)
if(test.length == 3 || test.length == 10) {
test.deleteCharAt(test.lastIndex)
}
} else {
test.append(value.last())
if (test.length == 3) {
test.append("-")
} else if (test.length == 10) {
test.append("/")
}
}
lastValue = value
textView.text = test
}
Try this, instead.
private fun onValueChange(value: String) {
if (value.length == 3) {
mNumberTxtView.text = "${value}_"
} else if (value.length == 10) {
mNumberTxtView.text = "${value}/"
}
}
Let me know if this works.
(The curly brackets around "value" in the strings may not be necessary. I'm still getting used to Kotlin's way of handling string concatenation.)
Edited to remove redundant and potentially loop-causing part.
You should not change text in beforeTextChanged and afterTextChanged to prevent re-call of those methods by TextWatcher. Make changes in afterTextChanged.
But be careful not to get yourself into an infinite loop, because any changes you make will cause this method to be called again recursively.
So set invoke of onValueChanged into afterTextChanged method
with removal of mNumberTxtView.text = value

Categories

Resources