Обернуть две строки за один раз в Android TextView с пролетами - PullRequest
2 голосов
/ 22 марта 2020

Сводка

У меня есть строка [tab] [ch]C[/ch] [ch]Am[/ch] \n I heard there was a secret chord[/tab]

Когда TextView достаточно большой, чтобы его можно было держать без упаковки, он должен (и должен) выглядеть следующим образом:

  C                  Am         
I heard there was a secret chord

Когда строки слишком длинны, чтобы поместиться в TextView, я хочу, чтобы они были обернуты следующим образом:

  C                
I heard there was a 
  Am
secret chord

Прямо сейчас это оборачивается так (как и следовало ожидать, если был просто текст)

  C                
 Am         
I heard there was a
secret chord

Ограничения:

  • Я использую моноширинный шрифт текста для выравнивания
  • Аккорды (C, F, Am, G) можно нажимать, поэтому, если вы делаете пользовательскую реализацию TextView, он все равно должен иметь возможность обрабатывать ClickableSpans или иным образом сохранять их активными
  • Kotlin или Java (или XML) в порядке

Если это полезно, это для моего проекта с открытым исходным кодом, поэтому исходный код доступен на Github. Вот источник фрагмента (ищите fun processTabContent(text: CharSequence) - вот где я сейчас обрабатываю текст. Вот макет xml.


Формат ввода

Мои данные хранятся в одной строке (это нельзя изменить - я получаю их из API). Вот как будет отформатирована вкладка выше:

[Intro]\n[tab][ch]C[/ch] [ch]Am[/ch] [ch]C[/ch] [ch]Am[/ch][/tab]\n[Verse 1][tab]      [ch]C[ch]                  [ch]Am[/ch]                         I heard there was a secret chord               [/tab][tab]      [ch]C[/ch]                     [ch]Am[/ch]\nThat David played, and it pleased the Lord[/tab][tab]   [ch]C[/ch]                [ch]F[/ch]               [ch]G[/ch]\n But you don't really care for music, do you?[/tab]

Обратите внимание, что аккорды (примечания, которые должен играть гитарист, например C или F) заключены в теги [ch]. В настоящее время у меня есть код, который находит их, удаляет теги [ch] и переносит каждый аккорд в ClickableSpan. При нажатии мое приложение показывает еще один фрагмент с инструкциями, как сыграть аккорд на гитаре. Это важно только потому, что ответ на этот вопрос должен позволять нажимать эти аккорды, как этот, до сих пор.

То, что я делаю сейчас (это не работает)

Как вы, возможно, уже заметили, это теги [tab], на которых мы собираемся сосредоточиться для этого вопрос. Прямо сейчас я иду через строку и заменяю [tab] с новой строкой и удалением всех экземпляров [/tab]. Это прекрасно работает, если размер текста моего TextView достаточно мал, чтобы на экране устройства помещались целые строки. Тем не менее, когда начинается слово «обтекание», у меня начинаются проблемы.

Это:

  C                  Am         
I heard there was a secret chord

Должно быть заключено в следующее:

  C                
I heard there was a 
  Am
secret chord

Но вместо этого переносится так:

  C                
 Am         
I heard there was a
secret chord

Ответы [ 2 ]

3 голосов
/ 27 марта 2020

Я думаю, что это решение может решить проблему. Но есть некоторые предположения:

  1. Каждый лири c начинается с [tab] и заканчивается [/tab]
  2. Он всегда разделяется \n между аккордами и лири c

И я считаю, что вам нужно очистить данные перед их использованием. Поскольку, вероятно, можно легко обрабатывать Intro, Verse, я сосредоточусь только на lyri c tab.

Вот пример данных для одного лири c

[табуляция] [ch] C [/ ch] [ch] F [/ ch] [ch] G [ / ch] \ n Но вы не особо заботитесь о музы c, не так ли? [/ tab]

Во-первых, нам нужно удалить некоторые ненужные блоки.

val inputStr = singleLyric
      .replace("[tab]", "")
      .replace("[/tab]", "")
      .replace("[ch]", "")
      .replace("[/ch]", "")

После этого я разделил аккорды и лири c

val indexOfLineBreak = inputStr.indexOf("\n")
val chords = inputStr.substring(0, indexOfLineBreak)
val lyrics = inputStr.substring(indexOfLineBreak + 1, inputStr.length).trim()

После того, как мы очистили данные, мы можем начать устанавливать данные.

text_view.text = lyrics
text_view.post {
  val lineCount = text_view.lineCount
  var currentLine = 0
  var newStr = ""

  if (lineCount <= 1) {// if it's not multi line, no need to manipulate data
    newStr += chords + "\n" + lyrics
  } else {

    val chordsCount = chords.count()
    while (currentLine < lineCount) {
      //get start and end index of selected line
      val lineStart = text_view.layout.getLineStart(currentLine)
      val lineEnd = text_view.layout.getLineEnd(currentLine)

      // add chord substring
      if (lineEnd <= chordsCount) //chords string can be shorter than lyric
        newStr += chords.substring(lineStart, lineEnd) + "\n"
      else if (lineStart < chordsCount) //it can be no more chords data to show
        newStr += chords.substring(lineStart, chordsCount) + "\n"

      // add lyric substring
      newStr += lyrics.substring(lineStart, lineEnd) + "\n"
      currentLine++
    }

  }
  text_view.text = newStr
}

Идея проста. После того, как мы установили данные lyri c в textview, мы можем получить количество строк. С текущим номером строки мы можем получить начальный индекс и конечный индекс выбранной строки. С помощью индексов мы можем манипулировать строкой. Надеюсь, это поможет вам.

1 голос
/ 31 марта 2020

Это основано на ответе Хейн Хтет Аунг . Общая идея заключается в том, что передается две строки (singleLyric), но перед добавлением строк может потребоваться обработка строк (следовательно, середина while l oop). Для удобства это было написано с параметром appendTo, к которому будет добавлен лири c. Возвращает готовый SpannableStringBuilder с добавленным лири c. Он будет использоваться следующим образом:

ssb = SpannableStringBuilder()
for (lyric in listOfDoubleLyricLines) {
    ssb = processLyricLine(lyric, ssb)
}
textView.movementMethod = LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
textView.setText(ssb, TextView.BufferType.SPANNABLE)

Вот функция обработки:

private fun processLyricLine(singleLyric: CharSequence, appendTo: SpannableStringBuilder): SpannableStringBuilder {
    val indexOfLineBreak = singleLyric.indexOf("\n")
    var chords: CharSequence = singleLyric.subSequence(0, indexOfLineBreak).trimEnd()
    var lyrics: CharSequence = singleLyric.subSequence(indexOfLineBreak + 1, singleLyric.length).trimEnd()
    var startLength = appendTo.length
    var result = appendTo

    // break lines ahead of time
    // thanks @Andro https://stackoverflow.com/a/11498125
    val availableWidth = binding.tabContent.width.toFloat() //- binding.tabContent.textSize / resources.displayMetrics.scaledDensity

    while (lyrics.isNotEmpty() || chords.isNotEmpty()) {
        // find good word break spot at end
        val plainChords = chords.replace("[/?ch]".toRegex(), "")
        val wordCharsToFit = findMultipleLineWordBreak(listOf(plainChords, lyrics), binding.tabContent.paint, availableWidth)

        // make chord substring
        var i = 0
        while (i < min(wordCharsToFit, chords.length)) {
            if (i+3 < chords.length && chords.subSequence(i .. i+3) == "[ch]"){
                //we found a chord; add it.
                chords = chords.removeRange(i .. i+3)        // remove [ch]
                val start = i

                while(chords.subSequence(i .. i+4) != "[/ch]"){
                    // find end
                    i++
                }
                // i is now 1 past the end of the chord name
                chords = chords.removeRange(i .. i+4)        // remove [/ch]

                result = result.append(chords.subSequence(start until i))

                //make a clickable span
                val chordName = chords.subSequence(start until i)
                val clickableSpan = makeSpan(chordName)
                result.setSpan(clickableSpan, startLength+start, startLength+i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
            } else {
                result = result.append(chords[i])
                i++
            }
        }
        result = result.append("\r\n")

        // make lyric substring
        val thisLine = lyrics.subSequence(0, min(wordCharsToFit, lyrics.length))
        result = result.append(thisLine).append("\r\n")

        // update for next pass through
        chords = chords.subSequence(i, chords.length)
        lyrics = lyrics.subSequence(thisLine.length, lyrics.length)
        startLength = result.length
    }

    return result
}

И, наконец, я обнаружил необходимость разбивать текст на слова, а не на строку макс. длина, так вот функция поиска слова для этого:

private fun findMultipleLineWordBreak(lines: List<CharSequence>, paint: TextPaint, availableWidth: Float): Int{
    val breakingChars = "‐–〜゠= \t\r\n"  // all the chars that we'll break a line at
    var totalCharsToFit: Int = 0

    // find max number of chars that will fit on a line
    for (line in lines) {
        totalCharsToFit = max(totalCharsToFit, paint.breakText(line, 0, line.length,
                true, availableWidth, null))
    }
    var wordCharsToFit = totalCharsToFit

    // go back from max until we hit a word break
    var allContainWordBreakChar: Boolean
    do {
        allContainWordBreakChar = true
        for (line in lines) {
            allContainWordBreakChar = allContainWordBreakChar
                    && (line.length <= wordCharsToFit || breakingChars.contains(line[wordCharsToFit]))
        }
    } while (!allContainWordBreakChar && --wordCharsToFit > 0)

    // if we had a super long word, just break at the end of the line
    if (wordCharsToFit < 1){
        wordCharsToFit = totalCharsToFit
    }

    return wordCharsToFit
}
...