Libgdx pixmap绘制透明圆角矩形
一、简述
1、需求
最近在使用Libgdx进行游戏大厅开发,遇到这种需求:为个别文本控件(Label)设置纯色透明的圆角矩形背景。
2、思路
Libgdx中的Label是提供背景设置的:对Label的Style的background属性进行设置即可,这个background是个Drawable,可以使用图片作为Label的背景,很好很强大,但我这个项目中的Label背景只需要一种透明颜色而已,用图片来实现的话我觉得并不是一种很好的方式(有种杀鸡用牛刀的感觉)。想来想去,认为Libgdx中的Pixmap可以帮助我实现这种需求,因为Pixmap是可以被用来绘制一个简单图形的,之后将pixmap转换成drawable赋值给background就好了:
Drawable bg = new TextureRegionDrawable(new TextureRegion(new Texture(pixmap)));
label.getStyle().background = bg;
3、难点
然而,pixmap只提供了如下几种绘制图形的方法:
pixmap.drawLine() // 画线
pixmap.drawRectangle(); // 画矩形
pixmap.drawCircle(); // 画环
pixmap.fillTriangle(); // 填充三角形
pixmap.fillRectangle(); // 填充矩形
pixmap.fillCircle(); // 填充圆形
我要的圆角矩形正好没有(毕竟圆角矩形不是简单图形是吧。。。),于是,经过google大法及本人的”缜密”思考之后,纯色透明圆角矩形实现出来了,本篇将记录两种实现圆角矩形的方案,下面开始进入正题。
二、方案一
这个方案借鉴了一个歪果人的博文,本文为我之后的方案二做了启发,这里就先把地址贴出来,方便今后再翻出来欣赏:
博文原地址:LIBGDX – DRAWING A ROUNDED RECTANGLE PIXMAP
下面就开始强行“翻译”一下。
1、原理
绘制出一个圆角矩形,实际上,可以通过使用填充了相同的颜色的2个矩形和4个圆圈来实现,这几个图形的摆放如下图所示。
2、实现
通过上图,可以很清晰的明白原作者的实现思想,下面就开始码代码(copy):
public Pixmap getRoundedRectangle(int width, int height, int radius, int color) {
Pixmap pixmap = new Pixmap(width, height, Pixmap.Format.RGBA8888);
pixmap.setColor(color);
// Pink rectangle
pixmap.fillRectangle(0, radius, pixmap.getWidth(), pixmap.getHeight() - 2 * radius);
// Green rectangle
pixmap.fillRectangle(radius, 0, pixmap.getWidth() - 2 * radius, pixmap.getHeight());
// Bottom-left circle
pixmap.fillCircle(radius, radius, radius);
// Top-left circle
pixmap.fillCircle(radius, pixmap.getHeight() - radius, radius);
// Bottom-right circle
pixmap.fillCircle(pixmap.getWidth() - radius, radius, radius);
// Top-right circle
pixmap.fillCircle(pixmap.getWidth() - radius, pixmap.getHeight() - radius, radius);
return pixmap;
}
3、效果
为了直观的看出效果,我把Demo的舞台背景渲染为黑色,圆角矩形设置为白色,下面列出demo中的部分代码:
Texture roundedRectangle = new Texture(getRoundedRectangle(color, width, height, radius));
Image image = new Image(roundedRectangle);
image.setPosition(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2, Align.center);
addActor(image);
4、缺陷
效果很棒,不得不说,歪果人的想法还是挺好的,但是,当我把圆角矩形的颜色设置为白色透明时,这效果就恶心了,这里贴出白透明色的设置代码:
Color color = new Color(1, 1, 1, 0.5f);
为什么会这样,仔细想想就能明白,这是因为pixmap在绘制这几个图形时,它们的重合部分透明度叠加了。
5、完善
既然知道了原因,那有什么解决办法呢?这里列出我能想到的2个办法:
先使用不透明颜色进行绘制,待所有图形绘制完成后,再来设置整体的透明度。
先用一个pixmap绘制出不透明圆角矩形,然后遍历所有的像素点,如果该像素不是透明的,则在另一个pixmap的相同位置,用rgb相同但a不同的颜色再绘制一次。
第一个方法我觉得是比较好的,感觉实现上比较简单可靠,然而我始终没有找到可以对pixmap设置整体透明度的方法,于是我这里采用了第二个方法来实现:
public Pixmap getRoundedRectangle(Color color, int width, int height, int radius) {
Pixmap pixmap = new Pixmap(width, height, Pixmap.Format.RGBA8888);
// 1、保存原先的透明度
float alpha = color.a;
// 2、将透明度设置为1之后开始绘制圆角矩形
color.set(color.r, color.g, color.b, 1);
pixmap.setColor(color);
// Pink rectangle
pixmap.fillRectangle(0, radius, pixmap.getWidth(), pixmap.getHeight() - 2 * radius);
// Green rectangle
pixmap.fillRectangle(radius, 0, pixmap.getWidth() - 2 * radius, pixmap.getHeight());
// Bottom-left circle
pixmap.fillCircle(radius, radius, radius);
// Top-left circle
pixmap.fillCircle(radius, pixmap.getHeight() - radius, radius);
// Bottom-right circle
pixmap.fillCircle(pixmap.getWidth() - radius, radius, radius);
// Top-right circle
pixmap.fillCircle(pixmap.getWidth() - radius, pixmap.getHeight() - radius, radius);
// 3、如果原来的背景色存在透明度,则需要对图形整体重绘一次
if (alpha != 1) {
Pixmap newPixmap = new Pixmap(pixmap.getWidth(), pixmap.getHeight(), pixmap.getFormat());
int r = ((int) (255 * color.r) << 16);
int g = ((int) (255 * color.g) << 8);
int b = ((int) (255 * color.b));
int a = ((int) (255 * alpha) << 24);
int argb8888 = new Color(r | g | b | a).toIntBits();
for (int y = 0; y < pixmap.getHeight(); y++) {
for (int x = 0; x < pixmap.getWidth(); x++) {
int pixel = pixmap.getPixel(x, y);
if ((pixel & color.toIntBits()) == color.toIntBits()) {
newPixmap.drawPixel(x, y, argb8888);
}
}
}
pixmap.dispose();
pixmap = newPixmap;
}
return pixmap;
}
来看下效果,嗯,还可以吧。
三、方案二(个人认为比较完美的方案)
虽然用2个pixmap的方式可以”完美”地绘制出纯色透明圆角矩形,但是,每创建出1个透明圆角矩形都必须创建出2个pixmap来为之辅助,尽管最后会对旧的pixmap进行dispose,但总感觉这种方案不是并最优方式。
1、原理
通过一番思考之后,我得出了这样一个结论:
既然最后在使用到第2个pixmap的时候需要遍历所有像素点来重新绘制一遍,那我干脆直接进行第2步(第1步绘制不透明矩形的步骤不要了),在遍历所有像素的时候把需要绘制到pixmap的像素点绘制出来不就好了吗?这样做还可以省掉一个pixmap的开销。
那么现在的问题就是,我怎么知道哪些像素应该被绘制,哪些像素不要被绘制呢?其实可以把圆角矩形看成是一个不完整的有缺角的矩形,而这些缺角正好就是不用被绘制的那些像素点。
通过观察,可以知道,四个缺角中的像素都有如下相同点:
在绿线与蓝线组成的小矩形区域中;
都不在圆上,换句话说就是点与圆心的距离超过半径。
2、实现
根据结论,代码实现如下:
public Pixmap getRoundedRectangle(Color color, int width, int height, int radius) {
Pixmap pixmap = new Pixmap(width, height, Pixmap.Format.RGBA8888);
pixmap.setColor(color);
for (int y = 0; y < pixmap.getHeight(); y++) {
for (int x = 0; x < pixmap.getWidth(); x++) {
if ((x >= 0 && x <= radius) && (y >= 0 && y <= radius)) { // bottom-left
if (Math.sqrt((radius - x) * (radius - x) + (radius - y) * (radius - y)) > radius) {
continue;
}
} else if ((x >= 0 && x <= radius) && (y >= (height - radius) && y <= height)) { // top-left
if (Math.sqrt((radius - x) * (radius - x) + ((height - radius) - y) * ((height - radius) - y)) > radius) {
continue;
}
} else if ((x >= (width - radius) && x <= width) && (y >= 0 && y <= radius)) {// bottom-right
if (Math.sqrt(((width - radius) - x) * ((width - radius) - x) + (radius - y) * (radius - y)) > radius) {
continue;
}
} else if ((x >= (width - radius) && x <= width) && (y >= (height - radius) && y <= height)) {// top-right
if (Math.sqrt(((width - radius) - x) * ((width - radius) - x) + ((height - radius) - y) * ((height - radius) - y)) > radius) {
continue;
}
}
pixmap.drawPixel(x, y);
}
}
return pixmap;
}
为了方便理解,下面列出各个缺角的圆心与小矩形x与y的取值范围:
// bottom-left
// ————圆心:(radius, radius)
// ————矩形:([0,radius], [0,radius])
// top-left
// ————圆心:(radius, height-radius)
// ————矩形:([0,radius], [height-radius,height])
// bottom-right
// ————圆心:(width-radius,radius)
// ————矩形:([width-radius,width], [0,radius])
// top-right
// ————圆心:(width-radius,height-radius)
// ————矩形:([width-radius,width], [height-radius,height])
结果是OK的,与方案一绘制出来的透明圆角矩形一致,并且少了一个pixmap的开销。
四、最后
最后,想多说两句,Libgdx作为一款优秀的Android端游戏开发引擎,网上的资料却相当的少,很多东西就算Google了也不一定能找到答案,本人也是最近才对其进行了解并上手使用,对于本文中所说的需求或许并不是最好的解决方式,如果您有什么好的解决方案或建议,请不吝赐教,thx。