在带有跨度的Android文本视图中一次换行两行



摘要

我有一个字符串[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上获得。这是片段源(查找fun processTabContent(text: CharSequence)——这就是我现在处理文本的地方


输入格式

我的数据存储在一个字符串中(这是无法更改的——我从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的文本大小足够小,以至于整行都可以放在设备屏幕上,这就很好了。然而,当单词wrap出现时,我就开始有问题了。

此:

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

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

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

我认为在使用之前需要清理数据。由于很可能很容易处理Intro, Verse,所以我将只关注歌词tab

这是单歌词的样本数据

[tab][ch]C[/ch][ch]F[/ch][h]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之后,我们可以获得行数。根据当前行号,我们可以得到所选行的起始索引和结束索引。使用索引,我们可以操作字符串。希望这能帮助你。

这是基于Hein Htet Aung的回答。一般的想法是,传入了两行(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("rn")
// make lyric substring
val thisLine = lyrics.subSequence(0, min(wordCharsToFit, lyrics.length))
result = result.append(thisLine).append("rn")
// 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 = "‐–〜゠= trn"  // 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
}

最新更新