Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Draw text in a given rectangle and automatically wrap lines on a Android Canvas
package de.markusfisch.android.textrect;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;
public class DemoActivity
extends Activity
implements SurfaceHolder.Callback
{
/** Activity is created */
@Override
public void onCreate( Bundle state )
{
super.onCreate( state );
SurfaceView v = new SurfaceView( this );
SurfaceHolder h = v.getHolder();
h.addCallback( this );
requestWindowFeature( Window.FEATURE_NO_TITLE );
setContentView( v );
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN );
}
/** Surface changed */
public void surfaceChanged(
SurfaceHolder holder,
int format,
int width,
int height )
{
}
/** Surface created */
public void surfaceCreated( SurfaceHolder holder )
{
if( holder == null )
return;
Canvas c = null;
try
{
if( (c = holder.lockCanvas()) != null )
drawBubbles( c );
}
finally
{
if( c != null )
holder.unlockCanvasAndPost( c );
}
}
/** Surface destroyed */
public void surfaceDestroyed( SurfaceHolder holder )
{
}
/** Draw bubbles */
private void drawBubbles( final Canvas c )
{
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 compeletly different. According to en.wikipedia.org, 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.",
"This class is designed to be used in games and therefore tries to minimize allocations after instantiation because those will trigger the GC more often which causes a slight but noticeable delay.",
"So, if you want to use this in a game, you should make the instance a class member, allocate it once and reuse it.",
"Don't forget to rotate your device!",
};
final int cells = (int) Math.ceil( Math.sqrt( texts.length ) );
final int margin = 8;
final int totalMargin = (cells + 1) * margin;
final int w = (c.getWidth() - totalMargin) / cells;
final int h = (c.getHeight() - totalMargin) / cells;
for( int n = 0, l = texts.length, x = margin, y = margin;
n < l; )
{
drawBubble(
c,
x,
y,
w,
h,
texts[n] );
if( ++n % cells == 0 )
{
x = margin;
y += h + margin;
}
else
x += w + margin;
}
}
/** Draw a text bubble */
private void drawBubble(
final Canvas c,
final int x,
final int y,
final int width,
final int height,
final String text )
{
final TextRect textRect;
// set up font
{
final Paint fontPaint = new Paint();
fontPaint.setColor( Color.WHITE );
fontPaint.setAntiAlias( true );
fontPaint.setTextSize( 24 );
textRect = new TextRect( fontPaint );
}
final int h = textRect.prepare(
text,
width - 8,
height - 8 );
// draw bubble
{
final Paint p = new Paint();
p.setColor( Color.BLUE );
p.setStyle( Paint.Style.FILL );
p.setAntiAlias( true );
c.drawRoundRect(
new RectF( x, y, x + width, y + h + 8 ),
4,
4,
p );
}
textRect.draw( c, x + 4, y + 4 );
}
}
package de.markusfisch.android.textrect;
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.
* This class is designed to be used in games and therefore tries
* to minimize allocations after instantiation because those will
* trigger the GC more often which causes a slight but noticeable
* delay. So, if you want to use this in a game, you should
* make the instance a class member, allocate it once and reuse it.
*
* @author mf@markusfisch.de
*/
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 = null;
private Paint paint = null;
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( final Paint paint )
{
metrics = paint.getFontMetricsInt();
this.paint = paint;
}
/**
* Calculate height of text block and prepare to draw it.
*
* @param text - text to draw
* @param width - maximum width in pixels
* @param height - maximum height in pixels
* @returns height of text in pixels
*/
public int prepare(
final String text,
final int maxWidth,
final 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 > 0 )
{
final int lineHeight = -metrics.ascent + metrics.descent;
int start = 0;
int stop = maximumInLine > length ? length : maximumInLine;
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 )
{
final int blank = text.lastIndexOf( " ", stop );
final 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 )
{
final 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 )
{
final 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(
final Canvas canvas,
final int left,
final int top )
{
if( textHeight == 0 )
return;
final int before = -metrics.ascent;
final int after = metrics.descent + metrics.leading;
int y = top;
--lines;
for( int n = 0; n <= lines; ++n )
{
String t;
y += before;
if( wasCut &&
n == lines &&
stops[n] - starts[n] > 3 )
t = text.substring( starts[n], stops[n] - 3 ).concat( "..." );
else
t = text.substring( starts[n], stops[n] );
canvas.drawText(
t,
left,
y,
paint );
y += after;
}
}
/** Returns true if text was cut to fit into the maximum height */
public final boolean wasCut()
{
return wasCut;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment