'Handling \n in LineBreakMeasurer
There are thousand articles how to use LineBreakMeasurer to draw multi-line text but there is none about drawing multi-line text taking into account also \n(when you want to force a new line at a specific position in text and not only when the right - or left - margin ends).
The secret seems to lie in BreakIterator, but I couldn't find an implementation which handles \n.
Solution 1:[1]
Instead of LineBreakMeasurer's (LBM's) nextLayout(float) method, use the overloaded LBM.nextLayout(float, int, boolean) method. This allows you to limit the text that LBM will include in the returned TextLayout. In your case, you'll instruct it not to go beyond the next newline.
This code snippet should give you the idea. First use LBM.nextOffset to "peek" which character index would be the end of the next layout. Then iterate over your string content up to that offset to see if you find any newline characters. If you do, then use that found limit as the second argument to nextLayout(float, int, boolean) which will tell LBM not to exceed the newline:
int next = lineMeasurer.nextOffset(formatWidth);
int limit = next;
if (limit < totalLength) {
for (int i = lineMeasurer.getPosition(); i < next; ++i) {
char c = string.charAt(i);
if (c == '\n') {
limit = i;
break;
}
}
}
TextLayout layout = lineMeasurer.nextLayout(formatWidth, limit, false);
References
http://java.sun.com/developer/onlineTraining/Media/2DText/style.html#layout http://java.sun.com/developer/onlineTraining/Media/2DText/Code/LineBreakSample.java
Solution 2:[2]
I find that this code works well for the newline issue. I used atdixon as a template to get this.
while (measurer.getPosition() < paragraph.getEndIndex()) {
next = measurer.nextOffset(wrappingWidth);
limit = next;
charat = tested.indexOf('\n',measurer.getPosition()+1);
if(next > (charat - measurer.getPosition()) && charat != -1){
limit = charat - measurer.getPosition();
}
layout = measurer.nextLayout(wrappingWidth, measurer.getPosition()+limit, false);
// Do the rest of your layout and pen work.
}
Solution 3:[3]
Aaron's code doesn't always work right so here's some tweaked code that is working for me:
int next = measurer.nextOffset(width);
int limit = next;
if (limit <= text.length()) {
for (int i = measurer.getPosition(); i < next; ++i) {
char c = text.charAt(i);
if (c == '\n') {
limit = i + 1;
break;
}
}
}
TextLayout textLayout = measurer.nextLayout(width, limit, false);
If you need text from an AttributedString you can just do this beforehand
AttributedCharacterIterator iterator = attributedString.getIterator();
StringBuilder stringBuilder = new StringBuilder(iterator.getEndIndex());
while (iterator.getIndex() < iterator.getEndIndex()) {
stringBuilder.append(iterator.current());
iterator.next();
}
String text = stringBuilder.toString();
Solution 4:[4]
Even though the topic is very old, I’ve had this issue myself and had to solve it. After considerable amount of investigation, I’ve come up with solution that would work in single class that wraps ’JTextArea’.
The code is in Kotlin, as that is what I’m using. Hopefully it’ll still be useful.
package [your package name]
import java.awt.Font
import java.awt.FontMetrics
import java.awt.Insets
import java.awt.font.LineBreakMeasurer
import java.awt.font.TextAttribute
import java.text.AttributedString
import java.text.BreakIterator
import javax.swing.JTextArea
class TextAreaLineCounter(
private val textArea: JTextArea
) {
private val font: Font
get() = textArea.font
private val fontMetrics: FontMetrics
get() = textArea.getFontMetrics(font)
private val insets: Insets
get() = textArea.insets
private val formatWidth: Float
get() = (textArea.width - insets.left - insets.right).toFloat()
fun countLines(): Int {
return countLinesInParagraphs(
textRaw = textArea.text,
font = font,
fontMetrics = fontMetrics,
formatWidth = formatWidth
)
}
private fun countLinesInParagraphs(
textRaw: String,
font: Font,
fontMetrics: FontMetrics,
formatWidth: Float
): Int {
val paragraphs: List<String> = textRaw.split("\n")
val lineCount = paragraphs.fold(0) { acc: Int, sentence: String ->
val newCount = acc + countLinesInSentence(sentence, font, fontMetrics, formatWidth)
newCount
}
return lineCount
}
private fun countLinesInSentence(
textRaw: String,
font: Font,
fontMetrics: FontMetrics,
formatWidth: Float
): Int {
val text = AttributedString(textRaw)
text.addAttribute(TextAttribute.FONT, font)
val frc = fontMetrics.fontRenderContext
val charIt = text.iterator
val lineMeasurer = LineBreakMeasurer(
charIt,
BreakIterator.getWordInstance(),
frc
)
lineMeasurer.position = charIt.beginIndex
var noLines = 0
while (lineMeasurer.position < charIt.endIndex) {
lineMeasurer.nextLayout(formatWidth)
noLines++
}
return noLines
}
}
Also, may be useful as well, a GUI application that lets you test out the line counter.
package [your package name]
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.awt.*
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JTextArea
import javax.swing.SwingUtilities
class MainJTextArea(
private val l: Logger
): JPanel(GridBagLayout()) {
init {
val inputStr = "Lorem ipsum dolor sit amet, consectetur adipisicing\n elit, sed do eiusmo," +
" Lorem ipsum \ndolor sit amet, consectetur adipisicing elit, sed do eiusmo," +
" Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo," +
" Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo"
val textArea = drawTextArea(
text = inputStr,
fontSize = 12.0
)
val textAreaLineCounter = TextAreaLineCounter(textArea)
// Add Components to this panel.
val c = GridBagConstraints().apply {
gridwidth = GridBagConstraints.REMAINDER
fill = GridBagConstraints.BOTH
weightx = 1.0
weighty = 1.0
}
add(textArea, c)
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) {
super.componentResized(e)
l.debug("Line count: ${textAreaLineCounter.countLines()}")
}
})
}
private fun drawTextArea(
text: String,
fontSize: Double = 12.0
): JTextArea {
val textArea = JTextArea(text)
textArea.size = Dimension(width, height)
textArea.foreground = Color.BLACK
textArea.background = Color(0, 0, 0, 0)
textArea.font = Font(null, Font.LAYOUT_LEFT_TO_RIGHT, fontSize.toInt())
textArea.lineWrap = true
textArea.wrapStyleWord = true
return textArea
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger(MainJTextArea::class.java)!!
SwingUtilities.invokeLater {
val frame = JFrame("JTextAreaLineCountDemo").apply {
preferredSize = Dimension(400, 360)
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
add(MainJTextArea(logger))
pack()
}
frame.isVisible = true
}
}
}
}
Update
After further invetigation, I’ve noticed calculator was still having problems and needed a bit of customization. So I’ve improved calculation mechanism to provide details with text breaks composed inside.
This mechanism works most of the time. I’ve noticed couple of cases, where JTextArea would wrap with empty line, which was not detected. So use the code at your own risk.
/**
* Parses text to fit in [TextProvider.formatWidth] and wraps whenever needed
*/
class TextAreaLineCounter(
private val textProvider: TextProvider
) {
private val formatWidth: Float
get() = textProvider.formatWidth
fun parseParagraph(
font: Font,
fontMetrics: FontMetrics
): WrappedParagraph {
return countLinesInParagraphs(
textRaw = textProvider.text,
font = font,
fontMetrics = fontMetrics,
formatWidth = formatWidth
)
}
/**
* Counts lines in [JTextArea]
* Includes line breaks ('\n')
*/
private fun countLinesInParagraphs(
textRaw: String,
font: Font,
fontMetrics: FontMetrics,
formatWidth: Float
): WrappedParagraph {
val paragraphsAsString: List<String> = textRaw.split("\n")
val sentences = paragraphsAsString.map { paragraph ->
countLinesInSentence(paragraph, font, fontMetrics, formatWidth)
}
return WrappedParagraph(sentences = sentences)
}
/**
* Counts lines in wrapped [JTextArea]
* Does not include line breaks.
*/
private fun countLinesInSentence(
textRaw: String,
font: Font,
fontMetrics: FontMetrics,
formatWidth: Float
): Sentence {
if (textRaw.isEmpty()) {
return Sentence(
wraps = listOf(
SentenceWrap(
wrapIndex = -1,
wrapText = textRaw
)
)
)
}
val text = AttributedString(textRaw)
text.addAttribute(TextAttribute.FONT, font)
val frc = fontMetrics.fontRenderContext
val charIt = text.iterator
val words = mutableListOf<SentenceWrap>()
val lineMeasurer = LineBreakMeasurer(
charIt,
BreakIterator.getLineInstance(),
frc
)
lineMeasurer.position = charIt.beginIndex
var posBegin = 0
var posEnd = lineMeasurer.position
var noLines = 0
do {
lineMeasurer.nextLayout(formatWidth)
posBegin = posEnd
posEnd = lineMeasurer.position
words.add(
SentenceWrap(
wrapIndex = noLines,
wrapText = textRaw.substring(posBegin, posEnd)
)
)
noLines++
} while (posEnd < charIt.endIndex)
return Sentence(words)
}
/**
* Holds wrapped [Sentence]s that break during 'wrap text' or text break symbols
*/
data class WrappedParagraph(
val sentences: List<Sentence>
) {
fun lineCount(): Int {
val sentenceCount = sentences.fold(0) { currentCount: Int, sentence: Sentence ->
val newCount = currentCount + sentence.lineCount()
newCount
}
return sentenceCount
}
}
/**
* Sentence contains text pieces which are broken by 'wrapText'
*/
data class Sentence(
val wraps: List<SentenceWrap>
) {
fun lineCount(): Int = wraps.size
}
/**
* Entity for holding a wrapped text snippet
*/
data class SentenceWrap(
val wrapIndex: Int,
val wrapText: String
)
interface TextProvider {
val text: String
val formatWidth: Float
}
companion object {
val l = LoggerFactory.getLogger(TextAreaLineCounter::class.java)!!
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | aaron |
| Solution 2 | Brett |
| Solution 3 | Nathan Brown |
| Solution 4 |
