Во время разработки моего последнего приложения мне пришлось провести довольно много времени, экспериментируя с разными подходами к размещению span’ов в EditText. В этом посте хотелось бы подвести некоторый итог этого времяпрепровождения, а также сэкономить время тем, кто в будущем будет решать подобные задачи.
Кода будет немного, только основные моменты.
Для начала хочу привести небольшой список фактов для того, чтобы ввести читателя в курс дела:
- Несмотря на N ядер (каждое с огромной частотой), современный смартфоны все еще очень сильно уступают в производительности даже недорогим, но большим компьютерам.
- Каждое приложении в андроиде имеет строго ограниченный размер выделяемой памяти. И он не велик.
- Метод setSpan работает медленно.
- Чем больше работы вы вынесете в Worker’ы, тем отзывчивее будет ваше приложение.
- Держать подсвеченным весь текст не получится — только видимую его часть.
- Довольно очевидно, но все же: поиск места размещения спана в UI потоке делать не получится.
Итак, сразу к моему решению, которое, возможно, далеко не самое оптимальное. В этом случае буду рад советам.
Общие описание структуры предлагаемого решения
Создаем расширение ScrollView и в него помещаем EditText. У ScrollView переопределяем onScrollChanged для того, чтобы отлавливать момент окончания скроллинга. В это время уведомляем наш постоянно висящий в фоне поток о том, что текст надо распарсить.
EditText’у вешаем слушателя изменения текста — TextWatcher’а . В его методе afterTextChanged информируем Worker’а о том, что надо распарсить текст. В классе (потомке EditText) заводим Handler, в который из Worker’а будем отсылать список спанов, которые необходимо навесить на текст.
Общая схема такова. Теперь к деталями, которые изложу в форме вопрос-ответ.
Как отловить момент окончания сроллинга?
Метод onScrollChanged вызывается после каждого «проскролленого» пикселя, и если заставлять поток-парсер работать после каждого вызова, то, понятное дело, ничего хорошего из этого не выйдет. Поэтому делаем следующим образом:
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); }
То есть каждый раз при вызове метода выставляем таймер в 500 мс и, если в течении этого времени метод не вызывается, то уведомляем OnScrollStoppedListener о том, что скроллинг остановился. В моем случае интерфейс OnScrollStoppedListener реализует мой EditText.
Как не стартовать поток-парсер после каждого введенного символа?
См. предыдущий пункт.
На самом деле этот способ в данном случае далеко не идеален потому, что пользователю всегда придется ждать N-ое количество миллисекунд до начала процесса парсинга. По-хорошему тут нужна какая-то интеллектуальная система, которая будет понимать, когда пользователь просто медленно печатает, а когда он уже завершил некоторую операцию ( к примеру написал оператор echo).
Как понять, какой текст попадает в видимую область?
К сожалению, точно этого сделать нельзя, поэтому приходится делать примерно. Для начала после каждого изменения текста я вызываю следующий метод:
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); } }
То есть я получаю номер символа начала каждой строки. Затем, зная высоту экрана в пикселях, легко вычисляем номер первой и последней видимой строки:
int lineHeight = mEditText.getLineHeight(); int startLine = scrollY / lineHeight; // scrollY - то что присылает нам ScrollView int endLine = mEditText.startLine + viewHeight / lineHeight + 1; // viewHeight высота дисплея в пикселях
Имея эти данные, вы без труда найдете первый и последний видимый символ.
Зачем нужно заполнять список со спанами? Почему бы просто не посылать каждый спан в handler сразу после его создания?
Во-первых, тогда вы потеряете удобную возможность использовать несколько потоков для парсинга текста. В такой конфигурации вы, к примеру, можете на этапе вставки спана в список проверять его на наличие двойника в листе. Во-вторых, на мой взгляд, программист работает итеративно. То есть он сделал какое-то действие, а затем не секунду задумался. В этот-то момент и придет пачка спанов в наш ui поток и подсветит его на доли секунды. В обратном же случае спаны будут приходить постоянно, создавая микро тормоза UI.
Зачем нам постоянно спящий поток? Почему бы не использовать ThreadPool?
По идее, так должно быть немного лучше, но я не пробовал.
Я осветил общую структуру решения, и, на мой взгляд, неочевидные моменты. Надеюсь, это кому-нибудь пригодится. Спасибо.
ссылка на оригинал статьи http://habrahabr.ru/post/204248/
Добавить комментарий