一、简述

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。

发表评论

邮箱地址不会被公开。 必填项已用*标注