Sorry for the big amount of code guys. Im at a loss lol. I needed a way in an EditText to overwrite chars, but skip the two spaces when the curser comes to them. So that spaces will be "Permanent" in a sense. This is for a basic hexadecimal editor style text box. While this somewhat does work, an when it does its slick. But it seems to have some flaw I am missing, Sometimes when you re typing it will start inserting characters, specifically when the curser is in between two chars ex this is before : "01 02 0|3 04 05" if you type 5 it should replace the 3, then skip over the spaces and end up at the next 0 But it ends up either one of two things, either inserting "01 02 05|3 04 05" or overwriting the 3, and removing one of the two space while jumping "01 02 05 |04 05". lastly it used to sometimes replace a space when the curser was next to a pace but didn't jump over the two spaces, I believe I have worked this out but I've been working on the other problems so I may have been blinded a bit and not noticed it. I'm guessing my text watcher is not being invoked by either formating var not returning to false, or some other thing I've overlooked. Because once curser is moved IE you touch somewhere else in the text, it begins to work briefly till it ends up inserting in between digits again. Anyone see anything I may have missed?
Here is the code so far:
class CustomEditText : AppCompatEditText {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var isFormatting = false
private var mStart = -1 // Start position of the Text being modified
private var mEnd = -1 // End position of the Text being modified
private val watcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
mStart = -1
// Keep track of the start and end position of the text change
if (before == 0) {
// The start and end variables have not been set yet, and there are no characters being deleted.
// Set the start position to the current cursor position.
mStart = start + count
// Set the end position to either the end of the string or the current cursor position + count.
mEnd = min(mStart + count, s!!.length)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// No action needed before text change
}
override fun afterTextChanged(s: Editable?) {
// Avoid infinite recursion if the TextWatcher is modifying the text
if (!isFormatting) {
if (mStart >= 0) {
// Set `isFormatting` to true to indicate that we're currently formatting the text.
isFormatting = true
// Check if the current position is a digit
if (Character.isDigit(s!![mStart - 1])) {
// Check if the next position is a space or the end of the string
if (mStart < s.length && s[mStart] == ' ' || mStart == s.length) {
// If the next position is a space or the end of the string, insert the digit at the next position
s.insert(mStart, s[mStart - 1].toString())
mStart++
} else {
// Overwrite the text at the current position
s.replace(mStart, mEnd, "")
}
} else if (s[mStart - 1] == ' ') {
// Check if the next position is a digit
if (mStart + 1 < s.length && Character.isDigit(s[mStart + 1])) {
// Jump over the spaces and overwrite the first character in the next set
mStart = s.indexOf(" ", mStart) + 2
s.replace(mStart, mStart + 1, "")
} else {
// Overwrite the text at the current position
s.replace(mStart, mEnd, "")
}
} else {
// Overwrite the text at the current position
s.replace(mStart, mEnd, "")
}
isFormatting = false
}
}
}
}
init {
// Initiate and add the text change listener "watcher"
addTextChangedListener(watcher)
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
// Get the current text in the EditText
val text = text
if (text != null) {
val len = text.length
// If the selection start and end are equal, meaning the cursor is at a certain position
if (selStart == selEnd) {
// Check if the cursor is positioned at a space between two hexadecimal digits
// And if the character after the space is also a space
if (selStart != 0 && selStart < len && text[selStart - 1] == ' ' && text[selStart + 1] == ' ') {
// Move the cursor one position to the right to position it at the start of the next hexadecimal digit
setSelection(selStart + 1)
return
}
// Check if the cursor is positioned at a space and the character after the space is not a space
if (selStart < len && text[selStart] == ' ' && (selStart + 1 >= len || text[selStart + 1] != ' ')) {
// Move the cursor one position to the right to position it at the start of the next hexadecimal digit
setSelection(selStart + 1)
}
}
}
// Call the superclass implementation of onSelectionChanged
super.onSelectionChanged(selStart, selEnd)
}
}
I've also toyed with using drawables for the spaces, I even thought that maybe if I make a custom drawable similar to a tictactoe board if you will, and have the digits in between the drawable to achieve the same result. I know either way I still have to handle backspaces and the arrow key movement in the even the user is using a keyboard, but that's a 3 day debug session for another time. If anyone has any ideas or see anything I missed that would be awesome, Or if you think this approach is not the best. I tired many different ways to approach this but this got the closest result to working. I do feel as though a drawable may be much more resource intensive than a text watcher, albeit this would be as well with larger files, but that can all be solved down the road. This is allot, and I don't expect a magical fix. But more eyes on a project might be able to spot what I've missed, thank you for your time =)
EDIT-----------
So it seems this is a buggy way to go about this type of use case, and very unreliable. Ive started working on instead overriding the onDraw method in EditText to simply draw the text in the positions. Hoping this isn't too resource intensive as I haven't ran the code on hardware yet to see but at any rate I'm assuming it will stay in place when edited and be pretty resource efficient as compared to other methods I've tried(Some even an S22 ultra had a hard time with). Then it's simply implementing overwrite mode. Which i have already done. Hopefully this pans out. If anyone has a better idea or if the above code can be made more reliable I would still love to see it! For now my efforts are going towards onDraw.
Related
I am creating a pixel art editor for Android, and as for all pixel art editors, a paint bucket (fill tool) is a must need.
To do this, I did some research on flood fill algorithms online.
I stumbled across the following video which explained how to implement an iterative flood fill algorithm in your code. The code used in the video was JavaScript, but I was easily able to convert the code from the video to Kotlin:
https://www.youtube.com/watch?v=5Bochyn8MMI&t=72s&ab_channel=crayoncode
Here is an excerpt of the JavaScript code from the video:
Converted code:
Tools.FILL_TOOL -> {
val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
val queue = LinkedList<XYPosition>()
queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
val selectedColor = getSelectedColor()
while (queue.isNotEmpty() && seedColor != selectedColor) { // While the queue is not empty the code below will run
val current = queue.poll()
val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
if (color != seedColor) {
continue
}
instance.extraCanvas.apply {
instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
for (index in expandToNeighborsWithMap(instance, current)) {
val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
queue.offer(candidate)
}
}
}
}
Now, I want to address two major issues I'm having with the code of mine:
Performance
Flooding glitch (fixed by suggestion from person in the comments)
Performance
A flood fill needs to be very fast and shouldn't take less than a second, the problem is, say I have a canvas of size 50 x 50, and I decide to fill in the whole canvas, it can take up to 8 seconds or more.
Here is some data I've compiled for the time it's taken to fill in a whole canvas given the spanCount value:
spanCount
approx time taken in seconds to fill whole canvas
10
<1 seconds
20
~2 seconds
40
~6 seconds
60
~15 seconds
100
~115 seconds
The conclusion from the data is that the flood fill algorithm is unusually slow.
To find out why, I decided to test out which parts of the code are taking the most time to compile. I came to the conclusion that the expandToNeighbors function is taking the most time out of all the other tasks:
Here is an excerpt of the expandToNeighbors function:
fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List<Int> {
var asIndex1 = from.x
var asIndex2 = from.x
var asIndex3 = from.y
var asIndex4 = from.y
if (from.x > 1) {
asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
}
if (from.x < instance.spanCount) {
asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
}
if (from.y > 1) {
asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
}
if (from.y < instance.spanCount) {
asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
}
return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
}
To understand the use of the expandToNeighbors function, I would recommend watching the video that I linked above.
(The if statements are there to make sure you won't get an IndexOutOfBoundsException if you try and expand from the edge of the canvas.)
This function will return the index of the north, south, west, and east pixels from the xyPositionData list which contains XYPosition objects.
(The black pixel is the from parameter.)
The xyPositionData list is initialized once in the convertXYDataToIndex function, here:
var xyPositionData: List<XYPosition>? = null
var rectangleData: List<RectF>? = null
fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
if (rectangleData == null) {
rectangleData = instance.rectangles.keys.toList()
}
if (xyPositionData == null) {
xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
rectangleData!!.size,
instance.spanCount.toInt()
)
}
return xyPositionData!!.indexOf(from)
}
So, the code works fine (kind of) but the expandToNeighbors function is very slow, and it is the main reason why the flood fill algorithm is taking a long time.
My colleague suggested that indexOf may be slowing everything down, and that I should probably switch to a Map-based implementation with a key being XYPosition and a value being Int representing the index, so I replaced it with the following:
fun expandToNeighborsWithMap(instance: MyCanvasView, from: XYPosition): List<Int> {
var asIndex1 = from.x
var asIndex2 = from.x
var asIndex3 = from.y
var asIndex4 = from.y
if (from.x > 1) {
asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
}
if (from.x < instance.spanCount) {
asIndex2 = rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
}
if (from.y > 1) {
asIndex3 = rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
}
if (from.y < instance.spanCount) {
asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
}
return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
}
It functions the same way, only this time it uses a Map which is initialized here:
var xyPositionData: List<XYPosition>? = null
var rectangleData: List<RectF>? = null
var rectangleDataMap: Map<XYPosition, Int>? = null
fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
if (rectangleData == null) {
rectangleData = instance.rectangles.keys.toList()
}
if (xyPositionData == null) {
xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
rectangleData!!.size,
instance.spanCount.toInt()
)
}
if (rectangleDataMap == null) {
rectangleDataMap = MathExtensions.convertListToMap(
rectangleData!!.size,
instance.spanCount.toInt()
)
}
return xyPositionData!!.indexOf(from)
}
Converting the code to use a map increased the speed by around 20%, although the algorithm is still slow.
After trying to make the algorithm work faster, I'm out of ideas and I'm unsure why the expandToNeighbors function is taking a long time.
Implementation-wise it is quite messy unfortunately because of the whole list index to XYPosition conversions, but at least it works - the only problem is the performance.
So I have two one major problem.
I've actually pushed the fill tool to GitHub as a KIOL (Known Issue or Limitation), so the user can use the fill tool if they want, but they need to be aware of the limitations/issues. This is so anyone can have a look at my code and reproduce the bugs.
Link to repository:
https://github.com/realtomjoney/PyxlMoose
Edit
I understand that this question is extremely difficult to answer and will require a lot of thinking. I would recommend cloning PyxlMoose and reproduce the errors, then work from there. Relying on the code snippets isn't enough.
Formula for converting XY position to an index
Somebody in the comments suggested a formula for converting an XYPosition to an index value, I came up with the following method which works:
fun convertXYPositionToIndex(xyPosition: XYPosition, spanCount: Int): Int {
val positionX = xyPosition.x
val positionY = xyPosition.y
return (spanCount - positionY) + (spanCount * (positionX - 1))
}
The only problem is - it increases the speed by around 50% but it's still taking around 10-15 seconds to fill in an area of 80 by 80 pixels, so it has helped to a large degree although it's still very slow.
I think the performance issue is because of expandToNeighbors method generates 4 points all the time. It becomes crucial on the border, where you'd better generate 3 (or even 2 on corner) points, so extra point is current position again. So first border point doubles following points count, second one doubles it again (now it's x4) and so on.
If I'm right, you saw not the slow method work, but it was called too often.
How I fixed it:
Getting rid of the toList() calls.
Creating an convertXYPositionToIndex() function.
Here is my new code:
Tools.FILL_TOOL -> {
val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
val queue = LinkedList<XYPosition>()
val spanCount = instance.spanCount.toInt()
queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), spanCount))
val selectedColor = getSelectedColor()
while (queue.isNotEmpty() && seedColor != selectedColor) {
val current = queue.poll()
val color = instance.rectangles[rectangleData[convertXYDataToIndex(spanCount, current)]]?.color ?: Color.WHITE
if (color != seedColor) {
continue
}
instance.rectangles[rectangleData[convertXYDataToIndex(spanCount, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
instance.extraCanvas.drawRect(rectangleData[MathExtensions.convertXYPositionToIndex(current, spanCount)], defaultRectPaint)
for (index in expandToNeighborsWithMap(spanCount, current)) {
val candidate = MathExtensions.convertIndexToXYPosition(index, spanCount)
queue.offer(candidate)
}
}
val timeTakenForThis = (System.currentTimeMillis()-startTime)
totalTime += timeTakenForThis
}
Expand to neighbors func:
fun expandToNeighborsWithMap(spanCount: Int, from: XYPosition): List<Int> {
val toReturn = mutableListOf<Int>()
if (from.x > 1) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x - 1, from.y), spanCount))
}
if (from.x < spanCount) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x + 1, from.y), spanCount))
}
if (from.y > 1) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x, from.y - 1), spanCount))
}
if (from.y < spanCount) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x, from.y + 1), spanCount))
}
return toReturn
}
It takes less than a second for canvas sizes of 100 by 100 and 200 by 200, so I'd say it's in the usable stage now.
I would say this is one of the simplest Android flood fill algorithms out there to understand, so if anyone is making an app similar to mine and they want a flood fill tool they can copy my code.
A guy in the comments called EvilTalk helped me with this.
This is my goal : user click on minus button the amount decrease by one and there is a if statement to not allow the amount go lower than 0 .
This is my Code :
var number = 0
view.text_amount.visibility = View.GONE
view.plus_btn.setOnClickListener {
if (number == 5) {
Toast.makeText(
requireContext(),
"Limit in order",
Toast.LENGTH_SHORT
).show()
} else {
view.text_amount.visibility = View.VISIBLE
number++
view.text_amount.text = number.toString()
}
}
view.minus_btn.setOnClickListener {
if (number <= 0) {
view.text_amount.visibility = View.GONE
} else {
number--
view.text_amount.text = number.toString()
}
}
there is problem with code : I don't want the amount be visible after getting to 0 . it's better experience when the amount is equal to 0 not be visible .
I think it has a simple solution but I can't see it .
do you have any idea ?
Your code works fine! If you want to make it simpler, there's a bit of repeated logic you could condense into one function that handles the situations:
fun adjustValue(amount: Int) {
val adjusted = number + amount
if (adjusted > 5) // show Toast
number = adjusted.coerceIn(0, 5)
view.text_amount.text = number.toString()
view.text_amount.visibility = if (number == 0) View.GONE else View.VISIBLE
}
view.minus_btn.setOnClickListener { adjustValue(-1) }
view.plus_btn.setOnClickListener { adjustValue(1) }
basically the idea is you work out the new value (I'm using a temporary variable so we never set number to an invalid value) and show whatever warnings you need to. Then the coerceIn line makes sure we lock it within the valid range of allowed values.
You could do if/else checks and only set the new value if it's a valid one, but sometimes it's simpler and shorter to just set things and then worry about the edge cases, so this is just an example of that!
Same thing for the TextView bit - it's easier to just set the value whatever it is, and then set whether it should be displayed or not. You could use if/else branches to look at the value and decide whether to set the text or not... but why make your life hard? We know the value's in the valid 0 to 5 range, we can hide it if it's 0 and show it otherwise... easy!
You could make the function take a Boolean instead, like plus: Boolean and then go val adjusted = number + if (plus) 1 else -1, but making it an Int means you could easily add a +10 button or whatever without adding any more code or any more validation logic, and it's not any more complicated to pass in -1 instead of false (arguably it's clearer!)
that's probably more than you were asking for but hopefully it's useful. If nothing else, the "just set the text and the visibility every time" approach is good and neat
Decrease the value of text_amount only if it contains a value greater than 0 and after that check again its value and if it is 0 then hide it:
view.minus_btn.setOnClickListener {
if (number > 0) {
number--
view.text_amount.text = number.toString()
if (number == 0) view.text_amount.visibility = View.GONE
}
}
In my Android App I've created 8 TextViews stacked on top of each other. Now I want to load in some plain text into those TextView-Lines. At the moment my Strings have a ";" as delimiter to indicate a line break, however it would be much more convenient if I would detect a linebreak automatically instead of using the hardcoded semicolon approach.
This is my String at the moment:
myString = "" +
"This seems to be some sort of spaceship,;" +
"the designs on the walls appear to be of;" +
"earth origin. It looks very clean here.;"
And in my other class I load in this string into the 8 TextViews, which I've loaded into an ArrayList, using the ";" as a delimiter.
public fun fillLines(myString: String) {
// How To Make Line Breaks Automatic??
for(i: Int in str until myString.split(";").size) {
if(i > textViewArray.size - 1) {
break
}
textViewArray[i].text = myString.split(";")[i]
textViewArray[i].alpha = 1.0f
}
}
Is there any way I can get the same result as shown above but without hardcoding the delimiter as ";" but instead somehow automatically detect the line break which would occur inside the TextView and then use this as a delimiter to advance through all 8 TextView "Lines".
The reason I need 8 TextViews Stacked On top of each other as individual "text lines" is because of an animation technique I want to use.
Line-breaking gets fairly complicated, so my recommendation would be that you allow a TextView to perform the measuring and layout to determine the line breaks. You could have an invisible TextView with the same style as your other views, and attach it to the layout so that it has the same width as your individual TextView instances. From there, add a layout change listener, and you can then retrieve the individual lines from the TextView Layout:
myTextView.text = // your text string here
myTextView.addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
(view as? TextView)?.layout?.let { layout ->
// Here you'll have the individual broken lines:
val lines = (0 until layout.lineCount).map {
layout.text.subSequence(layout.getLineStart(it), layout.getLineVisibleEnd(it)
}
}
}
That said, this comes with the caveat that you'll lose out on hyphenation provided by the TextView, so you may wish to disable hyphenation entirely in your case.
You could fill text view with html. Below example.
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
tvDocument.setText(Html.fromHtml(bodyData,Html.FROM_HTML_MODE_LEGACY));
} else {
tvDocument.setText(Html.fromHtml(bodyData));
}
If your delimiter ; it is possible call method replaceAll(";", "<br>");
Ok I got it working now:
First you must add these properties for the textviews:
android:singleLine="true"
android:ellipsize="none"
Then you can do this:
public fun fillStorylines() {
val linecap = 46
var finalLine: String
var restChars = ""
val index = 9999
val text1: String = "" +
"This seems to be some sort of spaceship, " +
"the designs on the walls appear to be of " +
"earth origin. It looks very clean here. "
for(j: Int in 0..index) {
try {
finalLine = ""
val lines: List<String> = (restChars + text1.chunked(linecap)[j]).split(" ")
for (i: Int in 0 until lines.size - 1) {
finalLine += lines[i] + " "
}
textViewArray[j].text = finalLine
textViewArray[j].alpha = 1.0f
restChars = lines[lines.size - 1]
} catch (ex: Exception) {
break
}
}
}
If anyone knows a more elegant way to solve this please go ahead, your feedback is appreciated :)
All,
I have a database that will store an HTML tagged text to retain formatting information from an EditText. I create this string using HTML.toHtml(EditText.getText). I notice this method wraps whatever Spanned Text is put in it with <p> and </p>. The issue with that is when I got to use the method HTML.fromHtml(HTMLFormattedString) and then use the setText method of either a TextView or EditText there are two extra lines at the end of my actual text, which makes sense because that is how the paragraph tag works with HTML.
My question is is there anyway to make the textView or EditText shrink to not display the extra blank lines? What is the simplest way to do this? I have experimented with just removing the last <p> and </p>, but that only works if the user did not enter 3 or more new lines with the return key.
I ended up searching for white space at the end of the spanned text that was created and removed it. This took care of extra spaces due to the <p> </p> and was less time consuming than overriding the mentioned class to achieve the same results.
public SpannableStringBuilder trimTrailingWhitespace(
SpannableStringBuilder spannableString) {
if (spannableString == null)
return new SpannableStringBuilder("");
int i = spannableString.length();
// loop back to the first non-whitespace character
while (--i >= 0 && Character.isWhitespace(spannableString.charAt(i))) {
}
return new SpannableStringBuilder(spannableString.subSequence(0, i + 1));
}
Well this is just a round about approach. I had the same issue. And you are provided with two options,
1)As you said that paragraph tag works the way what you have suspected. What it does , it appends two "\n" values to the end of each <\p> tag. So you can convert the html to string and remove the last two characters which are usually two "\n"s
or
2) You have get into the Html Class itself. That is, you have to override the HTML class and look for handleP(SpannableStringBuilder text) and change its core logic a little bit.
private static void handleP(SpannableStringBuilder text) {
int len = text.length();
if (len >= 1 && text.charAt(len - 1) == '\n') {
if (len >= 2 && text.charAt(len - 2) == '\n') {
return;
}
text.append("\n");
return;
}
if (len != 0) {
text.append("\n\n");
}
}
As you can see here, it appends two "\n" in len!=0 which is were you have to do the change.
I need to detect text changes in an EditText. I've tried TextWatcher, but it doesn't work in a way I would expect it to. Take the onTextChanged method:
public void onTextChanged( CharSequence s, int start, int before, int count )
Say I have the text "John" in already in the EditText. If press another key, "e", s will be "Johne", start will be 0, before will be 4, and count will be 5. The way I would expect this method to work would be the difference between what the EditText previously was, and what it's about to become.
So I would expect:
s = "Johne"
start = 4 // inserting character at index = 4
before = 0 // adding a character, so there was nothing there before
count = 1 // inserting one character
I need to be able to detect individual changes every time a key is pressed. So if I have text "John", I need to know "e" was added at index 4. If I backspace "e", I need to know "e" was removed from index 4. If I put the cursor after "J" and backspace, I need to know "J" was removed from index 0. If I put a "G" where "J" was, I want to know "G" replaced "J" at index 0.
How can I achieve this? I can't think of a reliable way to do this.
Use a textwatcher and do the diff yourself. store the previous text inside the watcher, and then compare the previous text to whatever sequence you get onTextChanged. Since onTextChanged is fired after every character, you know your previous text and the given text will differ by at most one letter, which should make it simple to figure out what letter was added or removed where. ie:
new TextWatcher(){
String previousText = theEditText.getText();
#Override
onTextChanged(CharSequence s, int start, int before, int count){
compare(s, previousText); //compare and do whatever you need to do
previousText = s;
}
...
}
The best approach you can follow to identify text changes.
var previousText = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
previousText = s.toString()
}
override fun onTextChanged(newText: CharSequence?, start: Int, before: Int, count: Int) {
val position = start + before ;
if(newText!!.length > previousText.length){ //Character Added
Log.i("Added Character", " ${newText[position]} ")
Log.i("At Position", " $position ")
} else { //Character Removed
Log.i("Removed Character", " ${previousText[position-1]} ")
Log.i("From Position", " ${position-1} ")
}
}
override fun afterTextChanged(finalText: Editable?) { }
You need to store and update the previous CharSequence every time the text is changed. You can do so by implementing the TextWatcher.
Example:
final CharSequence[] previousText = {""};
editText.addTextChangedListener(new TextWatcher()
{
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
#Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
if(i1 > 0)
{
System.out.println("Removed Chars Positions & Text:");
for(int index = 0; index < i1; index++)
{
System.out.print((i + index) + " : " + previousText[0].charAt(i + index)+", ");
}
}
if(i2 > 0)
{
System.out.println("Inserted Chars Positions & Text:");
for(int index = 0; index < i2; index++)
{
System.out.print((index + i) + " : " + charSequence.charAt(i + index)+", ");
}
System.out.print("\n");
}
previousText[0] = charSequence.toString();//update reference
}
#Override public void afterTextChanged(Editable editable)
{
}
});
I faced the exact same problem recently and I wrote my own custom algorithm to detect the diff from the TextWatcher output.
Algorithm -
We store 4 things -
Old selection size
Old text
Old text sequence before the cursor/selection.
Old text sequence after the cursor/selection.
Above 4 things are updated during the beforeTextChanged() callback.
Now during the onTextChanged() callback, we compute following two things -
New text sequence before the cursor/selection.
New text sequence after the cursor/selection.
Now following cases are possible -
Case 1
New text sequence before the cursor/selection == Old text sequence before the cursor/selection AND New text sequence after the cursor/selection isASuffixOf Old text sequence after the cursor/selection
This is a delete forward case. The number of deleted characters can be calculated by the oldText length minus the newText length.
Example -
Old text = Hello wo|rld (| represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello wo|ld (| represents the cursor)
New text sequence before the cursor/selection = Hello wo
New text sequence after the cursor/selection = ld
Clearly, this is a case of delete in forward direction by 1 character.
Case 2
New text sequence after the cursor/selection == Old text sequence after the cursor/selection AND New text sequence before the cursor/selection isAPrefixOf Old text sequence before the cursor/selection
This is a delete backward case. The number of deleted characters can be calculated by the oldText length minus the newText length.
Example -
Old text = Hello wo|rld (| represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello w|rld (| represents the cursor)
New text sequence before the cursor/selection = Hello w
New text sequence after the cursor/selection = rld
Clearly, this is a case of delete in backward direction by 1 character.
Case 3
New text sequence after the cursor/selection == Old text sequence after the cursor/selection AND Old text sequence before the cursor/selection isAPrefixOf New text sequence before the cursor/selection
This is an insert case. The exact insertion string can be calculated by removing the old text sequence from cursor + old text sequence after cursor from the new text string.
Example -
Old text = Hello wo|rld (| represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello wo123|rld (| represents the cursor)
New text sequence before the cursor/selection = Hello wo123
New text sequence after the cursor/selection = rld
Clearly, this is a case of insert and inserted string is 123.
Case 4
If none of the above cases are satisfied, then we can say that it is a replace case. And the replace data is already provided by TextWatcher in the onTextChanged callback.
Here is the code for above algorithm -
class MyTextWatcher : android.text.TextWatcher {
var oldSelectionSize = 0
var oldText: String = ""
var oldSequenceBeforeCursor: String = ""
var oldSequenceAfterCursor: String = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
oldSelectionSize = editText.selectionEnd - editText.selectionStart
oldText = s.toString()
oldSequenceBeforeCursor = s?.subSequence(0, editText.selectionStart).toString()
oldSequenceAfterCursor = s?.subSequence(editText.selectionEnd, s.length).toString()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
s?.toString()?.let { newText ->
val newSequenceBeforeCursor = newText.subSequence(0, selectionStart).toString()
val newSequenceAfterCursor = newText.subSequence(selectionEnd, newText.length)
.toString()
if (newSequenceBeforeCursor == oldSequenceBeforeCursor &&
oldSequenceAfterCursor.endsWith(newSequenceAfterCursor))
// handle delete forward
// number of characters to delete ==>
// if(oldSelectionSize > 0) then deleted number of characters = oldSelectionSize
// else number of characters to delete = oldText.length - newText.length
else if (newSequenceAfterCursor == oldSequenceAfterCursor &&
oldSequenceBeforeCursor.startsWith(newSequenceBeforeCursor))
// handle delete backward
// number of characters to delete ==>
// if(oldSelectionSize > 0) then deleted number of characters = oldSelectionSize
// else number of characters to delete = oldText.length - newText.length
else if (newSequenceAfterCursor == oldSequenceAfterCursor &&
newSequenceBeforeCursor.startsWith(oldSequenceBeforeCursor))
// handle insert
// inserted string = (newText - oldSequenceBeforeCursor) - oldSequenceAfterCursor
else
// handle replace
// replace info already provided in `onTextChanged()` arguments.
}
}
override fun afterTextChanged(s: Editable?) {
}
}