Подсветка кода на android. Мой опыт

от автора


Во время разработки моего последнего приложения мне пришлось провести довольно много времени, экспериментируя с разными подходами к размещению 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *