- Android rich text: Overview
- Why do we need rich text?
- Let’s zoom out to a big picture
- Implementations
- Example 1
- Example 2
- Example 3
- A deeper look
- Span style
- 1. BackgroundColorSpan
- 2. RelativeSizeSpan
- 3. TypefaceSpan
- 4. StrikethroughSpan
- 5. UnderlineSpan
- 6. ImageSpan
- 7. SuperscriptSpan and SubscriptSpan
- 8. TextSurroundSpan
- 9. Custom span
- How it helps?
- Making it better
- Использование RichText в Android. Spannable
- Теория
- Практика
- Вместо заключения
- Как мы сделали Rich Text Editor с поддержкой совместного редактирования под Android
- Совместное редактирование на смартфоне — но зачем?
- Варианты реализации
- Отображение Rich Text в EditText
- Обработка пользовательских стилей
- Отслеживание изменений в документе
- О совместимости с разными версиями API
- Заключение
Android rich text: Overview
Jan 16, 2018 · 7 min read
Why do we need rich text?
While working on UI tasks, we may often encounter some complex texts with various fonts, colors, sty l es displayed inline. Our first reaction to this is use multiple TextViews to achieve — each TextView sets different styles. We end up adding more weights to the view hierarchy . It becomes challenging when trying to render rich content which contains complicated views — a lot of text and images in RecyclerView . Fortunately, Android has fairly extensive support for formatted text lying in those packages android.text. *; android.text.style. * However, some of this rich text support has been shrouded in mystery. This article will explain how the rich text support in Android and how you can leverage it.
You may have seen some Android APIs that return CharSequence, Spanned, SpannableString, SpannedString… Is it confusing enough?
Let’s zoom out to a big picture
Technically speaking, these classes are used instead of the regular String. They contain inline markup rules. These rules indicate how text should be rendered such as bold, italic, foreground, background color, font size, font style…
Implementations
Spanned: An interface marker for text that has markup objects attached to ranges of it.
Spannable: An interface for text to which markup objects can be attached and detached
SpannedString: A class for text whose content and markup are immutable. It means , you cannot change either the text or the formatting of a SpannedString .
SpannableString: A class for text whose content is immutable but to which markup objects can be attached and detached. Compared to its immutable counterpart, SpannableString only allows to modify its markup rules via setSpan() .
SpannableStringInternal: An implementation markup logic for both SpannableString and SpannedString
Editable: An interface for text whose content and markup can be changed.
SpannableStringBuilder: A class acting as a Builder implements both Editable and Spannable , allows to modify text and formatting at the same time.
In some respects, we can deem SpannableString like a String. SpannableStringBuilder works like StringBuilder since it can splices multiple String through append() method.
After Spannables are created, now we can setSpan() to achieve desired effects. Take a look at what Android provided us
Example 1
The effect is applied only for Hello because we exclude the previous and following characters around the word.
Example 2
The texts after Hello will also be affected by the flag SPAN_EXCLUSIVE_INCLUSIVE
Example 3
Markup object of World will override spanned1 property with a length of 3.
A deeper look
In its declaration, there are two internal arrays —a mSpans contains markup objects, meanwhile the other mSpanData holds markup rules matching to a style in mSpans array. The mSpanData array is depicted as an visual below
The mSpanData groups three variables start, end, flags. The array can be accessed by the offset corresponding to the property.
Span style
Span is divided into 4 major groups — CharacterStyle, ParagraphStyle, UpdateAppearance, UpdateLayout
- A Span inherits from CharacterStyle/ParagraphStyle affects character-level/paragraph-level format.
- A Span whose super class is UpdateAppearance should only modify text appearance. If a Span also impacts size or other metrics, it should instead implement UpdateLayout.
Let’s go through some examples
1. BackgroundColorSpan
2. RelativeSizeSpan
3. TypefaceSpan
4. StrikethroughSpan
5. UnderlineSpan
6. ImageSpan
7. SuperscriptSpan and SubscriptSpan
8. TextSurroundSpan
9. Custom span
Visit the source code here.
How it helps?
Hunt’s upcoming product, a product detail screen in the Hunt for Android app, is an extreme example.
These white blocks you see are messages delivered by the speaker after a slide in animation driven by LayoutAnimationController. Each message could be fairly tall, and contains several pieces of text, styled text (bold, italic), hyperlink, photo or video (actually a photo with a Youtube icon on top). This makes the task of binding all of the message’s data difficult. Our initial design was based on the following concept: For every piece of a message, we created a UI class according to its type.
So, this meant to display like the first photo we had a TextView , ImageView , then TextView added to a VerticalLinearLayout . This approach was simple and intuitive for finding the right piece of code or knowing where to add it, but had various drawbacks:
- Deep-View hierarchy: Logically grouping things meant putting them inside the same view which resulted in a complicated hierarchy of views.
- Boilerplate code: Spending much time on constructing Views dynamically, and binding logic
- Heavy-lifting method: bind method is supposed to bind data than doing other works. There is no way to wait until the last item to be bound before RecyclerView’s animation. On some low-end devices, it may cause jank during animation.
Making it better
Before trying to solve these problems, we took a step back and considered a another idea: Composing each component of a message of Span . This nice trick results in various advantages right away. Let’s take the above message from photo1 as an example:
LinearLayout(TextView + ImageView + TextView) is mapped to an equivalent UI component TextView(String + ImageSpan + String + UnderlineSpan + ForegroundSpan + String)
It’s time when the concept of Span become handy. It turned out that composing a complex Spanned can be easily done through HTML.fromhtml since Android did all the heavy lifting. The rest of the work is just map message’s data to HTML by HTMLBuilder.
This approach has resulted in a few benefits:
- We were able to simplify each item’s hierarchy to one TextView.
Источник
Использование RichText в Android. Spannable
Привет Хабраобщество! Эта статья об использовании Spannable в Android. Предназначен он для стилизации текста. Если Вас интересует как реализовать нечто подобное:
тогда добро пожаловать под кат. Статья ориентированная на разработчиков, у которых нет большого опыта разработки под Android.
Теория
Spannable — интерфейс, который описывает маркировку обычного текста некими объектами. Задание этих объектов заключается в присвоению части текста некоторого определенного стиля. Такими маркировочными объектами могут быть экземпляры классов, которые реализуют интерфейс ParcelableSpan. Добавление маркировки осуществляется методом:
Удаление, соответственно, методом:
Теории немного, перейдем сразу к практике.
Практика
Для освоения практики необходимо создать android проект или открыть уже существующий. Я создал чистый проект, на главную activity разместил один TextView. Идем в метод, где будем инициализировать наш TextView (у меня onCreate) и добавляем следующий текст:
Итак, разберемся что же мы написали. SpannableString — класс, который реализует интерфейс Spannable (с другими реализациями можно ознакомится на сайте официальной документации). UnderlineSpan — реализация ParcelableSpan, маркирует часть текста как подчеркнутый (в нашем примере это с 8-ой по 17-тую букву). Флаг Spanned.SPAN_EXCLUSIVE_EXCLUSIVE обозначает, что наш span не будет расширятся на вставки текста слева или справа от маркированной части. (Со всеми флагами можно ознакомится на официальном сайте, при работе с текстами readonly они не столь важны).
Также здесь ми использовали еще одну реализацию ParcelableSpan — StyleSpan. Как вы уже догадались, с его помощью можно маркировать текст как полужирный (Typeface.BOLD) или курсив (Typeface.ITALIC).
С кодом ми разобрались, запускаем наш проект и смотрим на результат. Текст должен выглядеть следующим образом:
Хорошо, идем дальше: добавим на наше activity кнопку, а в инициализирующий метод следующий текст:
На этот раз не будем разбирать код, ничего нового уже для нас нет. Мы использовали маркировку курсивом, которую я описал выше. Запускаем, смотрим.
Мы смогли вывести стилизованный текст на кнопку.
Пойдем еще дальше, реализуем обработчик события клика кнопки, в нем пишем следующий код:
Здесь у нас появляется новый класс — ForegroundColorSpan, который задает цвет нашему тексту, в примере мы задали зеленой. Мы его используем в паре с StyleSpan. Запускаем приложение, смотрим на результат.
Мы смогли запихнуть стилизованный текст даже в Toast. Также мы показали, что для одной части текста можно использовать несколько маркировок со стилями.
Вместо заключения
В примере мы использовали только некоторые из доступных стилей, больший список можно посмотреть на сайте разработчиков. Думаю вы сможете поэкспериментировать с ними.
Целью статьи было в доступной форме показать столь сильный и простой инструмент для стилизации своего приложения разработчикам, которые не знали о его существовании или просто не использовали его. Надеюсь что статья будет полезна.
Источник
Как мы сделали Rich Text Editor с поддержкой совместного редактирования под Android
«Мобилизация» рабочих процессов в компаниях означает, что на телефон или планшет переносится все больше функций для совместной работы. Для Wrike, как кроссплатформенного сервиса управления проектами, важно, чтобы функционал мобильного приложения был абсолютно полноценным, удобным и не ограничивал пользователей в работе. И когда встала задача создать Rich Text Editor с поддержкой совместного редактирования описания задач, мы, оценив возможности существующих WebView компонентов, решили пойти своим путем и реализовали собственный нативный инструмент.
Для начала немного об истории продукта. Одной из базовых функций Wrike изначально была интеграция с почтой. С самой первой версии задачи можно было создавать и обновлять через e-mail, а затем работать над ними совместно с другими сотрудниками. Тело письма превращалось в описание задачи, а все дальнейшее обсуждение шло в комментариях к ней.
Поскольку в почте можно использовать HTML форматирование, в ранних версиях продукта мы использовали CKEditor для дальнейшей работы с описанием задачи. Но в среде, ориентированной на совместную работу, это очень неудобно – необходимо блокировать весь документ или его часть, чтобы подготовленное описание задачи не затер кто-то другой. В итоге мы решили углубиться в практику Operation Transformation (OT) и сделать инструмент для настоящей совместной работы. В этой статье я не буду подробно рассматривать теорию и реализацию OT для rich text документов, об этом есть уже достаточно материалов. Я рассмотрю лишь сложности, с которыми столкнулась наша команда при разработке мобильного приложения.
Совместное редактирование на смартфоне — но зачем?
Возможно и незачем, если, конечно, это не является ключевой функцией вашего продукта. Помимо общей цели обеспечить максимум базовой функциональности на всех платформах, был ряд более конкретных причин, по которым нам пришлось об этом задуматься:
- Реализация OT требует хранить документ в определенном формате, поддерживающим совместное редактирование. В случае простого текста особого формата тут нет — это может быть просто строка. Но в случае с Rich Text (текста с форматированием), формат хранения становится сложнее.
- Нам нужен способ сохранять изменения, сделанные мобильным клиентом, не сломав документ и не создав конфликт с изменениями, которые могли внести в тот же промежуток времени другие пользователи. Это задачи, которые как раз и решаются алгоритмами OT.
- Раз нам нужно перенести алгоритм OT на мобильную платформу, чтобы выполнить условия из пункта 2, то сделать полноценное совместное редактирование уже не требует значительных дополнительных усилий.
Итак, у нас есть rich text описание задачи как базовый функционал, необходимость поддерживать специфичный формат документа и протокола синхронизации, поэтому возьмемся за поиск решения.
Варианты реализации
С реализацией компонента для совместной работы опыт уже был, а вот с тем, как перенести его на Android, предстояло разобраться. Многое зависело от требований к редактору и их, по большому счету, было два:
- Поддержка базового форматирования, списков, вставка картинок и таблиц,
- API, позволяющий вносить и отслеживать изменения как в самом тексте, так и в его форматировании.
Способ 1: использовать существующий компонент из Web продукта
Действительно, мы могли бы использовать компонент, который у нас уже есть, и обернуть его в WebView. Из плюсов — простота интеграции, так как фактически весь код редактора находится в скриптах, и Android/iOS разработчику остается только реализовать WebView wrapper.
Достаточно быстро стало понятно, что существующий компонент из основного приложения, работающий с ContentEditable документом, функционирует весьма нестабильно в зависимости от версии ОС и от вендора. Экзотичность багов местами зашкаливала, но в основном они всплывали вокруг функций выделения и ввода текста, а также пропадающего фокуса и клавиатуры.
Чтобы обойти проблемы ContentEditable, мы пробовали использовать CodeMirror как фронтенд для редактора, при этом он заметно лучше и стабильнее работает на Android, поскольку обрабатывает все события от клавиатуры и отрисовку самостоятельно. Были, конечно, и минусы, но как быстрый workaround он работал очень неплохо до тех пор, пока не появилось небезызвестное изменение в обработке событий нажатия клавиш в IME — довольно подробно эта проблема обсуждается здесь. Если в двух словах — при использовании LatinIME, он не отправляет событие для KEYCODE_DEL.
Что это значит для пользователя? При нажатии на Delete ничего не происходит, то есть редактор работает корректно, можно вводить текст, применять форматирование… вот только текст нельзя удалить, как бы это абсурдно ни звучало. Единственный вариант решения данной проблемы помимо всего прочего включал в себя следующий код:
InputType.TYPE_NULL при этом переводил IME в «упрощенный» вид, сигнализируя, что InputConnection работает в ограниченном режиме, что означает отсутствие copy/paste, autocorrect/autocomplete, а также ввода текста с помощью жестов, но при этом он позволяет обрабатывать все события клавиатуры.
В итоге, в последней реализации редактора, который использовал веб-интерфейс, были следующие недостатки:
- медленная скорость загрузки;
- отсутствие доступа к расширенным возможностям IME (copy/paste, autocomplete/autocorrect, gesture input);
- в некоторых случаях нестабильная работа, в связи с различной реализацией WebView на разных версиях API и модификации этого компонента некоторыми вендорами;
- обычно WebView долго не держится в памяти, особенно на девайсах с небольшим объемом памяти, и, если свернуть приложение и через некоторое время запустить заново, то в большинстве случаев WebView придется снова инициализировать;
- многочисленные костыли в коде, число которых со временем только увеличивалось.
Осознав, что поддерживать подобную имплементацию редактора непросто, и учитывая описанные недостатки и ограничения, было решено разработать нативный компонент, который бы давал возможность работать с форматированным текстом.
Способ 2: нативная реализация
Для нативной реализации необходимо решить две задачи:
- UI редактор, то есть отображение текста с учетом форматирования и его редактирование.
- Работа с форматом документа, отслеживание изменений, а также обмен данными с сервером.
Для того, чтобы решить первую задачу, не нужно изобретать колесо — Android предоставляет необходимые инструменты, а именно компонент EditText и интерфейс Spannable, описывающий маркировку текста.
Вторая задача решается переносом алгоритмов OT из JavaScript на Java, и процесс здесь достаточно прозрачен.
Отображение Rich Text в EditText
В Android есть замечательный интерфейс Spannable, который позволяет задать разметку текста. Сам процесс формирования разметки довольно прост — нужно воспользоваться специальным классом SpannableStringBuilder, который позволяет как задавать/изменять текст, так и устанавливать стили для заданных участков текста через метод
Первый параметр как раз задает стиль. Он должен быть экземпляром класса, который реализует один или несколько интерфейсов из пакета android.text.style: CharacterStyle, UpdateAppearance, UpdateLayout, ParagraphStyle и т.д. Набор дефолтных стилей довольно широк — от изменения формата символов (StyleSpan, UnderlineSpan), задания размера текста (RelativeSizeSpan) и изменения его положения (AlignmentSpan) до поддержки изображений (ImageSpan) и кликабельного текста (ClickableSpan).
Последний параметр задает флаги, о роли которых будет рассказано чуть ниже. Например, вот так можно поменять цвет всего текста:
Итак, на входе есть текст в неком формате, а на выходе нужно получить его представление в виде Spannable объекта и передать его в EditText. В нашем случае с сервера документ приходит в особом формате в виде атрибутированной строки – необходимо распарсить эту строку, используя нашу библиотеку для OT, и применить атрибуты к заданным участкам текста. В зависимости от стиля, нужно выставить корректный флаг, чтобы маркировка текста соответствовала ожиданиям пользователя.
Если пометить стиль флагом SPAN_EXCLUSIVE_INCLUSIVE, то он будет применен к введенному в конце интервала тексту, но не будет применяться в начале. Например, есть интервал [10, 20], для которого выставлен стиль UnderlineSpan + SPAN_EXCLUSIVE_INCLUSIVE. В этом случае при вводе текста в позицию 9, к нему стиль UnderlineSpan применяться не будет, но если начать вводить текст в позиции 20, то интервал, который покрывает стиль, расширится и станет [10, 21]. Естественно, это полезно для inline форматирования (bold / italic / underline и т.п.).
При использовании флага SPAN_EXCLUSIVE_EXCLUSIVE, интервал стиля ограничивается с обоих концов. Это подходит, например, для ссылок — если начать вставлять текст сразу после ссылки, то стиль ссылки к нему применяться не должен.
Используя флаги SPAN_EXLUSIVE_INCLUSIVE и SPAN_EXCLUSIVE_EXCLUSIVE можно управлять поведением форматирования при вводе текста в зависимости от ожиданий пользователя. Например, если вы включили режим форматирования Bold, то вводимый текст должен оставаться жирным. А если вы сделали ссылку, то дописывание текста в конце не должно расширять границы ссылки.
Для отображения элементов списка можно воспользоваться BulletSpan, но он подойдет только для ненумерованных списков. Если же необходима нумерация, то можно написать свой класс, реализующий интерфейсы LeadingMarginSpan и UpdateAppearance, отрисовывая индикатор списка на свое усмотрение в методе drawLeadingMargin.
Обработка пользовательских стилей
Понятно, что редактор должен давать пользователю возможность применять форматирование, это включает:
- Добавление нового стиля к выбранному тексту,
- Вставку нового стиля в позиции курсора,
- Применение текущего стиля при редактировании.
В первую очередь нужно где-то разместить кнопки для поддерживаемых редактором стилей. Помещать их в тулбаре Activity было не практично до выхода Android Marshmallow. По умолчанию этот же тулбар используется для контекстного меню при выделении текста, и таким образом выбрать стиль для выбранного текста невозможно. Поэтому можно поместить их на тулбар внизу экрана. При нажатии на кнопку стиля необходимо определиться с текущим состоянием редактора и либо применить стиль к выбранному тексту, либо запомнить этот стиль как временный в позиции курсора.
mTempAttributes — экземпляр класса TempAttributes. Он определяет набор атрибутов в данной позиции, выбранных пользователем. Эта переменная обнуляется либо после использования, либо при смене позиции курсора.
Если пользователь нажимает на кнопку, соответствующую некоторому стилю, на тулбаре, но при этом никакой текст не выбран, в этом случае нужно сохранить этот стиль как «временный» в текущей позиции курсора и применить его при вводе текста в этой позиции. Подробнее об этом чуть ниже.
Когда текст был выбран, нужно определить, есть ли уже этот стиль в выбранном интервале или нет. Если нет или есть частично, то необходимо объединить все существующие span’ы и покрыть интервал этим стилем полностью. Если же есть, то удалить соответствующие span’ы из интервала, при необходимости разбив его.
Пример 1
Есть текст: Quick brown fox.
В нем 2 span-а: bold [0,4] и bold [12,14]. Если пользователь выделяет весь текст и применяет к нему стиль bold, то в итоге он должен покрывать весь интервал. Для этого можно либо удалить оба span’а и добавить новый bold [0, 14], либо удалить второй и продлить первый до конца интервала.
Пример 2
Есть текст: Quick brown fox.
В нем один span: bold [0, 14]. Если пользователь выделяет текст [4, 12] и выбирает стиль bold в тулбаре, то стиль нужно удалить из интервала, так как он полностью присутствует в выделении. Для этого нужно разбить интервал на две части: укоротить весь интервал [0, 14] до начала выделения ([0, 4]) и добавить новый интервал от конца выделения до конца текста ([4, 12]).
Отслеживание изменений в документе
Чтобы корректно отслеживать изменения пользователя и «скармливать» их алгоритму OT, редактор должен уметь их отслеживать. Для этого используется интерфейс TextWatcher — каждый раз, когда в EditText происходят какие-то изменения, последовательно вызываются методы beforeTextChanged, onTextChanged и afterTextChanged этого интерфейса, позволяя определить, что и где изменилось.
Важно учесть, что при первоначальной установке текста в редактор через setText(CharSequence), TextWatcher также получит уведомление об этом, поэтому программная установка текста оборачивается в:
В переменных mOldStr и mNewStr хранятся старая строка и новая строка соответственно, mCurrentPos указывает на позицию, начиная с которой произошли изменения. Например, если пользователь добавил символ «a» в позиции 10, то
Однако есть небольшой нюанс — при вставке текста из-за автокоррекции эти значения могут включать начало слова. Например, если текст начинается со слова «Text», и пользователь заменяет третий символ на «s», то IME может рапортовать это изменение как:
В этом случае нужно отрезать одинаковые последовательности символов от начала строки.
В конечном итоге, используя TextWatcher, можно однозначно определить, что конкретно произошло — был текст заменен, удален или добавлен. Если пользователь добавляет текст в позиции или заменяет часть имеющегося текста на текст из буфера, необходимо применить к добавленному тексту те атрибуты, которые находятся в позиции курсора. Для этого нужно найти все Spannable объекты в позиции курсора, при этом не забыв исключить те, которые стали пустыми (s.getSpanStart(span) == s.getSpanEnd(span)), удалив при этом сами объекты Spannable и отфильтровав только по inline атрибутам (bold, italic, etc.). Дополнительно добавляются те атрибуты, которым соответствуют стили, выбранные пользователем на тулбаре (mTempAttributes).
В итоге имеется позиция, в которой произошли изменения, известны старый и новый тексты в этой позиции, а также inline атрибуты, которые необходимо применить к новому тексту. После этого можно добавить дополнительную обработку. Например, если пользователь вставил перевод строки в конце последнего элемента списка, можно вставить новый элемент списка в текущей позиции курсора, чтобы продолжить список. В конечном итоге по этим данным составляется список изменений и отправляется на сервер.
Стоит заметить, что при отслеживании изменений в редакторе хорошей практикой будет использование оберток для всех дефолтных стилей. Например, вместо UnderlineSpan использовать класс CustomUnderlineSpan, который наследуется от UnderlineSpan, но при этом никакие методы в нем не переопределены. Такой подход позволит по классу однозначно отделить “свои” стили от тех, которые применяет EditText. Например, если включена поддержка автозамены, то при редактировании слова EditText добавляет ему стиль UnderlineSpan, и визуально слово подчеркивается на момент редактирования.
О совместимости с разными версиями API
На версиях API до Android KitKat существует проблема с наложением Spannable текста при редактировании. Она решается отключением аппаратного ускорения TextView (возможно, есть другие способы это исправить — предложения в комментариях горячо приветствуются):
Однако в таком виде TextView нельзя поместить в ScrollView, так как вся View будет рендериться в памяти («View too large to fit into drawing cache»), поэтому нужно включать прокрутку в самом TextView.
Заключение
Намучавшись с реализацией редактора на webview и осознав тупиковость данного подхода, нам удалось разработать нативный компонент, который решает непростую, но довольно интересную задачу совместного редактирования текста. Это позволило улучшить юзабилити приложения и повысить продуктивность наших пользователей. Получившийся результат можно оценить, скачав наше приложение из Google Play.
Источник