Как стать автором
Поиск
Написать публикацию
Обновить

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

Публикации

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