一、簡述
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大法及本人的”縝密”思考之後,純色透明圓角矩形實現出來了,本篇將記錄兩種實現圓角矩形的方案,下面開始進入正題。
二、方案一
這個方案借鑑了一個歪果人的博文,本文爲我之後的方案二做了啓發,這裏就先把地址貼出來,方便今後再翻出來欣賞:
下面就開始強行“翻譯”一下。
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。