
During the development of my last application, I had to spend quite a lot of time experimenting with different approaches to the placement of
spans in EditText. In this post I would like to summarize some of this pastime, as well as save time for those who will solve such problems in the future.
There will be little code, only highlights.
First I want to give a small list of facts in order to bring the reader up to date:
')
- Despite the N cores (each with a huge frequency), modern smartphones are still very much inferior in performance even to inexpensive, but large computers.
- Each application in android has a strictly limited amount of allocated memory. And he is not great.
- The setSpan method is slow.
- The more work you take out in Workers, the more responsive your application will be.
- Keep highlighted all the text will not work - only the visible part of it.
- It’s pretty obvious, but still: the search for the placement of spans in the UI stream will not work.
So, immediately to my decision, which, perhaps, is far from the most optimal. In this case, I will be glad to advice.
General description of the structure of the proposed solution

Create a
ScrollView extension and put
EditText in it. In ScrollView, we override
onScrollChanged in order to catch the end of scrolling. At this time, we notify our constantly hanging in the background thread that the text should be parsed.
EditText
hangs the text change listener
TextWatcher . In his
afterTextChanged method,
we inform Worker that the text should be parsed. In the class (the descendant of EditText), we get a Handler, to which from the Worker we will send a list of the spans that need to be hung on the text.
The general scheme is as follows. Now for the details that will be presented in the form of question-answer.
How to catch the end of scrolling?
The onScrollChanged method is called after each "scrolled" pixel, and if you force the stream parser to work after each call, then, of course, nothing good will come of it. Therefore, we do as follows:
private Thread timerThread; protected void onScrollChanged(int x, int y, int oldx, int oldy) { super.onScrollChanged(x, y, oldx, oldy); timer = 500; if (timerThread == null || !timerThread.isAlive()) { timerThread = new Thread(lastScrollTime); timerThread.start(); } } Runnable lastScrollTime = new Runnable() { @Override public void run() { while (timer != 0) { timer -= 10; try { Thread.sleep(10); } catch (InterruptedException e) { } } CustomScrollView.this.post(new Runnable() { @Override public void run() { if (onScrollStoppedListener != null) { onScrollStoppedListener.onScrollStopped(CustomScrollView.this.getScrollY()); } } }); } }; public interface OnScrollStoppedListener { void onScrollStopped(int scrollY); }
That is, each time the method is called, we set the timer to 500 ms and, if the method is not called during this time, we inform OnScrollStoppedListener that the scrolling has stopped. In my case, the OnScrollStoppedListener interface implements my EditText.
How not to start the parser thread after each character entered?
See previous paragraph.
In fact, this method in this case is far from ideal because the user will always have to wait for the N-th number of milliseconds before the parsing process begins. In an amicable way, some kind of intellectual system is needed, which will be understood when the user just types slowly, and when he has already completed some operation (for example, he wrote the echo operator).
How to understand which text falls into the visible area?
Unfortunately, this can not be done exactly, so you have to do about. To start, after each text change, I call the following method:
List<Integer> charsCountPerLine = new ArrayList<>(); public void fillArrayWithCharsCountPerLine(String text) { charsCountPerLine.clear(); charsCountPerLine.add(0); BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(text.getBytes()))); int currentLineLength = 0; char current; try { while (true) { int c = br.read(); if (c == -1) { charsCountPerLine.add(currentLineLength); break; } current = (char) c; currentLineLength++; if (current == '\n') { charsCountPerLine.add(currentLineLength); } } } catch (IOException e) { Log.e(TAG, "", e); } }
That is, I get the symbol number of the beginning of each line. Then, knowing the height of the screen in pixels, we can easily calculate the number of the first and last visible line:
int lineHeight = mEditText.getLineHeight(); int startLine = scrollY / lineHeight;
Having this data, you can easily find the first and last visible symbol.
Why do I need to fill the list with spans? Why not just send each span to the handler right after its creation?
First, then you lose the convenient opportunity to use multiple threads to parse the text. In this configuration, for example, at the stage of inserting a span into the list you can check it for the presence of a double in the sheet. Secondly, in my opinion, the programmer works iteratively. That is, he did some kind of action, and then thought for a second. At this moment, a pack of spans will come to our ui stream and highlight it for a split second. In the opposite case, the spans will come constantly, creating micro brakes UI.
Why do we constantly sleep flow? Why not use ThreadPool?
In theory, it should be a little better, but I have not tried.
I highlighted the general structure of the decision, and, in my opinion, unobvious moments. I hope it will be useful to someone. Thank.