I have EditText with content: #<m id="36c03920-f411-4919-a175-a5b1eb616592">Full name</m>, how are you doing?. I would like to hide tag with id from the user and keep only #Full name, how are you doing? when displaying text. Still getText should return full content.
I found ReplacementSpan useful to this approach. At first step, tried replacing only </m> but text is drawn twice in two lines. First line starts with # and second one starts with <. Also cannot insert new text. getSize is called multiple times and draw is called twice.
Is my approach correct? Should I try to found different solution and store ids in separate collection and do post processing on getText()?
Code:
inner class RemoveTagSpan : ReplacementSpan() {
private val regexEndTag = Regex("</m>")
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
text?.replace(regexEndTag, "")?.let { canvas.drawText(it, start, it.length - 1, x, y.toFloat(), paint) }
}
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: FontMetricsInt?): Int {
return text?.replace(regexEndTag, "")?.let { paint.measureText(it).roundToInt() } ?: 0
}
}
Not sure if this will give you exactly what you want, but generally displaying markup language in a TextView or EditText(?) can be done in this way:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.setText(Html.fromHtml("#<m id="36c03920-f411-4919-a175-a5b1eb616592">Full name</m>, how are you doing?", Html.FROM_HTML_MODE_COMPACT));
} else {
textView.setText(Html.fromHtml("#<m id="36c03920-f411-4919-a175-a5b1eb616592">Full name</m>, how are you doing?"));
Related
I'm trying to use prolificinteractive's material-calendarview to achieve something similar to the image I've attached, which has events added to the calendar. I'm thinking I could do this by creating a custom Span class as shown here in another post.
However, my issue is that DayViewDecorator.decorate doesn't know which date it's currently decorating, so I can't pass specific event names to it. Is there a way I could achieve this using material-calendarview or are there other alternatives? Cheers
What I'm trying to achieve
MainActivity.kt
I thought decorate would be called after shouldDecorate, so I could define calendarDay in shouldDecorate, then call for its corresponding Event value in mapOfCalendarDays. But decorate is actually called first, and only once, before shouldDecorate is called for each day in the Calendar. This results in a crash, though, since calendarDay has not yet been initialized.
calendarView.addDecorator(object: DayViewDecorator {
lateinit var calendarDay: CalendarDay
override fun shouldDecorate(day: CalendarDay?): Boolean {
return if (mapOfCalendarDays.containsKey(day)) { // check if 'day' is in mapOfCalendarDays (a map of CalendarDay to Event)
calendarDay = day!!
true
} else false
}
override fun decorate(view: DayViewFacade?) {
val event: Event? = mapOfCalendarDays[calendarDay]
if (event != null) {
view?.addSpan(AddTextToDates(event.name))
}
}
})
Event.kt
data class Event(
val name: String,
val date: LocalDate
)
AddTextToDates.kt
class AddTextToDates(text: String): LineBackgroundSpan {
private val eventName = text
override fun drawBackground(
canvas: Canvas,
paint: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lnum: Int
) {
canvas.drawText(eventName, ((left+right)/4).toFloat(), (bottom+15).toFloat(), paint)
}
}
i have 1 edittext maxLength = 30, but i can only type 6 character emoji dog => 1 emoji dog = 6 regular character. So please help me type 30 emoji dog. Thanks everyone.
[enter image description here][1]
When someone types an emoji, you can call .length on that emoji, then increase your max character count by that amount. (You would have to remember your original character count and use that on your UI if you wanted to hide the magic).
i.e. when someone types a "dog" then you increase your max count from 30 to 35. (1 has been used and a dog usually counts for 6)
Ref:
https://twitter.com/riggaroo/status/1148278279713017858
https://developer.android.com/reference/java/text/BreakIterator
https://lemire.me/blog/2018/06/15/emojis-java-and-strings/
https://developer.android.com/reference/kotlin/androidx/emoji/text/EmojiCompat
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
oldTextString = charSequence.toString()
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
var newTextString = editable.toString()
if (!oldTextString.equals(newTextString)) {
if (Character.codePointCount(
newTextString,
0,
newTextString.length
) > maxCharactersAllowed
) {
newTextString = oldTextString
}
editText.setText(newTextString)
editText.setSelection(newTextString.length)
}
}
})
I have an edittext. I've added a text watcher to edittext. I listen text changes. İf the word user typing starts with #, I show user suggestions (like when you type # and twitter show you suggestions)
If text starts with a normal letter everything works fine.
For example:
hello #random_user how are you
#this also works because there is an empty space before '#'
This examples works.
However if text starts with special characters Text Watcher shows incorrect values
For example:
#hello_user
#someHashtag
text watcher return false value. I'm using onTextChanged method to track text
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//Text in edittext is '#user' but I get:
//start = 0, before = 0 and count = 1;
//edittext.getSelectionStart() also returns 1 but cursor is at the end of the line.
//edittext.getText().toString().length() also returns 1 but #user is 5 length.
}
How can I solve this?
Edit: edittext.postdesc.getText().toString() only returns first char. For example if my text is '#user', getText method only returns #
You can try this workaround:
class TextWatcherExtended(
private val onTextChanged: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit
) : TextWatcher {
private var before = 0
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) {
this.before = text?.length ?: 0
}
override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) {
onTextChanged.invoke(text, start, this.before, count)
}
}
I'm trying to create a custom class that extends ImageSpan because I need some kind of margin/padding on the spans.
What I figured I need to do is to override the getSize function to return a bigger width so the spans get graphically spaced.
The problem is that as soon as I override the getSize function my view gets completely screwed up. My educated guess is then that I'm doing something stupid inside that funcion, but I can't get what.
Custom class code:
class PaddingImageSpan(drawable: Drawable, private val offset: Float = 0f) : ImageSpan(drawable) {
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
val width = paint.measureText(text, start, end)
val fontMetricsInt = paint.fontMetricsInt
if (fm != null){
fm.ascent = fontMetricsInt.ascent
fm.bottom = fontMetricsInt.bottom
fm.descent = fontMetricsInt.descent
fm.leading = fontMetricsInt.leading
fm.top = fontMetricsInt.top
}
println(width)
return width.roundToInt()
}
}
I figured it out. I'm posting the solution so if someone looks for it he can find it!
My problem was I was using the text metrics instead of the drawable metrics.
This is the correct code:
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
val rect = drawable.bounds
if (fm != null) {
fm.ascent = -rect.bottom
fm.descent = 0
fm.top = fm.ascent
fm.bottom = 0
}
return rect.right// + offset
}
That said, the cleaner way that I could come up with to space spannable is not by working on the spannable class but changing the setBounds() values.
This is similar, but a somewhat different problem than the question I asked here: When my InputFilter source comes in as spannable, the source does not remove the characters I filter out
In my example, I'm making an input filter to validate Canadian zip codes.
I have an abstract here:
abstract class MyInputFilter : InputFilter {
protected abstract fun String.isValid(): Boolean
private fun getFinalResultOfChange(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence {
return dest.replaceRange(dstart, dend, source.subSequence(start, end))
}
private fun getNoChangeResult(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence {
val initialSubSequence = dest.subSequence(dstart, dend)
return try {
if (source is Spanned) {
val spannable = SpannableString(initialSubSequence)
TextUtils.copySpansFrom(source, start, end, null, spannable, 0)
spannable
} else {
initialSubSequence
}
} catch (e: Exception) {
initialSubSequence
}
}
override fun filter(source: CharSequence?, start: Int, end: Int, dest: Spanned?, dstart: Int, dend: Int): CharSequence? {
if (source == null || dest == null) {
return null
}
val input = getFinalResultOfChange(source, start, end, dest, dstart, dend).toString()
return if (input.isValid()) {
null // Allow the edit to proceed unchanged.
} else {
getNoChangeResult(source, start, end, dest, dstart, dend)
}
}
}
which I inherit and override isValid for all of my input filters. This filter is... weird, but came about based on the question linked above, and somewhat solved the problem I had.
This is naive due to my intent to be deliberate, but here is the CAN filter:
private val patterns = listOf(
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z][0-9]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z][0-9][A-Za-z]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z][0-9][A-Za-z][0-9]$"
)
override fun String.isValid(): Boolean {
return when (length) {
0 -> true
1, 2, 3, 4, 5, 6 -> this.matches(Regex(patterns[length - 1]))
else -> false
}
}
Now, I have a new problem. If the very first character entered is an invalid letter, the keyboard keeps loading the characters into a 'word' and trying to resubmit them. So, if I type
D
nothing gets entered, correctly. But, if I type
DA
still nothing is entered, and my phone is trying to 'auto suggest' the word "da". Unless I either click the suggested word or backspace to empty, the field WILL not accept any input of any kind, but the keyboard will continue to 'queue' a word with all of the letters I've typed. If I input a bad character in ANY OTHER position, as long as the first character has been accepted, it works. The phone does not start 'queueing' a word into the keyboard, and I can immediately enter the correct character.
Also of importance: Numbers do not cause the problem. The keyboard does not try to start 'building' a word from a number, so if I type
1A
then "A" is entered into the field with no problems.
I can't find nearly anyone else having these problems with input filters and spannable vs not spannable CharSequence input, but the keyboard is building a word and trying to submit the whole thing in a way that I don't understand. I've tried it on the GBoard on a OnePlus 6T, as well as the default samsung keyboard of a Samsung Tab E, so it doesn't feel like an individual keyboard bug.