I have a string [tab] [ch]C[/ch] [ch]Am[/ch] \\n I heard there was a secret chord[/tab]
When the TextView is big enough to hold it with no wrapping it should (and does) look like this:
C Am
I heard there was a secret chord
When the line(s) are too long to fit in the TextView, I want it to wrap like this:
C
I heard there was a
Am
secret chord
Right now it wraps like this (like you'd expect if it was just text)
C
Am
I heard there was a
secret chord
Constraints:
C
, F
, Am
, G
) are clickable so if you make a custom implementation of TextView, it still has to be able to handle ClickableSpans
or otherwise keep them clickableIf it's helpful, this is for an open source project of mine, so the source is available on Github. Here's the fragment source (look for fun processTabContent(text: CharSequence)
-- that's where I process the text right now. Here's the layout xml .
My data is stored in a single string (this can't be changed -- I get it from an API). Here's how the above tab would be formatted:
[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]
Note that the chords (notes that a guitarist would play, like C
or F
) are wrapped in [ch]
tags. I currently have code that finds these, removes the [ch]
tags, and wraps each chord in a ClickableSpan
. On click, my application shows another fragment with instructions how to play the chord on a guitar. This is only important in that the answer to this question must allow these chords to be clicked like this still.
As you may have noticed by now, it's the [tab]
tags that we're going to have to focus on for this question. Right now, I'm going through the string and replacing [tab]
with a newline and removing all instances of [/tab]
. This works fine if my TextView
's text size is small enough that entire lines fit on the device screen. However, when the word wrap kicks in I start having problems.
This:
C Am
I heard there was a secret chord
Should wrap to this:
C
I heard there was a
Am
secret chord
But instead wraps like this:
C
Am
I heard there was a
secret chord
I think this solution might solve the issue. But there are some assumption,
[tab]
and end with [/tab]
\\n
between chords and lyricAnd I believe you need to cleanse the data before you use it. Since, it is likely possible to handle Intro, Verse
easily, I will focus on lyric tab
only.
Here is the sample data for single lyric
[tab] [ch]C[/ch] [ch]F[/ch] [ch]G[/ch] \\n But you don't really care for music, do you?[/tab]
Firstly, We need to remove some unwanted blocks.
val inputStr = singleLyric
.replace("[tab]", "")
.replace("[/tab]", "")
.replace("[ch]", "")
.replace("[/ch]", "")
After that, I separated the chords and lyric
val indexOfLineBreak = inputStr.indexOf("\n")
val chords = inputStr.substring(0, indexOfLineBreak)
val lyrics = inputStr.substring(indexOfLineBreak + 1, inputStr.length).trim()
After we clean the data, we can start to set the data.
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
}
Idea is simple. After we set the lyric data to textview, we can get line count. With the current line number, we can get starting index and ending index of the selected line. With the indexes, we can manipulate the string. Hope this can help u.
This is based off of Hein Htet Aung's answer . The general idea is that you have two lines passed in ( singleLyric
), but the lines might have to be processed before appending them (hence the middle while
loop). For convenience, this was written with a parameter appendTo
that the lyric will be appended to. It returns a finished SpannableStringBuilder
with the lyric appended. It would be used like this:
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)
Here's the processing function:
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
}
And finally, I found the need to break my text at words rather than just at the max line length, so here's the word break finder function for that:
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
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.