Недостаточные пролеты
Интервалы - это мощные концепции, которые позволяют стилизовать текст на уровне символа или абзаца, предоставляя доступ к таким компонентам, как TextPaint и Canvas. В предыдущей статье мы говорили о том, как использовать Spans, какие Spans предоставляются «из коробки», как легко создавать свои собственные и как их тестировать.
Давайте посмотрим, какие API можно использовать для обеспечения максимальной производительности в конкретных случаях использования при работе с текстом. Мы собираемся подробнее изучить, что происходит под капотом с промежутками и как их использует фреймворк. В конце концов, мы увидим, как мы можем передавать промежутки в одном и том же процессе или в промежуточных процессах, и, исходя из этого, какие предостережения необходимо учитывать при принятии решения о создании собственных пользовательских промежутков.
Под капотом: как работают пролеты
Платформа Android обрабатывает стили текста и работает с промежутками в нескольких классах: TextView
, EditText
, классы макета (Layout
, StaticLayout
, DynamicLayout
) и TextLine
(частный класс пакета, используемый в Layout
), и это зависит от нескольких параметров:
- Тип текста: выбираемый, редактируемый или невыбираемый
BufferType
LayoutParams
типаTextView
- так далее
Платформа проверяет, содержат ли Spanned
объекты экземпляры разных диапазонов платформы, и запускает различные действия.
Логика разметки и рисования текста сложна и распространяется на разные классы; В этом разделе мы можем представить только упрощенное представление о том, как обрабатывается текст, и только для некоторых случаев.
Каждый раз, когда диапазон изменяется, TextView.spanChange
проверяет, является ли диапазон экземпляром UpdateAppearance
, ParagraphStyle
или CharacterStyle
, и, если да, аннулирует себя, инициируя новое рисование вида.
Класс TextLine
представляет строку стилизованного текста и работает специально с интервалами, которые расширяют CharacterStyle
, MetricAffectingSpan
и ReplacementSpan
. Это класс, запускающий MetricAffectingSpan.updateMeasureState
и CharacterStyle.updateDrawState
.
Базовым классом, который управляет расположением текста в визуальных элементах на экране, является android.text.Layout
. Layout
вместе с двумя его подклассами, StaticLayout
и DynamicLayout
, проверяют интервалы, заданные в тексте, для вычисления высоты строки и поля макета. Помимо этого, всякий раз, когда диапазон, отображаемый в DynamicLayout
, обновляется, макет проверяет, является ли диапазон диапазоном UpdateLayout
, и генерирует новый макет для затронутого текста.
Настройка текста для максимальной производительности
Есть несколько способов установки текста в TextView
с эффективным использованием памяти, в зависимости от ваших потребностей.
1. Текст, установленный на TextView
, никогда не меняется.
Если вы просто установите текст в TextView один раз и никогда не обновите его, вы можете просто создать новый экземпляр SpannableString
или SpannableStringBuilder
, установить необходимые промежутки и затем вызвать textView.setText(spannable)
. Поскольку вы больше не работаете с текстом, производительность больше не требуется.
2. Изменения стиля текста путем добавления / удаления промежутков.
Давайте рассмотрим случай, когда текст не меняется, но изменяются прикрепленные к нему интервалы. Например, предположим, что всякий раз, когда нажимается кнопка, вы хотите, чтобы слово в тексте становилось серым. Итак, нам нужно добавить к тексту новый диапазон. Для этого, скорее всего, у вас возникнет соблазн дважды позвонить textView.setText(CharSequence)
: сначала для установки исходного текста, а затем еще раз при нажатии кнопки. Более оптимальным решением было бы вызвать textView.setText(CharSequence, BufferType)
и просто обновить интервалы объекта Spannable
при нажатии кнопки.
Вот что происходит с каждым из этих вариантов:
Вариант 1. Вызов textView.setText (CharSequence) несколько раз - неоптимально
При вызове textView.setText(CharSequence)
под капотом TextView
создает копию вашего Spannable
как SpannedString
и сохраняет ее в памяти как aCharSequence
. Следствием этого является то, что ваш текст и интервалы неизменны. Итак, когда вам нужно обновить текстовый стиль, вам нужно будет создать новый Spannable
с текстом и промежутками, снова вызвать textView.setText
, который, в свою очередь, создаст новую копию объекта.
Вариант 2. Вызов textView.setText (CharSequence, BufferType) один раз и обновление объекта Spannable - оптимальный вариант
При вызове textView.setText(CharSequence, BufferType)
параметр BufferType
сообщает TextView
, какой тип текста установлен: статический (тип по умолчанию при вызове textView.setText(CharSequence)
), стилизованный / составной текст или редактируемый (который будет использоваться EditText
).
Поскольку мы работаем с текстом, который можно стилизовать, мы можем вызвать:
textView.setText(spannableObject, BufferType.SPANNABLE)
В этом случае TextView
больше не создает SpannedString
, но создаст SpannableString
с помощью объекта-члена типа Spannable.Factory
. Поэтому теперь копия CharSequence
, хранящаяся в TextView
, имеет изменяемую разметку и неизменяемый текст.
Чтобы обновить диапазон, мы сначала получаем текст как Spannable
, а затем обновляем промежутки по мере необходимости.
// if setText was called with BufferType.SPANNABLE textView.setText(spannable, BufferType.SPANNABLE) // the text can be cast to Spannable val spannableText = textView.text as Spannable // now we can set or remove spans spannableText.setSpan( ForegroundColorSpan(color), 8, spannableText.length, SPAN_INCLUSIVE_INCLUSIVE)
С помощью этой опции мы создаем только начальный объект Spannable
. TextView
будет содержать его копию, но когда нам нужно ее изменить, нам не нужно создавать какие-либо другие объекты, потому что мы будем работать напрямую с экземпляром текста Spannable
, который хранится в TextView
. Однако TextView
будет проинформирован только о добавлении / удалении / изменении положения пролетов. Если вы измените внутренний атрибут диапазона, вам нужно будет вызвать invalidate()
или requestLayout()
, в зависимости от типа изменения. См. Подробности в «Совете по бонусной производительности» ниже.
3. Изменения текста (повторное использование TextView)
Допустим, мы хотим повторно использовать TextView
и задать текст несколько раз, как в RecyclerView.ViewHolder
. По умолчанию, независимо от набора BufferType
, TextView
создает копию объекта CharSequence
и сохраняет ее в памяти. Это гарантирует, что все TextView
обновления являются преднамеренными, а не случайными, когда разработчик изменяет значение CharSequence
по другим причинам.
В варианте 2 выше мы видели, что при установке текста с помощью textView.setText(spannableObject, BufferType.SPANNABLE)
TextView
копирует CharSequence
, создавая новый SpannableString
с помощью экземпляра Spannable.Factory
. Итак, каждый раз, когда мы устанавливаем новый текст, он создает новый объект. Если вы хотите получить больший контроль над этим процессом и избежать создания дополнительных объектов, реализуйте свой собственный Spannable.Factory
, переопределите newSpannable (CharSequence) и установите для фабрики значение TextView
.
В нашей собственной реализации мы хотим избежать создания этого нового объекта, поэтому мы можем просто вернуть преобразование CharSequence
как Spannable
. Имейте в виду, что для этого необходимо вызвать textView.setText(spannableObject, BufferType.SPANNABLE)
, иначе источник CharSequence
будет экземпляром Spanned
, который не может быть преобразован в Spannable
, что приведет к ClassCastException
.
val spannableFactory = object : Spannable.Factory() { override fun newSpannable(source: CharSequence?): Spannable { return source as Spannable } }
Установите объект Spannable.Factory один раз сразу после того, как получите ссылку на свой TextView
. Если вы используете RecyclerView
, сделайте это, когда впервые увеличите количество просмотров.
textView.setSpannableFactory(spannableFactory)
Благодаря этому вы избегаете создания лишних объектов каждый раз, когда RecyclerView
привязывает новый элемент к ViewHolder
.
Чтобы добиться еще большей производительности при работе с текстом и RecyclerViews
, вместо создания объекта Spannable
из String
в ViewHolder
сделайте это перед передачей списка в Adapter
. Это позволяет вам создавать Spannable
объекты в фоновом потоке вместе с любой другой работой, которую вы выполняете с элементами списка. Тогда ваш Adapter
может сохранить ссылку на List<Spannable>
.
Совет по бонусной производительности
Если вам нужно изменить только внутренний атрибут диапазона (например, цвет маркера для настраиваемого диапазона маркеров), вам не нужно снова вызывать TextView.setText
, а просто invalidate()
или requestLayout()
. Повторный вызов setText
приведет к запуску ненужной логики и созданию объектов, когда представление нужно просто перерисовать или повторно измерить.
Что вам нужно сделать, так это сохранить ссылку на изменяемый диапазон и, в зависимости от того, какое свойство вы изменили в представлении, вызвать:
TextView.invalidate()
если вы просто меняете внешний вид текста, чтобы вызвать перерисовку и пропустить повторную раскладку.TextView.requestLayout()
, если вы внесли изменение, которое влияет на размер текста, чтобы вид мог позаботиться об измерении, компоновке и рисовании.
Допустим, у вас есть собственная реализация маркированного списка, в которой цвет маркера по умолчанию - красный. Каждый раз, когда вы нажимаете кнопку, вы хотите изменить цвет маркера на серый. Реализация будет выглядеть так:
class MainActivity : AppCompatActivity() { // keeping the span as a field val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) { … val spannable = SpannableString(“Text is spantastic”) // setting the span to the bulletSpan field spannable.setSpan( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE) styledText.setText(spannable) button.setOnClickListener( { // change the color of our mutable span bulletSpan.color = Color.GRAY // color won’t be changed until invalidate is called styledText.invalidate() } }
Под капотом: передача текста с интервалами внутри и между процессами
TL;DR:
Настраиваемые атрибуты диапазона не будут использоваться, когда составные объекты передаются внутри или между процессами. Если желаемый стиль может быть достигнут только с помощью промежутков фреймворка, предпочтительнее применять несколько промежутков фреймворка реализации собственных промежутков. В противном случае предпочтите реализацию настраиваемых диапазонов, расширяющих некоторые из базовых интерфейсов или абстрактных классов.
В Android текст может передаваться в одном и том же процессе (внутри процесса), например, из одного действия в другое через намерения, а также между процессами (между процессами), когда текст копируется из одного приложения в другое.
Реализации пользовательского диапазона не могут переходить границы процессов, поскольку другие процессы не знают о них и не знают, как с ними обращаться. Интервалы фреймворка Android являются глобальными объектами, но внутри и между процессами можно передавать только те диапазоны, которые простираются от ParcelableSpan
. Эта функция позволяет разделять и распаковывать все свойства диапазона, определенного в фреймворке. Метод TextUtils.writeToParcel
отвечает за сохранение информации о промежутках в Parcel
.
Например, вы можете передавать интервалы в том же процессе между Activities
с помощью намерения:
// start Activity with text with spans val intent = Intent(this, MainActivity::class.java) intent.putExtra(TEXT_EXTRA, mySpannableString) startActivity(intent) // read text with Spans val intentCharSequence = intent.getCharSequenceExtra(TEXT_EXTRA)
Таким образом, даже если вы передаете промежутки в одном и том же процессе, только структура
ParcelableSpans
выживает при прохождении через намерение.
ParcelableSpans
также позволяет копировать текст вместе с промежутками от одного процесса к другому. Процесс копирования и вставки текста проходит через ClipboardService
, который под капотом использует тот же метод TextUtil.writeToParcel
. Таким образом, даже если вы копируете промежутки из своего приложения и вставляете их в одно и то же приложение, это межпроцессное действие и требует разделения, поскольку текст проходит через ClipboardService
.
По умолчанию любой класс, реализующий Parcelable
, может быть записан и восстановлен из Parcel
. При передаче объекта Parcelable
между процессами единственными классами, которые гарантированно восстанавливаются правильно, являются классы каркаса. Если процесс, который пытается восстановить данные из Parcel
, не может создать объект, потому что тип данных определен в другом приложении, процесс завершится аварийно.
Здесь есть два больших предостережения:
- Когда текст с диапазоном передается либо в том же процессе, либо между процессами, сохраняются только
ParcelableSpans
ссылки на фреймворк. Как следствие, стили нестандартных пролетов не распространяются. - Вы не можете создать свой собственный
ParcelableSpan
. Чтобы избежать сбоев из-за неизвестных типов данных, фреймворк не позволяет реализовать пользовательскийParcelableSpan
, определяя два метода,getSpanTypeIdInternal
иwriteToParcelInternal
, как скрытые. Оба они используютсяTextUtils.writeToParcel
.
Предположим, вы хотите определить диапазон маркеров, который позволяет настраивать размер маркера, поскольку существующий BulletSpan
определяет фиксированный размер радиуса 4 пикселя. Вот как это можно реализовать и каковы последствия каждого из них:
- Создайте
CustomBulletSpan
, который расширяетBulletSpan
, но также позволяет установить параметр для размера маркера. Когда диапазон передается либо от одного действия к другому, либо путем копирования текста, диапазон, прикрепленный к тексту, будетBulletSpan
. Это означает, что при отрисовке текста он будет иметь радиус маркера по умолчанию, а не установленный вCustomBulletSpan
. - Создайте
CustomBulletSpan
, который просто является продолжениемLeadingMarginSpan
и повторно реализует функциональность маркированного списка. Когда диапазон передается либо от одногоActivity
к другому, либо путем копирования текста, диапазон, прикрепленный к тексту, будетLeadingMarginSpan
. Это означает, что когда текст будет нарисован, он потеряет все стили.
Если желаемый стиль может быть достигнут только с помощью промежутков фреймворка, предпочтительнее применять несколько диапазонов фреймворка, а не реализовывать свой собственный. В противном случае предпочтите реализацию настраиваемых диапазонов, расширяющих некоторые базовые интерфейсы или абстрактные классы. Таким образом, вы можете избежать применения реализации фреймворка к spannable, когда объект передается внутри или между процессами.
Понимая, как Android отображает текст с помощью промежутков, мы надеемся, что вы сможете эффективно и результативно использовать его в своем приложении. В следующий раз, когда вам понадобится стилизовать текст, решите, следует ли вам применить несколько диапазонов фреймворка или создать свой собственный диапазон, в зависимости от дальнейшей работы, которую вы выполняете с этим текстом.
Работа с текстом в Android - настолько распространенная задача, что вызов правильного TextView.setText
метода может помочь вам уменьшить использование памяти вашим приложением и повысить его производительность.
Большое спасибо Сиямед Синир, Кларе Баярри и Нику Бутчеру.