I want to capizalize every word typed in EditText inside onTextChanged. I've tried some solutions but none of them worked. Problem what I'm facing is if you change capitalize letter on keyboard and you will type James JoNEs it should repair that String to correct form after you type E character to Jone. This is not working with default android:inputType="textCapWords". I've used some function what I've found but it is not working at all.
fun onFieldChanged(s: String, tv: TextWatcher, et: EditText) {
et.removeTextChangedListener(tv)
val changedString = capitalizeFirstLetterWord(s)
with(et) {
text.clear()
append(changedString)
setSelection(changedString.length)
}
et.addTextChangedListener(tv)
}
fun capitalizeFirstLetterWord(s: String): String{
var finalStr = ""
if(s != "") {
val strArray = s.split("[\\s']")
if (strArray.isNotEmpty()) {
for(i in strArray.indices){
finalStr+= capitalize(strArray[i])
}
}
}
return finalStr
}
You can try to achieve that with something like this
"yourString".split(" ").map { it.toLowerCase().capitalize() }.joinToString(" ")
Related
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)
}
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)
}
}
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.
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.
I want to allow users only to type certain characters based on the a regex in my android applications. How do I achieve it?
Used a TextWatcher as #Matt Ball suggested.
#Override
public void afterTextChanged(Editable s) {
String text = s.toString();
int length = text.length();
if(length > 0 && !Pattern.matches(PATTERN, text)) {
s.delete(length - 1, length);
}
}
Edit
Although the TextWatcher works, it would be cleaner to use an InputFilter. Check this example.
You could use android:digits on the xml EditText instead of using a regex.
For your allowed chars of the regex (numbers, comma and plus symbol):
android:digits="0123456789,+"
And you could use a string resource as the digits value in case you want to reuse it.
Try this: If the character to type matches /[a-zA-Z0-9{insert valid characters here}]/ then allow it, otherwise don't.
You can use an InputFilter for advanced filtering:
class CustomInputFilter : InputFilter {
private val regex = Pattern.compile("^[A-Z0-9]*$")
override fun filter(
source: CharSequence,
start: Int,
end: Int,
dest: Spanned?,
dstart: Int,
dend: Int
): CharSequence? {
val matcher = regex.matcher(source)
return if (matcher.find()) {
null
} else {
""
}
}
}
And then add it to an EditText or TextInputEditText like this:
textInputLayout.editText!!.filters += CustomInputFilter()
//or
editText.filters += CustomInputFilter()
Remember that if you have a TextWatcher this will not prevent the TextWatcher to fire, you can filter out those events checking if the previous and next text values are the same.
Something like:
//addTextChangedListener is an extension function available in core-ktx library
textInputLayout.editText!!.addTextChangedListener(afterTextChanged = { editable ->
val editTextValue = viewModel.editTextLiveData.value ?: ""
if (!editTextValue.equals(editable)) {
viewModel.updateEditTextValue(editable?.toString() ?: "")
}
})