言大官人 2018-02-09
最近在使用Libgdx进行游戏大厅开发,遇到这种需求:为个别文本控件(Label)设置纯色透明的圆角矩形背景。
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;
然而,pixmap只提供了如下几种绘制图形的方法:
pixmap.drawLine() // 画线 pixmap.drawRectangle(); // 画矩形 pixmap.drawCircle(); // 画环 pixmap.fillTriangle(); // 填充三角形 pixmap.fillRectangle(); // 填充矩形 pixmap.fillCircle(); // 填充圆形
我要的圆角矩形正好没有(毕竟圆角矩形不是简单图形是吧。。。),于是,经过google大法及本人的"缜密"思考之后,纯色透明圆角矩形实现出来了,本篇将记录两种实现圆角矩形的方案,下面开始进入正题。
这个方案借鉴了一个歪果人的博文,本文为我之后的方案二做了启发,这里就先把地址贴出来,方便今后再翻出来欣赏:
博文原地址:LIBGDX – DRAWING A ROUNDED RECTANGLE PIXMAP
下面就开始强行“翻译”一下。
绘制出一个圆角矩形,实际上,可以通过使用填充了相同的颜色的2个矩形和4个圆圈来实现,这几个图形的摆放如下图所示。
通过上图,可以很清晰的明白原作者的实现思想,下面就开始码代码(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; }
为了直观的看出效果,我把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);
效果很棒,不得不说,歪果人的想法还是挺好的,但是,当我把圆角矩形的颜色设置为白色透明时,这效果就恶心了,这里贴出白透明色的设置代码:
Color color = new Color(1, 1, 1, 0.5f);
为什么会这样,仔细想想就能明白,这是因为pixmap在绘制这几个图形时,它们的重合部分透明度叠加了。
既然知道了原因,那有什么解决办法呢?这里列出我能想到的2个办法:
第一个方法我觉得是比较好的,感觉实现上比较简单可靠,然而我始终没有找到可以对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,但总感觉这种方案不是并最优方式。
通过一番思考之后,我得出了这样一个结论:
既然最后在使用到第2个pixmap的时候需要遍历所有像素点来重新绘制一遍,那我干脆直接进行第2步(第1步绘制不透明矩形的步骤不要了),在遍历所有像素的时候把需要绘制到pixmap的像素点绘制出来不就好了吗?这样做还可以省掉一个pixmap的开销。
那么现在的问题就是,我怎么知道哪些像素应该被绘制,哪些像素不要被绘制呢?其实可以把圆角矩形看成是一个不完整的有缺角的矩形,而这些缺角正好就是不用被绘制的那些像素点。
通过观察,可以知道,四个缺角中的像素都有如下相同点:
根据结论,代码实现如下:
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。