为Blackberry应用程序设计和实现语音气泡






4.85/5 (13投票s)
本文介绍了如何在Blackberry设备上运行的应用程序中实现语音气泡(或消息气泡)。

引言
语音气泡(或消息气泡)越来越受欢迎。它们现在几乎出现在所有平台的众多移动消息应用程序中(iOS、Android、RIM和WP7)。在本文中,我将介绍我在RIM平台上对这一非常漂亮的UI功能的实现。
文章目录
语音气泡要求
在深入介绍语音气泡的实现之前,我将先谈谈语音气泡需要支持的功能。这些功能可以在下面的列表中看到。
- 气泡将需要具有可变大小。出于美观原因,气泡的宽度不应超过屏幕宽度的四分之三。
- 气泡应同时包含文本和图像。文本将首先显示,然后是文本下方的图像列表。
- 图像需要换行(如果有很多),以遵守气泡的最大宽度。
下面图片中可以看到这些功能的图形示例。

从上图可以看出,如果语音气泡只包含图像,则没有文本可用的空间。如果只有文本(没有为图像分配空间),也会发生同样的情况。
实现
本节将介绍语音气泡的实现。正如您可能已经猜到的,语音气泡包含两个部分:文本部分和图像部分。这两部分都包含在语音气泡管理器中。同时,所有消息图像都包含在另一个管理器中。该管理器是一个包裹面板。
本节的第一部分将介绍包裹面板的实现。之后,我将介绍语音气泡管理器的实现。
实现包裹面板管理器
当我考虑如何实现图像面板时,我试图使用现有的字段管理器(使用FlowFieldManager
类)。在花费几个小时尝试使其工作后,我放弃了,并决定实现自己的包裹面板。本节将介绍此实现。
为了实现自定义字段管理器,唯一的要求是重写sublayout()
方法。在此方法体内,需要完成两件事。第一件事是通过调用layoutChild()
方法来测量字段并定位它们,以及通过调用setPositionChild()
方法来定位它们;第二件事是在方法体结束时(退出方法之前)设置管理器的尺寸。
下面提供的代码列表显示了这个管理器的实现。
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);
}
sublayout()
方法接受两个参数。这些参数代表管理器可用的最大尺寸。
代码的前几行初始化了几个辅助变量。在这些行之后,代码会遍历所有子控件,并通过使用layoutChild()
方法告诉它们测量自身。在我们对特定子项调用此方法后,就可以使用getWidth()
和getHeight()
方法访问该子项的实际尺寸了。
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();
//...
检索当前字段的尺寸后,代码会检查是否需要换行。如果需要,我们就存储到目前为止的最大宽度,重置x坐标,并增加y坐标。
//...
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;
在遍历完所有字段后,我们需要调整管理器的最终尺寸。这可以通过将最后一行最大高度添加到现有高度并重新计算最大宽度来完成。最后,调用setExtent()
方法。
实现消息气泡管理器
在本节中,我将讨论语音气泡的实现。语音气泡也使用自定义字段管理器实现。除了sublayout()
方法的实现之外,这里还有一些额外的考虑因素。
实现的另一个重要部分是背景绘制。为了节省资源,用于绘制语音气泡背景的图像只加载一次,并保存在static
变量中。代码如下所示。
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");
下图显示了这些资源。用于绘制背景的代码将使用这些资源的各个部分来绘制语音气泡。我稍后会讨论这一点。

除了这些图像资源,该类使用的另一组常量是代表各种边距的常量。我已经为气泡、文本和图像定义了边距。这些常量可以在下面的代码中看到。
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;
其中一些边距值取决于图像资源,因此如果您想更改背景的绘制方式,也必须调整其中一些边距。
最后private
变量是用于保存消息数据和显示这些数据的变量。这些变量可以在下面的列表中看到。
private String message;
private int direction;
private WrapPanelManager wrap;
private EditField lbl;
消息气泡的构造函数执行所有必要的初始化。构造函数需要消息文本和方向。构造函数代码如下所示。
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);
}
从上面的代码可以看出,根据消息的方向,语音气泡的样式被设置为Manager.FIELD_LEFT
或Manager.FIELD_RIGHT
。
构造函数还设置了文本和图像边距所需的边距。最后,根据消息内容,将标签添加到管理器中。包裹面板始终添加到管理器中。
为了实现消息气泡,我们需要重写两个方法。我们需要重写paintBackground()
方法来绘制语音气泡,并重写sublayout()
方法来排列气泡内容并设置其大小。
接下来的段落将描述paintBackground()
覆盖的实现。
方法将做的第一件事是检索气泡尺寸和消息的方向。
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){
//...
如果消息是发出的消息,我们将使用绿色资源来绘制背景。我们将首先使用drawBitmap()
方法绘制气泡角。
//...
//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
//...
下面图片中可以看到使用的sent_bubble
资源的部分。

下一步是绘制气泡的其余部分。这将借助tileRop()
方法完成。顾名思义,该方法将平铺指定的图像。绘制气泡其余部分的绘制代码如下所示。
//...
//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);
//...
接收消息的气泡将以相同的方式绘制。唯一的区别是使用灰色资源。代码如下所示。
//...
} 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);
}
要覆盖的最后一个方法是sublayout()
方法。实现如下所示。
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));
}
}
代码的前几行使用getAdvance()
方法确定气泡的最大宽度(可用宽度的3/4)以及消息中文本的长度。
此长度仅在气泡中有文本时确定。
然后代码对两个子字段调用layoutChild()
。从代码中可以看到,文本的宽度被限制在文本长度和气泡最大宽度之间的最小值。此外,表示气泡文本的EditField
仅在其长度大于0
时才进行测量。
if(realTextWidth>0)
layoutChild(lbl, Math.min(maxBubbleWidth, realTextWidth), height);
layoutChild(wrap, maxBubbleWidth, height);
在子项布局完成后,使用setPositionChild()
方法将它们定位在气泡内。对于这部分,代码还考虑了文本的长度和元素的边距。
//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));
}
使用消息气泡
为了测试消息气泡,我将生成一些消息。这些消息在下面的代码中生成。
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
}
结果可以在下面的图片中看到。

最终想法
最初,我想通过派生自VerticalFieldManager
类来实现语音气泡。在该实现中,气泡宽度存在很多问题。我只是无法以正确的方式约束它,因为我还必须调用基类的setExtent
方法。最终,从基类Manager
派生消息气泡解决了这些问题。
我认为就是这样了。如果您喜欢这篇文章,并认为此代码对您有用,请花一分钟发表评论并投票。
历史
- 2011年10月6日 - 初始发布
- 2011年10月11日 - 更新了文章内容和代码示例