【问题标题】:Wrap two lines at a time in an Android TextView with spans在具有跨度的 Android TextView 中一次换行两行
【发布时间】:2020-03-22 00:07:43
【问题描述】:

总结

我有一个字符串[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

约束:

  • 我使用等宽文本字体来保持对齐
  • 和弦(CFAmG)是可点击的,因此如果您自定义实现 TextView,它仍然必须能够处理 ClickableSpans 或以其他方式保持它们可点击
  • Kotlin 或 Java(或 XML)都可以

如果有帮助,这是我的一个开源项目,所以源代码可以在 Github 上找到。这是fragment source(查找fun processTabContent(text: CharSequence)——我现在正在处理文本。这是layout 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]

请注意,和弦(吉他手会演奏的音符,如CF)被包裹在[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

【问题讨论】:

  • 当前想法:stackoverflow.com/questions/41779934/…stackoverflow.com/questions/42219292/… 结合起来可能会奏效,但我没有足够的经验来适应它。
  • 仅供参考,我的 TextView 还包含这些和弦的 ClickableSpan,因此答案必须允许保留这些跨度。
  • 对不起,我在 android 上很不错,但在吉他指法上不行。我需要一些关于指法的基本信息,比如什么是 C、Am、F 和 G?。你的数据结构是什么?和弦和单词是单个字符串还是映射或数组格式?
  • 你能显示一些数据 xml 你是如何显示的
  • @Heisenberg 好问题。我用 TL;DR 更新了问题并添加了该信息。基本上,CAmFG 告诉吉他手要弹什么音符。吉他手可以通过结束的单词来判断何时播放该音符。对于数据结构,整个选项卡都在一个巨大的字符串中,包括和弦和歌词。但是,它的格式是[ch] / [/ch] 标签包裹在和弦周围,[tab] / [/tab] 标签包裹在应该被包裹在一起的一组行(一行和弦和一个歌词)周围。希望对您有所帮助。

标签: java android kotlin textview word-wrap


【解决方案1】:

我认为这个解决方案可能会解决这个问题。但是有一些假设,

  1. 每首歌词都以[tab] 开头,以[/tab] 结尾
  2. 和弦和歌词之间总是用\n隔开

而且我相信您需要在使用数据之前对其进行清理。因为,很可能很容易处理Intro, Verse,所以我将只关注歌词tab

这是单个歌词的示例数据

[tab] [ch]C[/ch] [ch]F[/ch] [ch]G[/ch] \n但你并不真正关心音乐,是吗?[/tab]

首先,我们需要删除一些不需要的块。

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

之后,我把和弦和歌词分开了

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
}

想法很简单。我们将歌词数据设置为textview后,我们可以得到行数。通过当前行号,我们可以得到所选行的起始索引和结束索引。使用索引,我们可以操作字符串。希望这可以帮助你。

【讨论】:

  • 这些假设是正确的。我不在我的电脑前测试它,但我会告诉你的!
  • 另外,正如您所指出的,不需要处理 Into 和 Verse。这些都是纯文本
  • 我尝试了不同的宽度。应该没问题;)。哦,正如您所描述的,您需要使用等宽,因为它使用空格字符来表示和弦间距。
  • 这在很大程度上要归功于您!恭喜,我想你已经赢得了赏金!我们最终做了很多额外的工作来使用每个[tab] 内的内部可点击跨度,所以我相信一位同事将在周一早上发布最终产品,我计划接受。不过不用担心!赏金归你!
  • 哈哈,赏金很好。如果您发现其他用例,我想修复。不管怎么说,还是要谢谢你。很高兴我能帮助你。 :D
【解决方案2】:

这是基于Hein Htet Aung's answer。一般的想法是你有两行传入(singleLyric),但在附加它们之前可能必须处理这些行(因此中间的while循环)。为方便起见,这是用参数appendTo 编写的,歌词将附加到该参数。它返回一个完成的SpannableStringBuilder,并附加了歌词。它会这样使用:

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
}

【讨论】:

    猜你喜欢
    • 2017-04-07
    • 2015-08-04
    • 2012-10-02
    • 1970-01-01
    • 2015-05-18
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 2012-01-28
    相关资源
    最近更新 更多