...

вторник, 3 декабря 2013 г.

Подсветка кода на 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?




По идее, так должно быть немного лучше, но я не пробовал.

Я осветил общую структуру решения, и, на мой взгляд, неочевидные моменты. Надеюсь, это кому-нибудь пригодится. Спасибо.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий