Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Designing and Implementing Speech Bubbles for Blackberry Applications

0.00/5 (No votes)
3 Nov 2011 6  
The article presents how to implement speech bubbles (or message bubbles) for applications that run on Blackberry devices
Sample Image

Introduction

Speech bubbles (or message bubbles) are becoming more and more popular. They are now present in a vast number of mobile messaging applications across most of the platforms (iOS, Android, RIM and WP7). In this article, I’m going to present my implementation of this very nice UI feature for the RIM platform.

Article Contents

Speech Bubble Requirements

Before I get into the implementation of the speech bubble, I’m going to talk about the features that the speech bubble will need to support. These features can be seen in the list below:

  • The bubble will need to have a variable size. For aesthetic reasons, the bubble should not have a width bigger than 3 quarters of the screen width.
  • The bubble should contain both text and images. The text will be shown first followed by a list of images beneath the text.
  • The images will need to be wrapped (if there are many) in order to respect the maximum with of the bubble.

A graphical example of these features can be seen in the image below:

As you can see from the above image, if the speech bubble contains only images, there will be no space left available for the text. The same thing happens if there is only text (no space is allocated for the images).

The Implementation

This section will present the speech bubble implementation. As you have probably guessed by now, the speech bubble contains two parts: the text part and the images part. These 2 parts are contained in the speech bubble manager. At the same time, all the message images are contained in another manager. This manager is a wrap panel.

The first part of this section will present the wrap panel implementation. After this, I'm going to present the speech bubble manager implementation.

Implementing the Wrap Panel Manager

When I thought about how I was going to implement the image panel, I tried to use an existing field manager (using the FlowFieldManager class). After spending a few hours trying to make this work, I gave up and I decided to implement my own wrap panel. This section will present this implementation.

In order to implement a custom field manager, the only requirement is to override the sublayout() method. In this method’s body, 2 things need to be done. The first is to position the child fields (by calling the layoutChild() method to measure the fields and the setPositionChild() method to position them) and the second is to set the manager’s size at the end (before exiting the method body).

The code listing presented below shows the implementation of this manager.

protected void sublayout(int width, int height) {
	int currX=0, currY=0;//the current position at which to insert
	int maxX=0;//max width of the panel
	int maxY=0;//the maximum height of the current panel line 
		//(elements can be of different heights)

	int count=getFieldCount();
	for(int i=0;i<count;i++){
		Field f=getField(i);
		//lay out the element inside the panel
		layoutChild(f, width, height);
		int fw=f.getWidth()+f.getMarginLeft()+f.getMarginRight();
		int fh=f.getHeight()+f.getMarginTop()+f.getMarginBottom();
		//check for new line
		if((currX+fw) > width && currX!=0){
			//set the max width before resetting the x position
			if(maxX<currX)maxX=currX;
			//reset x
			currX=0;
			//update the total height thus far 
			//(and the max line height to the current y position)
			currY+=maxY;
			//the new line is empty at this point and so 
			//it has a height of 0
			maxY=0;
		}
		//position the current element
		setPositionChild(f, currX+f.getMarginLeft(), currY+f.getMarginTop());
		//after the element is added calculate a new max height for the line
		maxY=Math.max(maxY, fh);
		//update the x position
		currX+=fw;
	}
	//after all elements have been added determine the final size of the panel
	currY+=maxY;
	if(maxX<currX)maxX=currX;
	//set the panel's width and height
	setExtent(maxX, currY);
}

The sublayout() method takes 2 parameters. These parameters represent the maximum size available to the manager.

The first lines of code initialize a few helper variables. After these lines, the code iterates over all the child controls and tells them to measure themselves by using the layoutChild() method. After we call this method on a particular child, we can then access that child’s actual dimensions using the getWidth() and getHeight() methods.

int currX=0, currY=0;//the current position at which to insert
int maxX=0;	//max width of the panel
int maxY=0;	//the maximum height of the current panel line 
		//(elements can be of different heights)

int count=getFieldCount();
for(int i=0;i<count;i++){
	Field f=getField(i);
	//lay out the element inside the panel
	layoutChild(f, width, height);
	//...

The next lines of code determine the width and height of each child field. These include the margins for each element. The code than tries to position the elements from left to right and from top to bottom.

//...
int fw=f.getWidth()+f.getMarginLeft()+f.getMarginRight();
int fh=f.getHeight()+f.getMarginTop()+f.getMarginBottom();
//...

After the dimension of the current field is retrieved, the code checks to see if a new line is required. If it is, we store the maximum width thus far, we reset the x position and we increment the y position.

//...
if((currX+fw) > width && currX!=0){
	//set the max width before resetting the x position
	if(maxX<currX)maxX=currX;
	//reset x
	currX=0;
	//update the total height thus far (and the max line height 
	//to the current y position)
	currY+=maxY;
	//the new line is empty at this point and so it has a height of 0
	maxY=0;
}
//...

The next lines of code position the field at the current position taking into account the top and left margins. After the element is positioned, the new line height is calculated and the horizontal offset is updated.

//position the current element
setPositionChild(f, currX+f.getMarginLeft(), currY+f.getMarginTop());
//after the element is added calculate a new max height for the line
maxY=Math.max(maxY, fh);
//update the x position
currX+=fw;

After all the fields have been iterated over, we need to adjust the final size of the manager. This is done by adding the max height of the final line to the existing height and by recalculating the maximum width. At the end, the setExtent() method is called.

Implementing the Message Bubble Manager

In this section, I will talk about the implementation of the speech bubble. The speech bubble is also implemented with a custom field manager. There are some additional things to consider here besides the implementation of the sublayout() method.

Another important part of the implementation is the background painting. In order to preserve resources, the images used to paint the background of the speech bubbles are loaded only once and are held in static variables. The code for this can be seen below:

private static final Bitmap sent_bubble = Bitmap.getBitmapResource("bubble_sent.png");
private static final Bitmap sent_left_bar = Bitmap.getBitmapResource("sent_left.png");
private static final Bitmap sent_top_bar = Bitmap.getBitmapResource("sent_top.png");
private static final Bitmap sent_right_bar = Bitmap.getBitmapResource("sent_right.png");
private static final Bitmap sent_bottom_bar = Bitmap.getBitmapResource("sent_bottom.png");
private static final Bitmap sent_inside_bubble = 
			Bitmap.getBitmapResource("sent_inside.png");

private static final Bitmap rec_bubble = Bitmap.getBitmapResource("bubble_received.png");
private static final Bitmap rec_left_bar = Bitmap.getBitmapResource("received_left.png");
private static final Bitmap rec_top_bar = Bitmap.getBitmapResource("received_top.png");
private static final Bitmap rec_right_bar = Bitmap.getBitmapResource("received_right.png");
private static final Bitmap rec_bottom_bar = 
			Bitmap.getBitmapResource("received_bottom.png");
private static final Bitmap rec_inside_bubble = 
			Bitmap.getBitmapResource("received_inside.png");

The image below presents these resources. The code that paints the background will use different parts of these resources to paint the speech bubble. I will talk about this a bit later.

Besides these image resource, another set of constants that the class uses are those that represent the various margins. I have defined margins for the bubble, the text and the images. These constants can be seen in the code below:

private static final int BUBBLE_MARGIN=5;
//text margins for out bubbles
private static final int TEXT_MARGIN_TOP=2;
private static final int TEXT_MARGIN_BOTTOM=0;
private static final int OUT_TEXT_MARGIN_RIGHT=17;
private static final int OUT_TEXT_MARGIN_LEFT=5;
//text margins for in bubbles
private static final int IN_TEXT_MARGIN_RIGHT=5;
private static final int IN_TEXT_MARGIN_LEFT=17;
//asset margins
private static final int ASSET_MARGIN_TOP=4;
private static final int ASSET_MARGIN_BOTTOM=6;

Some of the values for these margins depend on the image resources, so if you want to change the way you paint the background, you will also have to adjust some of these margins.

The last private variables are the ones used to hold the message data and the ones used to display this data. These variables can be seen in the listing below:

private String message;
private int direction;
private WrapPanelManager wrap;
private EditField lbl;

The message bubble’s constructor performs all the necessary initialization. The constructor requires the message text and direction. The constructor code can be seen below:

public MessageBubble(String message, int direction){
	super(direction==DIRECTION_IN?Manager.FIELD_LEFT:Manager.FIELD_RIGHT);
	this.direction=direction;
	this.message=message;
	setMargin(BUBBLE_MARGIN, BUBBLE_MARGIN, BUBBLE_MARGIN, BUBBLE_MARGIN);
	//init the contents of the bubble
	lbl=new EditField(Field.FIELD_LEFT);lbl.setEditable(false);
	wrap=new WrapPanelManager(Field.FIELD_LEFT);
	//establish the margins depending on the direction of the message
	if(direction==DIRECTION_OUT){
		lbl.setMargin(TEXT_MARGIN_TOP, OUT_TEXT_MARGIN_RIGHT, 
				TEXT_MARGIN_BOTTOM, OUT_TEXT_MARGIN_LEFT);
		wrap.setMargin(ASSET_MARGIN_TOP, OUT_TEXT_MARGIN_RIGHT, 
				ASSET_MARGIN_BOTTOM, OUT_TEXT_MARGIN_LEFT);

	}else{
		lbl.setMargin(TEXT_MARGIN_TOP, IN_TEXT_MARGIN_RIGHT, 
				TEXT_MARGIN_BOTTOM, IN_TEXT_MARGIN_LEFT);
		wrap.setMargin(ASSET_MARGIN_TOP, IN_TEXT_MARGIN_RIGHT, 
				ASSET_MARGIN_BOTTOM, IN_TEXT_MARGIN_LEFT);

	}

	if(message!=null && message.trim().length()>0){
		lbl.setText(message);
		add(lbl);
	}
	add(wrap);
}

As you can see from the code above, based on the direction of the message, the style for the speech bubble is set to either Manager.FIELD_LEFT or Manager.FIELD_RIGHT.

The constructor also sets the required margins for the text and image margins. At the end, depending on the message content, the label is added to the manager. The wrap panel is always added to the manager.

In order to implement the message bubble, we will need to override 2 methods. We need to override the paintBackground() method in order to paint the speech bubbles and we need to override the sublayout() method in order to arrange the bubble contents and to set its size.

The following paragraphs will describe the implementation of the paintBackground() override.

The first thing the method will do is to retrieve the bubble dimensions and the direction of the message.

protected void paintBackground(Graphics g) {
	//paint my bubble
	int col=g.getColor();

	int height = this.getContentHeight();
	int width = this.getContentWidth();

	if(width>=33 && height>=28){
		if(direction == DIRECTION_OUT){
	//...

If the message is an outgoing message, we will use the green resources to paint the background. We will first paint the bubble corners by using the drawBitmap() method.

//...
//draw corners
g.drawBitmap(0, 0, 14, 14, sent_bubble, 0, 0); //left top
g.drawBitmap(width-19, 0, 19, 14, sent_bubble, 24, 0);//right top
g.drawBitmap(0, height-14, 14, 14, sent_bubble, 0, 18);//left bottom
g.drawBitmap(width-19, height-14, 19, 14, sent_bubble, 24, 18);	//right bottom
//...

The parts of the sent_bubble resource that are used can be seen in the image below:

The next step is to paint the rest of the bubble. This will be done with the help of the tileRop() method. Like the name says, the method will tile the specified image. The code for painting the rest of the bubble can be seen below:

//...
//draw borders
g.tileRop(Graphics.ROP_SRC_ALPHA, 14, 0,
		width-33, 14, sent_top_bar, 0, 0);
g.tileRop(Graphics.ROP_SRC_ALPHA, 0, 14, 14,
		height-28, sent_left_bar, 0, 0);
g.tileRop(Graphics.ROP_SRC_ALPHA, 14, height-14,
		width-33, 14, sent_bottom_bar, 0, 0);
g.tileRop(Graphics.ROP_SRC_ALPHA, width-19, 14, 19,
		height-28, sent_right_bar, 0, 0);
//draw inside bubble
g.tileRop(Graphics.ROP_SRC_ALPHA, 14, 14, width-33,
		height-28, sent_inside_bubble, 0, 0);
//...

The incoming bubble will be painted in the same way. The only difference is that the gray resources will be used. The code can be seen below:

//...
	} else{
		//draw corners
		g.drawBitmap(0, 0, 19, 14, rec_bubble, 0, 0);//left top
		g.drawBitmap(width-14, 0, 14, 14, rec_bubble, 29, 0);//right top
		g.drawBitmap(0, height-14, 19, 14, rec_bubble, 0, 18);//left bottom
		g.drawBitmap(width-14, height-14, 14, 14, 
					rec_bubble, 29, 18);//right bottom
		//draw borders
		g.tileRop(Graphics.ROP_SRC_ALPHA, 19, 0,
				width-33, 14, rec_top_bar, 0, 0);
		g.tileRop(Graphics.ROP_SRC_ALPHA, 0, 14, 19,
				height-28, rec_left_bar, 0, 0);
		g.tileRop(Graphics.ROP_SRC_ALPHA, 19, height-15,
				width-33, 14, rec_bottom_bar, 0, 0);
		g.tileRop(Graphics.ROP_SRC_ALPHA, width-14, 14, 14,
				height-28, rec_right_bar, 0, 0);
		//draw inside bubble
		g.tileRop(Graphics.ROP_SRC_ALPHA, 19, 14, width-33,
				height-28, rec_inside_bubble, 0, 0);
	}
	super.paintBackground(g);
}

The last method to override is the sublayout() method. The implementation can be seen below:

protected void sublayout(int width, int height) {
	//get the maximum width of the bubble
	int maxBubbleWidth=width*3/4;
	//get the text width
	int realTextWidth = 0;
	if(message!=null && message.trim().length()>0)
		realTextWidth = getFont().getAdvance(message);
	//call layoutChild on the contents of the bubble
	if(realTextWidth>0)
		layoutChild(lbl, Math.min(maxBubbleWidth, realTextWidth), height);
	layoutChild(wrap, maxBubbleWidth, height);
	//position the elements
	if(realTextWidth>0)
		setPositionChild(lbl, lbl.getMarginLeft(), lbl.getMarginTop());
	int marginTop=wrap.getMarginTop();
	if(realTextWidth>0)
		marginTop+=(lbl.getHeight()+lbl.getMarginBottom()+lbl.getMarginTop());
	setPositionChild(wrap, wrap.getMarginLeft(), marginTop);
	//get the length of the wrap panel
	int realWrapWidth=wrap.getContentWidth();
	//maximum of text width and wrap width
	int w=Math.max(Math.min(realTextWidth,maxBubbleWidth), 
			Math.min(realWrapWidth, maxBubbleWidth));
	w+=+lbl.getMarginLeft()+lbl.getMarginRight();
	//set the size of the bubble
	if(realTextWidth>0){
		int h=lbl.getHeight()+wrap.getHeight()+
			lbl.getMarginTop()+lbl.getMarginBottom()+
				wrap.getMarginTop()+wrap.getMarginBottom();
		setExtent(Math.max(33, w), Math.max(28, h));
	}else{
		int h = wrap.getHeight()+
				wrap.getMarginTop()+wrap.getMarginBottom();
		setExtent(Math.max(33, w), Math.max(28, h));
	}
}

The first lines of code determine the maximum width of the bubble (3/4 of the available width) as well as the length of the text inside the message by using the getAdvance() method.

This length is determined only if there is text in the bubble.

The code then calls layoutChild() on the 2 child fields. You can see from the code that the width of the text is restricted to the minimum between the text length and the bubble’s maximum width. Also the EditField representing the bubble text is only measured if its length is greater than 0.

if(realTextWidth>0)
	layoutChild(lbl, Math.min(maxBubbleWidth, realTextWidth), height);
layoutChild(wrap, maxBubbleWidth, height);

After the children are laid out, they are positioned inside the bubble using the setPositionChild() method. For this part, the code also takes into account the length of the text and the margins of the elements.

//position the elements
if(realTextWidth>0)
	setPositionChild(lbl, lbl.getMarginLeft(), lbl.getMarginTop());
int marginTop=wrap.getMarginTop();
if(realTextWidth>0)
	marginTop+=(lbl.getHeight()+lbl.getMarginBottom()+lbl.getMarginTop());
setPositionChild(wrap, wrap.getMarginLeft(), marginTop);

At the end of the method, the size of the bubble manager is set. The height is easily calculated by adding the height of the 2 children as well as the top and bottom margins, also taking into account the possible lack of text.

The width is a little trickier to calculate. The width is determined by choosing the maximum between the text width and the images width. We also need to add to this value the left and right margins.

//get the length of the wrap panel
int realWrapWidth=wrap.getContentWidth();
//maximum of text width and wrap width
int w=Math.max(Math.min(realTextWidth,maxBubbleWidth), 
		Math.min(realWrapWidth, maxBubbleWidth));
w+=+lbl.getMarginLeft()+lbl.getMarginRight();
//set the size of the bubble
if(realTextWidth>0){
	int h=lbl.getHeight()+wrap.getHeight()+
		lbl.getMarginTop()+lbl.getMarginBottom()+
			wrap.getMarginTop()+wrap.getMarginBottom();
	setExtent(Math.max(33, w), Math.max(28, h));
}else{
	int h = wrap.getHeight()+
			wrap.getMarginTop()+wrap.getMarginBottom();
	setExtent(Math.max(33, w), Math.max(28, h));
}

Using the Message Bubble

In order to test the message bubble, I am going to generate a few messages. These messages are generated in the code below:

public void addMessages(){
	//generate and add the message bubbles
	int count =30;
	for(int i=0;i<count;i++){
		int dir=(i%2);
		String msg=texts[i%3];
		MessageBubble bbl=new MessageBubble(msg, dir);
		for(int j=0;j<i%9;j++){
			bbl.addAsset(bmps[j%bmps.length]);
		}

		this.add(bbl);
	}//end for
}

The result can be seen in the image below:

Final Thoughts

At first, I wanted to implement the speech bubble by deriving from the VerticalFieldManager class. There were a lot of issues with the bubble width in that implementation. I just couldn't constrain with the correct way because I also needed to call the base setExtent method. In the end, deriving the message bubble from the base Manager class solved the issues.

I think this is it. If you like the article and you think this code will be useful to you, please take a minute to comment and vote.

History

  • 10/6/2011 - Initial release
  • 10/11/2011 - Updated article contents and code sample

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here