Неудивительно, как часто мы используем TextViews в Android (конечно, в проектах XML), и также так часто их стилизуют с помощью цветов, шрифтов, размеров и т. д. Мы можем добиться этого, используя стили и темы, как мы знаем. Но что, если вы хотите изменить только часть текста в TextView? Например, вы хотите изменить цвет подстроки «Hello» в тексте «Hello World» на красный, а цвет подстроки «World» на синий.

Вот где SpannableString пригодится. Он позволяет стилизовать только часть текста, используя интервалы и не только цвета, но также размеры, шрифты и даже события кликов. Spans и SpannableString не новы для Android. Они существуют с API Level 1. Более того, для простоты я не буду вдаваться в подробности о них, но подробнее о них можно прочитать здесь. В этой статье я расскажу о проблемах, с которыми столкнулся при использовании спанов, и покажу вам простое их решение.

Проблема

Проблемы, с которыми я столкнулся с SpannableString:

- шаблонный код

- трудно читать

- управление индексами диапазона (особенно в многоязычных текстах)

Позвольте мне объяснить их более подробно. Как вы, возможно, знаете, библиотека span написана на Java, поэтому она содержит функции Java и не использует возможности Kotlin (по умолчанию). Я не говорю, что это фигня, но я думаю, что это может быть лучше. Его можно улучшить с помощью функций Kotlin. Например, если вы хотите создать прослушиватель кликов для диапазона или части вашего текста в используемом вами текстовом представлении, вам нужно будет написать что-то вроде этого:

val spannable = SpannableString("")
spannable.setSpan(
  object : ClickableSpan() {
      override fun onClick(widget: View) {
          // do something
      }
  },
  /*start index*/,
  /*end index*/,
  SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
)

Этот код работает нормально, но для него требуются точные индексы диапазона, для которого вы хотите установить прослушиватель кликов. Это становится проблемой, когда ваше приложение является многоязычным, поскольку вам нужно будет рассчитать индексы для каждого языка. Помимо того, что это требует усилий каждый раз, когда вы хотите добавить новый язык, также легко забыть добавить индексы для нового языка. Более того, о возможности kotlin SAM и ее преимуществах я писал в одной из своих прошлых статей (можно прочитать здесь). И ясно, что приведенный выше код можно улучшить с помощью функции Kotlin SAM.

Другой вопрос, что читать тяжело. Код, который вы пишете, становится слишком длинным и трудным для чтения слишком быстро из-за особенностей библиотеки. Это особенно верно, когда вы хотите установить несколько диапазонов для одного текста. Например, если вы хотите установить прослушиватель кликов для диапазона и изменить цвет другого диапазона, вам нужно будет написать что-то вроде этого:

val spannable = SpannableString("")
spannable.setSpan(
    object : ClickableSpan() {
        override fun onClick(widget: View) {
            // do something
        }
    },
    /*start index*/,
    /*end index*/,
    SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
)
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    /*start index*/,
    /*end index*/,
    SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
)

Просто представьте случай, когда вы хотите подчеркнуть, изменить цвет и установить прослушиватель кликов для диапазона. Будет кошмаром читать и помнить, что вам нужно отслеживать индексы для каждого диапазона. Я думаю, что для такой простой задачи все стало немного грязно.

Решение

Jetpack compose также имеет функцию переноса текста, которая просто решает все проблемы, о которых я упоминал выше. давайте рассмотрим, как это работает, не вдаваясь в подробности.

Text(
    text = buildAnnotatedString {
        withStyle(style = SpanStyle(Color.Blue)) {
            append("Hello")
        }
        append(" World")
    },
    modifier = Modifier.clickable {
        // do something
    }
)

Как видите, нет индексов, которые нужно поддерживать, нет шаблонного кода, и его так легко читать. А также не нужно запоминать классы span, такие как ForegroundColorSpan, ClickableSpan и т. д. Вы можете просто использовать класс SpanStyle и установить цвет, шрифт, размер и т. д.

Конечно, вы можете использовать функцию компоновки по охвату текста и в своих xml-проектах. Но вам нужно добавить compose в свой проект. Но мы можем использовать аналогичный подход для функции компоновки по охвату текста в наших проектах xml без добавления компоновки в наш проект. Давайте посмотрим, как мы можем это сделать.

Реализация

Я хочу добавить функцию kotlin DSL для охвата и добиться чего-то вроде этого:

textView.span(/* substring to span */) {
    onClick {/* do something */}
    foreground(/* color */)
    underline()
    background( /* color */)
    insert("insertText", /* index */)
}

который просто берет подстроку для охвата и лямбда-функцию для изменения параметров охвата.

Прежде всего, нам нужно определить функцию расширения под названием `span` для класса TextView.

fun TextView.span(
    spanSubString: String,
    builderBlock: SpanBuilder.() -> Unit,
) {
    if(spanSubString.isEmpty()) {
        Log.d(TAG, "span: Spannable string cannot be empty")
        return
    }
    if(text.isEmpty()) {
        Log.d(TAG, "span: Text cannot be empty")
        return
    }
    if(!text.contains(spanSubString)) {
        Log.d(TAG, "span: Text must contain the spannable string")
        return
    }
    val spannableString = SpannableString(text)
    val textOfView = text.toString()
    val rangeOfSpanText = textOfView.indexRangeOf(spanSubString)
    rangeOfSpanText?.let {
        val spanBuilder = SpanBuilder(textView = this)
        spanBuilder.range = rangeOfSpanText
        spanBuilder.spannableString = spannableString
        spanBuilder.builderBlock()
        text = spannableString
    }
}

Здесь нам нужно проверить, содержит ли текст textView расширяемую строку. Если это не так, мы вернемся, вы можете изменить эту часть, чтобы создать исключение. Также функция `indexRangeOf` возвращает пару начального и конечного индексов подстроки в тексте.

fun String.indexRangeOf(sub: String): Pair<Int, Int>? {
    val start = indexOf(sub)
    return when (start != -1) {
        true -> Pair(start, start + sub.length - 1)
        false -> null
    }
}

Кроме того, нам нужно определить класс SpanBuilder, который отвечает за изменение строки spannable.

class SpanBuilder(
    private val textView: TextView
) {
   internal var range: Pair<Int, Int>? = null
   internal var spannableString: SpannableString? = null

    fun foreground(
        @ColorInt color: Int,
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
    ) {
        range?.let {
            spannableString?.setSpan(
                ForegroundColorSpan(color),
                it.first,
                it.second + 1,
                flag
            )
        }
    }

    fun background(
        @ColorInt color: Int,
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
    ) {
        range?.let {
            spannableString?.setSpan(
                BackgroundColorSpan(color),
                it.first,
                it.second + 1,
                flag
            )
        }
    }

    fun underline(
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
    ) {
        range?.let {
            spannableString?.setSpan(
                UnderlineSpan(),
                it.first,
                it.second + 1,
                flag
            )
        }
    }

    fun bulletSpan(
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
        gapInPx: Int = BulletSpan.STANDARD_GAP_WIDTH,
        @ColorInt color : Int = Color.BLACK
        ) {
        range?.let {
            spannableString?.setSpan(
                BulletSpan(gapInPx,color),
                it.first,
                it.second + 1,
                flag
            )
        }
    }

    fun bulletSpan(
        subString: String,
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
        gapInPx: Int = BulletSpan.STANDARD_GAP_WIDTH,
        @ColorInt color : Int = Color.BLACK
    ) {
        val range = textView.text.toString().indexRangeOf(subString)
        range?.let {
            spannableString?.setSpan(
                BulletSpan(gapInPx,color),
                it.first,
                it.second + 1,
                flag
            )
        }
    }

    fun insert(
       text : String,
       index : Int
    ) {
        val spannableStringBuilder = SpannableStringBuilder(textView.text)
        spannableStringBuilder.insert(index,text)
    }

    fun bold(
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
    ) {
        range?.let {
            spannableString?.setSpan(
                StyleSpan(android.graphics.Typeface.BOLD),
                it.first,
                it.second + 1,
                flag
            )
        }
    }

    fun onClick(
        flag: Int = SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE,
        onClick: (View) -> Unit,
    ) {
        range?.let {
            spannableString?.setSpan(
                object : ClickableSpan() {
                    override fun onClick(p0: View) {
                        onClick(p0)
                    }
                },
                it.first,
                it.second + 1,
                flag
            )
        }
    }
}

Выше класс SpanBuilder устанавливает диапазоны для строки spannable для вычисляемого диапазона. используя этот класс, нам больше не нужно помнить точный класс диапазона, чтобы применить требуемый диапазон. Это просто вызов функции. Кроме того, диапазон индекса диапазона рассчитывается автоматически. Итак, нам больше не нужно поддерживать индексы.

Наконец, мы можем использовать эту функцию расширения следующим образом:

textView.span("spannable") {
    onClick {
        Toast.makeText(textView.context, "spannable clicked", Toast.LENGTH_SHORT).show()
    }
    foreground(Color.RED)
    underline()
    background(Color.YELLOW)
}

Бонус

Если вы хотите охватить весь текст textView, вы можете написать такую ​​функцию перегрузки.

fun TextView.span(
    builderBlock: SpanBuilder.() -> Unit,
) {
    val textOfView = text.toString()
    this.span(textOfView, builderBlock)
}

Заключение

Я надеюсь, что вы найдете эту статью полезной. Вы можете найти исходный код этой статьи здесь.

https://gist.github.com/oguzhanaslann/39ac005aa177514b869ec252aa261791

ЛинкедИн

Всех вас люблю.

Будьте в курсе новых блогов.

Будьте осторожны.