Как стать автором
Обновить

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

Время на прочтение4 мин
Количество просмотров17K

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


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

Я осветил общую структуру решения, и, на мой взгляд, неочевидные моменты. Надеюсь, это кому-нибудь пригодится. Спасибо.
Теги:
Хабы:
Всего голосов 27: ↑27 и ↓0+27
Комментарии14

Публикации

Истории

Работа

Java разработчик
341 вакансия

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань