Last active
September 9, 2024 07:09
-
-
Save markusfisch/2655909 to your computer and use it in GitHub Desktop.
Draw text in a given rectangle and automatically wrap lines on a Android Canvas
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package de.markusfisch.android.textrect.widget; | |
import android.content.Context; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Paint; | |
import android.graphics.RectF; | |
import android.view.View; | |
import de.markusfisch.android.textrect.graphics.TextRect; | |
public class DemoBubblesView extends View { | |
private static final String[] TEXTS = new String[]{ | |
"Hi there, I'm a blue bubble.", | |
"Me too!", | |
"There are a lot of bubbles around here. And all of them are blue.", | |
"And now for something completely different. According to Wikipedia, the origin of this phrase \"is credited to Christopher Trace, founding presenter of the children's television programme Blue Peter, who used it (in all seriousness) as a link between segments\". Interesting, isn't it?", | |
"Lorem ipsum is so boring.", | |
"Draw text in a given rectangle and automatically wrap lines.", | |
"Don't forget to rotate your device.", | |
}; | |
private static final int CELLS = (int) Math.ceil(Math.sqrt(TEXTS.length)); | |
private final RectF bubbleRect = new RectF(); | |
private final Paint bubblePaint = new Paint(); | |
private final TextRect textRect; | |
private int outerPadding; | |
private int outerPaddingBoth; | |
private int bubblePadding; | |
public DemoBubblesView(Context context) { | |
super(context); | |
float dp = context.getResources().getDisplayMetrics().density; | |
outerPadding = Math.round(16f * dp); | |
outerPaddingBoth = (CELLS + 1) * outerPadding; | |
bubblePadding = Math.round(8f * dp); | |
// create text rect for this font | |
{ | |
Paint fontPaint = new Paint(); | |
fontPaint.setColor(Color.WHITE); | |
fontPaint.setAntiAlias(true); | |
fontPaint.setTextSize(14 * dp); | |
textRect = new TextRect(fontPaint); | |
} | |
bubblePaint.setStyle(Paint.Style.FILL); | |
bubblePaint.setColor(Color.BLUE); | |
bubblePaint.setAntiAlias(true); | |
} | |
@Override | |
public void onDraw(Canvas canvas) { | |
int bubbleWidth = (getWidth() - outerPaddingBoth) / CELLS; | |
int bubbleHeight = (getHeight() - outerPaddingBoth) / CELLS; | |
int x = outerPadding; | |
int y = outerPadding; | |
for (int i = 0, l = TEXTS.length; i < l; ) { | |
drawTextBubble(canvas, x, y, bubbleWidth, bubbleHeight, | |
bubblePadding, TEXTS[i]); | |
if (++i % CELLS == 0) { | |
x = outerPadding; | |
y += bubbleHeight + outerPadding; | |
} else { | |
x += bubbleWidth + outerPadding; | |
} | |
} | |
} | |
private void drawTextBubble( | |
Canvas canvas, | |
int x, | |
int y, | |
int width, | |
int height, | |
int padding, | |
String text) { | |
int paddingBoth = padding * 2; | |
int h = textRect.prepare( | |
text, | |
width - paddingBoth, | |
height - paddingBoth); | |
bubbleRect.set(x, y, x + width, y + h + paddingBoth); | |
canvas.drawRoundRect(bubbleRect, padding, padding, bubblePaint); | |
textRect.draw(canvas, x + padding, y + padding); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package de.markusfisch.android.textrect.activity; | |
import android.app.Activity; | |
import android.os.Bundle; | |
import de.markusfisch.android.textrect.widget.DemoBubblesView; | |
public class MainActivity extends Activity { | |
@Override | |
public void onCreate(Bundle state) { | |
super.onCreate(state); | |
setContentView(new DemoBubblesView(this)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package de.markusfisch.android.textrect.graphics; | |
import android.graphics.Canvas; | |
import android.graphics.Paint; | |
import android.graphics.Paint.FontMetricsInt; | |
import android.graphics.Rect; | |
/** | |
* Draw text in a given rectangle and automatically wrap lines. | |
*/ | |
public class TextRect { | |
// maximum number of lines; this is a fixed number in order | |
// to use a predefined array to avoid ArrayList (or something | |
// similar) because filling it does involve allocating memory | |
static private int MAX_LINES = 256; | |
// those members are stored per instance to minimize | |
// the number of allocations to avoid triggering the | |
// GC too much | |
private FontMetricsInt metrics; | |
private Paint paint; | |
private int[] starts = new int[MAX_LINES]; | |
private int[] stops = new int[MAX_LINES]; | |
private int lines = 0; | |
private int textHeight = 0; | |
private Rect bounds = new Rect(); | |
private String text = null; | |
private boolean wasCut = false; | |
/** | |
* Create reusable text rectangle (use one instance per font). | |
* | |
* @param paint paint specifying the font | |
*/ | |
public TextRect(Paint paint) { | |
metrics = paint.getFontMetricsInt(); | |
this.paint = paint; | |
} | |
/** | |
* Calculate height of text block and prepare to draw it. | |
* | |
* @param text text to draw | |
* @param maxWidth maximum width in pixels | |
* @param maxHeight maximum height in pixels | |
* @returns height of text in pixels | |
*/ | |
public int prepare(String text, int maxWidth, int maxHeight) { | |
lines = 0; | |
textHeight = 0; | |
this.text = text; | |
wasCut = false; | |
// get maximum number of characters in one line | |
paint.getTextBounds("i", 0, 1, bounds); | |
final int maximumInLine = maxWidth / bounds.width(); | |
final int length = text.length(); | |
if (length < 1) { | |
return 0; | |
} | |
final int lineHeight = -metrics.ascent + metrics.descent; | |
int start = 0; | |
int stop = Math.min(maximumInLine, length); | |
for (; ; ) { | |
// skip LF and spaces | |
for (; start < length; ++start) { | |
char ch = text.charAt(start); | |
if (ch != '\n' && | |
ch != '\r' && | |
ch != '\t' && | |
ch != ' ') { | |
break; | |
} | |
} | |
for (int o = stop + 1; stop < o && stop > start; ) { | |
o = stop; | |
int lowest = text.indexOf("\n", start); | |
paint.getTextBounds( | |
text, | |
start, | |
stop, | |
bounds); | |
if ((lowest >= start && lowest < stop) || | |
bounds.width() > maxWidth) { | |
--stop; | |
if (lowest < start || lowest > stop) { | |
int blank = text.lastIndexOf(" ", stop); | |
int hyphen = text.lastIndexOf("-", stop); | |
if (blank > start && | |
(hyphen < start || blank > hyphen)) { | |
lowest = blank; | |
} else if (hyphen > start) { | |
lowest = hyphen; | |
} | |
} | |
if (lowest >= start && lowest <= stop) { | |
char ch = text.charAt(stop); | |
if (ch != '\n' && ch != ' ') { | |
++lowest; | |
} | |
stop = lowest; | |
} | |
continue; | |
} | |
break; | |
} | |
if (start >= stop) { | |
break; | |
} | |
int minus = 0; | |
// cut off lf or space | |
if (stop < length) { | |
char ch = text.charAt(stop - 1); | |
if (ch == '\n' || ch == ' ') { | |
minus = 1; | |
} | |
} | |
if (textHeight + lineHeight > maxHeight) { | |
wasCut = true; | |
break; | |
} | |
starts[lines] = start; | |
stops[lines] = stop - minus; | |
if (++lines > MAX_LINES) { | |
wasCut = true; | |
break; | |
} | |
if (textHeight > 0) { | |
textHeight += metrics.leading; | |
} | |
textHeight += lineHeight; | |
if (stop >= length) { | |
break; | |
} | |
start = stop; | |
stop = length; | |
} | |
return textHeight; | |
} | |
/** | |
* Draw prepared text at given position. | |
* | |
* @param canvas canvas to draw text into | |
* @param left left corner | |
* @param top top corner | |
*/ | |
public void draw(Canvas canvas, int left, int top) { | |
if (textHeight == 0) { | |
return; | |
} | |
final int before = -metrics.ascent; | |
final int after = metrics.descent + metrics.leading; | |
int y = top; | |
int lastLine = lines - 1; | |
for (int i = 0; i < lines; ++i) { | |
String line; | |
if (wasCut && i == lastLine && stops[i] - starts[i] > 3) { | |
line = text.substring(starts[i], stops[i] - 3).concat("..."); | |
} else { | |
line = text.substring(starts[i], stops[i]); | |
} | |
y += before; | |
canvas.drawText(line, left, y, paint); | |
y += after; | |
} | |
} | |
/** | |
* Returns true if text was cut to fit into the maximum height | |
*/ | |
public boolean wasCut() { | |
return wasCut; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you remove
--lines;
it will crash because the condition in thefor()
loop isn <= lines
what meansstops[]
will cause anOutOfBoundsException
in the last iteration. The reason for decrementinglines
was to know when it's the last line to append the...
in line 220. But you're totally right: invokingdraw()
multiple times without callingprepare()
beforehand will lead not work :(So I've updated the code to make successive
draw()
calls possible.