某天,設(shè)計(jì)師同學(xué)Syson問(wèn)我,怎么在安卓上做出類(lèi)似iOS上的按鈕點(diǎn)擊效果,也就是在手指按下去的時(shí)候,按鈕中非透明的部分變暗,是不是應(yīng)該給每個(gè)按鈕出兩個(gè)素材。
這個(gè)效果在iOS上是自帶的,但是該怎么在安卓實(shí)現(xiàn)呢。上網(wǎng)查了查,發(fā)現(xiàn)可以使用ColorFilter對(duì)Drawable的東西做改變,這樣,設(shè)計(jì)師就不用為每個(gè)按鈕出兩套素材了。
看到ColorFilter這個(gè)詞,猜猜意思大概就是類(lèi)似濾鏡的存在,應(yīng)該就是我想要找的東西了吧。但是剛看了一眼Drawable中的setColorFilter方法,就已經(jīng)被其中的各種PorterDuff.Mode中的模式弄暈了。官方文檔中對(duì)ColorFilter中需要設(shè)置的PorterDuff.Mode的解釋也都是我看不懂的公式-.-還好找到了幾篇比較通俗的講解,所以大概了解了一下每一種Mode直觀上的意思,當(dāng)然啦沒(méi)學(xué)過(guò)圖形學(xué)還是不能完完全全理解,所以這里寫(xiě)的只是我覺(jué)得比較好理解的方式吧。
先要說(shuō):PorterDuff是什么,因?yàn)槲彝耆床欢@兩個(gè)詞拼在一起是什么意思。后來(lái)發(fā)現(xiàn),這居然是兩個(gè)人的姓!Thomas Porter和Tom Duff在1984年發(fā)了一篇paper,提出了帶alpha通道的數(shù)字圖像的組合運(yùn)算(可以這么說(shuō)么?),反正就是很厲害的樣子-.-。
進(jìn)入正題~
當(dāng)我們描述一個(gè)像素點(diǎn)的時(shí)候,可以用RGBA四個(gè)維度來(lái)描述它。其中的A是alpha,它的值在0到1之間。0表示了這個(gè)像素點(diǎn)是完全透明的。1表示了這個(gè)像素點(diǎn)是完全不透明的。為了方便理解,假設(shè)我手頭有兩張圖片,每一張的每一個(gè)像素點(diǎn),要么是全不透明,要么是全透明,也就是說(shuō)現(xiàn)在暫時(shí)沒(méi)有alpha在(0, 1)之間的像素點(diǎn)。
比如這樣兩張圖片:紅色的圓形和藍(lán)色的方形。


現(xiàn)在我們把紅色的圓形這張圖放在下面,將藍(lán)色的方形這張圖疊上去。這時(shí),每一個(gè)像素點(diǎn)的位置可能會(huì)有四種情況:
- 這兩張圖片,在這一個(gè)位置上的像素點(diǎn)都是透明;
- 這兩張圖片,在這一個(gè)位置上的像素點(diǎn)都是不透明的;
- 紅色的圓形圖片中,在這個(gè)位置的像素點(diǎn)是透明的,而藍(lán)色的方形圖片中,在這個(gè)位置上的像素點(diǎn)是不透明的;
- 藍(lán)色的方形圖片中,在這個(gè)位置的像素點(diǎn)是透明的,而紅色的圓形圖片中,在這個(gè)位置上的像素點(diǎn)是不透明的;
那么這兩張圖片上下疊起來(lái)之后,最后呈現(xiàn)在這個(gè)像素點(diǎn)的位置的顏色應(yīng)該是什么呢?如果我們找了一張新的畫(huà)布,要把最后呈現(xiàn)的樣子畫(huà)到新的畫(huà)布上,那么新的畫(huà)布上的每個(gè)像素點(diǎn),已經(jīng)默默的根據(jù)上面的四種情況被分為了四類(lèi)。
現(xiàn)在我們要對(duì)新的畫(huà)布涂顏色了。新畫(huà)布上每一個(gè)像素點(diǎn)應(yīng)該涂什么顏色呢?因?yàn)檫@個(gè)顏色不能憑空變出來(lái),所以得遵循這樣的規(guī)則(為了好理解,這是以alpha只能為0或1為前提的,如果alpha是其它值,就不一定是這樣啦):
| 像素點(diǎn)的類(lèi)型 | 都是透明的 | 都是不透明的 | 只在紅色的圓形圖片中是透明的 | 只在藍(lán)色的方形圖片中是透明的 | |
|---|---|---|---|---|---|
| 可以涂什么顏色 | 只能涂透明色 | 能涂透明色、藍(lán)色或紅色 | 能涂透明色或藍(lán)色 | 能涂透明色或紅色 |
這樣以來(lái),對(duì)于整張圖,我們一共得到了1X3X2X2也就是12種規(guī)則。
這12種規(guī)則可以類(lèi)比PorterDuff的12種模式,當(dāng)然也是在alpha值只能為0或1的前提下。表格里“R”表示涂紅色,“B”表示涂藍(lán)色,“0”表示涂透明色。
| PorterDuff模式 | 都是透明的 | 都是不透明的 | 只在紅色的圓形圖片中是透明的 | 只在藍(lán)色的方形圖片中是透明的 | |
|---|---|---|---|---|---|
| CLEAR | 0 | 0 | 0 | 0 | |
| SRC | 0 | B | B | 0 | |
| DST | 0 | R | 0 | R | |
| SRC_OVER | 0 | B | B | R | |
| DST_OVER | 0 | R | B | R | |
| SRC_IN | 0 | B | 0 | 0 | |
| DST_IN | 0 | R | 0 | 0 | |
| SRC_OUT | 0 | 0 | B | 0 | |
| DST_OUT | 0 | 0 | 0 | R | |
| SRC_ATOP | 0 | B | 0 | R | |
| DST_ATOP | 0 | R | B | 0 | |
| XOR | 0 | 0 | B | R |
畫(huà)圖直觀感受一下:

感受就是,這好像和“邏輯運(yùn)算”有著隱隱的關(guān)系-.-,這里DST代表了壓在下面的圖片,比如例子中的紅色的圓形圖片,SRC代表了疊在上面的圖片,比如例子中的藍(lán)色的方形圖片。
好啦,那么在alpha可以是[0, 1]中的任意值的情況下呢?
現(xiàn)在我們讓紅色的圓形和藍(lán)色的方形的都變成半透明的:


先用這張圖直觀的感受一下:

能發(fā)現(xiàn),這里產(chǎn)生了除了帶透明度的藍(lán)色、帶透明度的紅色之外的顏色,比如SRC_OVER和DST_OVER中重疊部分的顏色。那么這些顏色都是怎么被創(chuàng)造出來(lái)的呢?安卓的官方文檔中記載了PorterDuff的各種模式的計(jì)算公式。

雖然第一眼看不太懂,但是查了一下各個(gè)變量代表的意思:
Sa全稱(chēng)為Source alpha表示源圖的Alpha通道;
Sc全稱(chēng)為Source color表示源圖的顏色;
Da全稱(chēng)為Destination alpha表示目標(biāo)圖的Alpha通道;
Dc全稱(chēng)為Destination color表示目標(biāo)圖的顏色;
得到的結(jié)果以[alpha, color]的形式表示。
雖然我想盡力多知道一些為什么,但是好多地方還是不那么理解,所以目前只能知道這些TAT:
- 兩張圖“重疊部分”之外的顏色,要么是透明,要么就是原來(lái)的顏色;
- CLEAR模式下,整張圖都是透明的;
- SRC模式下得到的圖,就是SRC;DST模式下得到的圖,就是DST;
對(duì)了,除了這12中模式,PorterDuff中還剩下的6種模式(ADD、DARKEN、LIGHTEN、MULTIPLY、OVERLAY、SCREEN)是什么呢?
如果我沒(méi)理解錯(cuò)的話,這6種模式得到的效果,和Photoshop里對(duì)應(yīng)的混合模式得到的效果是一致的(好像MULTIPLY是不完全一致0.0)。
效果:

終于把PorterDuff的幾種模式整理了一下,現(xiàn)在回到最開(kāi)頭的問(wèn)題:應(yīng)該怎么在安卓的button中實(shí)現(xiàn)iOS上的點(diǎn)擊效果呢?
Drawable類(lèi)中有一個(gè)setColorFilter()方法,它可以接收兩個(gè)參數(shù):顏色的色值和PorterDuff模式。通過(guò)setColorFilter()設(shè)置顏色,相當(dāng)于在Drawable對(duì)象上加上了一層純色,并采用對(duì)應(yīng)PorterDuff模式來(lái)顯示得到最終效果。
通過(guò)Button類(lèi)中g(shù)etBackground(),我們可以得到按鈕的圖標(biāo)對(duì)應(yīng)的Drawable對(duì)象,setColorFilter()方法正好可以施加在按鈕的圖標(biāo)上。
考慮到我們需要得到的最終效果是,背景圖案上透明的部分仍然透明,而背景圖案上不透明的部分蒙上設(shè)置的顏色,所以可以選擇SRC_ATOP模式或者M(jìn)ULTIPLY模式。
使用SRC_ATOP模式的代碼片段:
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
Button touchedButton = (Button)view;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchedButton.getBackground().setColorFilter(0x77FFFF00, PorterDuff.Mode.SRC_ATOP);
touchedButton.invalidate();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchedButton.getBackground().clearColorFilter();
touchedButton.invalidate();
break;
}
return true;
}
});
效果:


寫(xiě)這篇文章時(shí)遇到的一個(gè)問(wèn)題:
文章中紅色圓形和藍(lán)色方塊的例子,是我在安卓sample code的基礎(chǔ)上加以修改然后生成的圖片。(雖然傳說(shuō)android studio自帶sample code,但是不知道我這出了什么問(wèn)題沒(méi)弄出來(lái),所以參考的是這里的。)這段代碼生成出來(lái)的圖片應(yīng)該是這樣的(隨便google一下全是這張圖-.-):

讓我覺(jué)得很奇怪的地方是,這里的CLEAR不是全透明的。我奇怪了很久google了很久也改代碼改了很久,確認(rèn)我對(duì)CLEAR的理解沒(méi)有錯(cuò)。最后發(fā)現(xiàn)了真相:
造成這個(gè)問(wèn)題的原因是硬件加速。需要在View中關(guān)閉硬件加速才能得到正確的結(jié)果。
比如在構(gòu)造函數(shù)中調(diào)用:
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
關(guān)閉硬件加速后,就能生成正確的結(jié)果了。生成之后,我發(fā)現(xiàn)上張圖中的Darken和Lighten也是錯(cuò)誤的喲。
參考資料:
click effect on button in Android
Porter/Duff Compositing and Blend Modes
PorterDuff.Mode
Alpha compositing
PorterduffXfermode: Clear a section of a bitmap
詳解Paint的setXfermode
Android example source code file (Xfermodes.java)