上一回關羽操刀怒砍秦檜子龍拼命相救,豈料劉備這狗賊耍賴以張飛為祭品特殊召喚黑暗大法師消滅了場上所有逗逼,霎時間血流成河,鮮紅的血液與冰冷的大地融合交匯在一起煥發出血液的煞氣……那么,問題來了,請問這是使用了哪種PorterDuffXfermode?
在上一節的最后一個Example中我們做了一個橡皮擦的View,但是這個View雖然在效果上沒有什么問題,但是邏輯確實有問題的!你們發現了么?哥故意挖了個坑讓你們往里面跳哦!!!
在對Xfermode和ColorFilter有了深情的了解后我們不能只愛上這倆二貨,前方必定還有更多的好貨色在等著我們開發……^_^~!今天我們繼續向前看看Paint的其他一些“另類”的屬性。
筆對于我們來說第一印象一定是能寫字對吧,而Android給我們的這支Paint當然也不例外,它也定義了大量關于“寫字”的功能,這些方法總數接近Paint的一半!可見Android對Paint寫字功能的重視,在講Paint提供的“寫字”方法前我先給大家說一個Android中和字體相關的很重要的類
**FontMetrics**
FontMetrics意為字體測量,這么一說大家是不是瞬間感受到了這玩意的重要性?那這東西有什么用呢?我們通過源碼追蹤進去可以看到FontMetrics其實是Paint的一個內部類,而它里面呢就定義了top,ascent,descent,bottom,leading五個成員變量其他什么也沒有:

這五個成員變量除了top和bottom我們較熟悉外其余三個都很陌生是做什么用的呢?首先我給大家看張圖:

這張圖很簡單但是也很扼要的說明了top,ascent,descent,bottom,leading這五個參數。首先我們要知道Baseline基線,在Android中,文字的繪制都是從Baseline處開始的,Baseline往上至字符最高處的距離我們稱之為ascent(上坡度),Baseline往下至字符最底處的距離我們稱之為descent(下坡度),而leading(行間距)則表示上一行字符的descent到該行字符的ascent之間的距離,top和bottom文檔描述地很模糊,其實這里我們可以借鑒一下TextView對文本的繪制,TextView在繪制文本的時候總會在文本的最外層留出一些內邊距,為什么要這樣做?因為TextView在繪制文本的時候考慮到了類似讀音符號,可能大家很久沒寫過拼音了已經忘了什么叫讀音符號了吧……下圖中的A上面的符號就是一個拉丁文的類似讀音符號的東西:

然而根據世界范圍內已入案的使用語言中能夠標注在字符上方或者下方的除了類似的符號肯定是數不勝數的……哥不是語言專家我母雞啊……而top的意思其實就是除了Baseline到字符頂端的距離外還應該包含這些符號的高度,bottom的意思也是一樣,一般情況下我們極少使用到類似的符號所以往往會忽略掉這些符號的存在,但是Android依然會在繪制文本的時候在文本外層留出一定的邊距,這就是為什么top和bottom總會比ascent和descent大一點的原因。而在TextView中我們可以通過xml設置其屬性android:includeFontPadding="false"去掉一定的邊距值但是不能完全去掉。下面我們在Canvas上繪制一段文本并嘗試打印文本的top,ascent,descent,bottom和leading:
~~~
public class FontView extends View {
private static final String TEXT = "ap愛哥ξτβбпшㄎㄊěǔぬも┰┠№@↓";
private Paint mPaint;// 畫筆
private FontMetrics mFontMetrics;// 文本測量對象
public FontView(Context context) {
this(context, null);
}
public FontView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(50);
mPaint.setColor(Color.BLACK);
mFontMetrics = mPaint.getFontMetrics();
Log.d("Aige", "ascent:" + mFontMetrics.ascent);
Log.d("Aige", "top:" + mFontMetrics.top);
Log.d("Aige", "leading:" + mFontMetrics.leading);
Log.d("Aige", "descent:" + mFontMetrics.descent);
Log.d("Aige", "bottom:" + mFontMetrics.bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(TEXT, 0, Math.abs(mFontMetrics.top), mPaint);
}
}
~~~
logcat輸出如下:

注:Baseline上方的值為負,下方的值為正
如圖我們得到了top,ascent,descent,bottom和leading的值,因為只有一行文本所以leading恒為0,那么此時的顯示效果是如何的呢?上面我們說到Android中文本的繪制是從Baseline開始的,在屏幕上的體現便是Y軸坐標,所以在
`canvas.drawText(TEXT, 0, Math.abs(mFontMetrics.top), mPaint); `
中我們將文本繪制的起點Y坐標向下移動Math.abs(mFontMetrics.top)個單位(注:mFontMetrics.top是負數),相當于把文本的Baseline向下移動Math.abs(mFontMetrics.top)個單位,此時文本的頂部剛好會和屏幕頂部重合:

從代碼中我們可以看到一個很特別的現象,在我們繪制文本之前我們便可以獲取文本的FontMetrics屬性值,也就是說我們FontMetrics的這些值跟我們要繪制什么文本是無關的,而僅與繪制文本Paint的size和typeface有關我們來分別更改這兩個值看看:
`mPaint.setTextSize(70); `

如圖所示所有值都改變了,我們再為Paint設置一個typeface:
`mPaint.setTypeface(Typeface.SERIF);`

同樣所有的值也改變了,那么我們知道這樣的一個東西有什么用呢?如上所說文本的繪制是從Baseline開始,并且Baseline并非文本的分割線,當我們想讓文本繪制的時候居中屏幕或其他的東西時就需要計算Baseline的Y軸坐標,比如我們讓我們的文本居中畫布:
~~~
public class FontView extends View {
private static final String TEXT = "ap愛哥ξτβбпшㄎㄊ";
private Paint textPaint, linePaint;// 文本的畫筆和中心線的畫筆
private int baseX, baseY;// Baseline繪制的XY坐標
public FontView(Context context) {
this(context, null);
}
public FontView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(70);
textPaint.setColor(Color.BLACK);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(1);
linePaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 計算Baseline繪制的起點X軸坐標
baseX = (int) (canvas.getWidth() / 2 - textPaint.measureText(TEXT) / 2);
// 計算Baseline繪制的Y坐標
baseY = (int) ((canvas.getHeight() / 2) - ((textPaint.descent() + textPaint.ascent()) / 2));
canvas.drawText(TEXT, baseX, baseY, textPaint);
// 為了便于理解我們在畫布中心處繪制一條中線
canvas.drawLine(0, canvas.getHeight() / 2, canvas.getWidth(), canvas.getHeight() / 2, linePaint);
}
}
~~~
效果如圖:

Baseline繪制的起點x坐標為畫布寬度的一半(中點x坐標)減去文本寬度的一半(這里我們的畫布大小與屏幕大小一樣),這個很好理解,而y坐標為畫布高度的一半(中點y坐標)減去ascent和descent絕對值之差的一半,這一點很多朋友可能不是很好理解,其實很簡單,如果直接以畫布的中心為Baseline:
`baseY = canvas.getHeight() / 2;`
那么畫出來的效果必定是如下的樣子

也就是說Baseline和屏幕中線重合,而這樣子繪制出來的文本必定不在屏幕中心,因為ascent的距離大于descent的距離(大多數情況下我們沒有考慮top和bottom),那么我們就需要將Baseline往下移使繪制出來的文本能在中心

那么該下移多少呢?這是一個問題,很多童鞋的第一反應是下移ascent的一半高度,但是你要考慮到已經在中線下方的descent的高度,所以我們應該先在ascent的高度中減去descent的高度再除以二再讓屏幕的中點Y坐標(也就是高度的一半)加上這個偏移值
`baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(textPaint.ascent()-Math.abs(textPaint.descent()))) / 2));`
這個公式跟我們上面代碼中的是一樣的,不信大家可以自己算算這里就不多說了。這里我們的需求是讓文本繪制在某個區域的中心,實際情況中有很多不同的需求不如靠近某個區域離某個區域需要多少距離等等,熟練地去學會計算文本測繪中的各個值就顯得很有必要了!
Paint有一個唯一的子類TextPaint就是專門為文本繪制量身定做的“筆”,而這支筆就如API所描述的那樣能夠在繪制時為文本添加一些額外的信息,這些信息包括:baselineShift,bgColor,density,drawableState,linkColor,這些屬性都很簡單大家顧名思義或者自己去嘗試下即可這里就不多說了,那么這支筆有何用呢?最常用的用法是在繪制文本時能夠實現換行繪制!在正常情況下Android繪制文本是不能識別換行符之類的標識符的,這時候如果我們想實現換行繪制就得另辟途徑使用StaticLayout結合TextPaint實現換行,StaticLayout是android.text.Layout的一個子類,很明顯它也是為文本處理量身定做的,其內部實現了文本繪制換行的處理,該類不是本系列重點我們不再多說直接Look一下它是如何實現換行的:
~~~
public class StaticLayoutView extends View {
private static final String TEXT = "This is used by widgets to control text layout. You should not need to use this class directly unless you are implementing your own widget or custom display object, or would be tempted to call Canvas.drawText() directly.";
private TextPaint mTextPaint;// 文本的畫筆
private StaticLayout mStaticLayout;// 文本布局
public StaticLayoutView(Context context) {
this(context, null);
}
public StaticLayoutView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(50);
mTextPaint.setColor(Color.BLACK);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mStaticLayout = new StaticLayout(TEXT, mTextPaint, canvas.getWidth(), Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false);
mStaticLayout.draw(canvas);
canvas.restore();
}
}
~~~
運行效果如下:

好了,對Paint繪制文本的一個簡單了解就先到這,我們來看看Paint中到底提供了哪些實用的方法來繪制文本
**ascent()**
顧名思義就是返回上坡度的值,我們已經用過了
**descent()**
同上,不多說了
**breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)**
這個方法讓我們設置一個最大寬度在不超過這個寬度的范圍內返回實際測量值否則停止測量,參數很多但是都很好理解,text表示我們的字符串,start表示從第幾個字符串開始測量,end表示從測量到第幾個字符串為止,measureForwards表示向前還是向后測量,maxWidth表示一個給定的最大寬度在這個寬度內能測量出幾個字符,measuredWidth為一個可選項,可以為空,不為空時返回真實的測量值。同樣的方法還有breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)和breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)。這些方法在一些結合文本處理的應用里比較常用,比如文本閱讀器的翻頁效果,我們需要在翻頁的時候動態折斷或生成一行字符串,這就派上用場了~~~
**getFontMetrics (Paint.FontMetrics metrics)**
這個和我們之前用到的getFontMetrics()相比多了個參數,getFontMetrics()返回的是FontMetrics對象而getFontMetrics(Paint.FontMetrics metrics)返回的是文本的行間距,如果metrics的值不為空則返回FontMetrics對象的值。
**getFontMetricsInt()**
該方法返回了一個FontMetricsInt對象,FontMetricsInt和FontMetrics是一樣的,只不過FontMetricsInt返回的是int而FontMetrics返回的是float
**getFontMetricsInt(Paint.FontMetricsInt fmi)**
不扯了
**getFontSpacing()**
返回字符行間距
**setUnderlineText(boolean underlineText)**
設置下劃線
**setTypeface(Typeface typeface)**
設置字體類型,上面我們也使用過,Android中字體有四種樣式:BOLD(加粗),BOLD_ITALIC(加粗并傾斜),ITALIC(傾斜),NORMAL(正常);而其為我們提供的字體有五種:DEFAULT,DEFAULT_BOLD,MONOSPACE,SANS_SERIF和SERIF,這些什么類型啊、字體啊之類的都很簡單大家自己去試試就知道就不多說了。但是系統給我們的字體有限我們可不可以使用自己的字體呢?答案是肯定的!Typeface這個類中給我們提供了多個方法去個性化我們的字體
**defaultFromStyle(int style)**
最簡單的,簡而言之就是把上面所說的四種Style封裝成Typeface
**create(String familyName, int style)和create(Typeface family, int style)**
兩者大概意思都一樣,比如
~~~
textPaint.setTypeface(Typeface.create("SERIF", Typeface.NORMAL));
textPaint.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));
~~~
兩者效果是一樣的
`createFromAsset(AssetManager mgr, String path)、createFromFile(String path)和createFromFile(File path)`
這三者也是一樣的,它們都允許我們使用自己的字體比如我們從asset目錄讀取一個字體文件:
~~~
// 獲取字體并設置畫筆字體
Typeface typeface = Typeface.createFromAsset(context.getAssets(), "kt.ttf");
textPaint.setTypeface(typeface);
~~~
我們將會得到如下效果:

這里我用了一個卡通的字體,而另外兩個方法也類似的我就不講了。
說到文本大家第一時間想到的應該是TextView,其實在TextView里我們依然可以找到上面很多方法的影子,比如我們可以從TextView中獲取到TextPaint:
`TextPaint paint = mTextView.getPaint(); `
當然也可以設置TextView的字體等等:
~~~
Typeface typeface = Typeface.createFromAsset(getAssets(), "kt.ttf");
mTextView.setTypeface(typeface);
~~~
更多的雷同點還是留給大家去發掘,下面繼續來看
**setTextSkewX(float skewX)**
這個方法可以設置文本在水平方向上的傾斜,效果類似下圖:
~~~
// 設置畫筆文本傾斜
textPaint.setTextSkewX(-0.25F);
~~~

這個傾斜值沒有具體的范圍,但是官方推崇的值為-0.25可以得到比較好的傾斜文本效果,值為負右傾值為正左傾,默認值為0
**setTextSize (float textSize)**
不說了但是要注意該值必需大于零
**setTextScaleX (float scaleX)**
將文本沿X軸水平縮放,默認值為1,當值大于1會沿X軸水平放大文本,當值小于1會沿X軸水平縮放文本
~~~
// 設置畫筆文本傾斜
textPaint.setTextScaleX(0.5F);
~~~

~~~
// 設置畫筆文本傾斜
textPaint.setTextScaleX(1.5F);
~~~

大家注意哦!setTextScaleX不僅放大了文本寬度同時還拉伸了字符!這是亮點~~
**setTextLocale (Locale locale)**
設置地理位置,這個不講,我們會在屏幕適配系列詳解什么是Locale,這里如果你要使用,直接傳入Locale.getDefault()即可
**setTextAlign (Paint.Align align)**
設置文本的對其方式,可供選的方式有三種:CENTER,LEFT和RIGHT,其實從這三者的名字上看我們就知道其意思,但是問題是這玩意怎么用的?好像沒什么用啊……我們的文本大小是通過size和typeface確定的(其實還有其他的因素但這里影響不大忽略~~),一旦baseline確定,對不對齊好像不相干吧……但是,你要知道一點,文本的繪制是從baseline開始沒錯,但是是從哪邊開始繪制的呢?左端還是右端呢?而這個Align就是為我們定義在baseline繪制文本究竟該從何處開始,上面我們在進行對文本的水平居中時是用Canvas寬度的一半減去文本寬度的一半:
~~~
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 計算Baseline繪制的起點X軸坐標
baseX = (int) (canvas.getWidth() / 2 - textPaint.measureText(TEXT) / 2);
// 計算Baseline繪制的Y坐標
baseY = (int) ((canvas.getHeight() / 2) - ((textPaint.descent() + textPaint.ascent()) / 2));
canvas.drawText(TEXT, baseX, baseY, textPaint);
// 為了便于理解我們在畫布中心處繪制一條中線
canvas.drawLine(0, canvas.getHeight() / 2, canvas.getWidth(), canvas.getHeight() / 2, linePaint);
}
~~~
實際上我們大可不必這樣計算,我們只需設置Paint的文本對齊方式為CENTER,drawText的時候起點x = canvas.getWidth() / 2即可:
~~~
textPaint.setTextAlign(Align.CENTER);
canvas.drawText(TEXT, canvas.getWidth() / 2, baseY, textPaint);
~~~
當我們將文本對齊方式設置為CENTER后就相當于告訴Android我們這個文本繪制的時候從文本的中點開始向兩端繪制,如果設置為LEFT則從文本的左端開始往右繪制,如果為RIGHT則從文本的右端開始往左繪制:

**setSubpixelText (boolean subpixelText)**
設置是否打開文本的亞像素顯示,什么叫亞像素顯示呢?你可以理解為對文本顯示的一種優化技術,如果大家用的是Win7+系統可以在控制面板中找到一個叫ClearType的設置,該設置可以讓你的文本更好地顯示在屏幕上就是基于亞像素顯示技術。具體我們在設計色彩系列將會細說,這里就不扯了
**setStrikeThruText (boolean strikeThruText)**
文本刪除線,不扯
**setLinearText (boolean linearText)**
設置是否打開線性文本標識,這玩意對大多數人來說都很奇怪不知道這玩意什么意思。想要明白這東西你要先知道文本在Android中是如何進行存儲和計算的。在Android中文本的繪制需要使用一個bitmap作為單個字符的緩存,既然是緩存必定要使用一定的空間,我們可以通過setLinearText (true)告訴Android我們不需要這樣的文本緩存。
**setFakeBoldText (boolean fakeBoldText)**
設置文本仿粗體
**measureText (String text),measureText (CharSequence text, int start, int end),measureText (String text, int start, int end),measureText (char[] text, int index, int count)**
測量文本寬度,上面我們已經使用過了,這四個方法都是一樣的只是參數稍有不同這里就不撤了!Paint對文本的繪制方法就上面那些,API 21中還新增了兩個方法這里就先不講了,大家可以看到雖然說這些方法很多很多但是效果都是顯而易見的,很多方法大家一試就知道所以哥也沒有做太多的測試之類什么什么的,這樣講東西是很累的,關于文本也沒有什么有趣的Demo可以玩~~~~~so~~~~~Fuck……
下面我們來看一個比較深奧的東西
**setDither(boolean dither)**
這玩意用來設置我們在繪制圖像時的抗抖動,也稱為遞色,那什么叫抗抖動呢?在Android中我確實不好拿出一個明顯的例子,我就在PS里模擬說明一下

大家看到的這張七彩漸變圖是一張RGB565模式下圖片,即便圖片不是很大我們依然可以很清晰地看到在兩種顏色交接的地方有一些色塊之類的東西感覺很不柔和,因為在RGB模式下只能顯示2^16=65535種色彩,因此很多豐富的色彩變化無法呈現,而Android呢為我們提供了抗抖動這么一個方法,它會將相鄰像素之間顏色值進行一種“中和”以呈現一個更細膩的過渡色:

放大來看,其在很多相鄰像素之間插入了一個“中間值”:

抗抖動不是Android的專利,是圖形圖像領域的一種解決位圖精度的技術。上面說了太多理論性的東西,估計大家都疲憊了,接下來我們來瞅瞅一個比較酷的東西MaskFilter遮罩過濾器!在Paint我們有個方法來設置這東西
**setMaskFilter(MaskFilter maskfilter)**
MaskFilter類中沒有任何實現方法,而它有兩個子類BlurMaskFilter和EmbossMaskFilter,前者為模糊遮罩濾鏡(比起稱之為過濾器哥更喜歡稱之為濾鏡)而后者為浮雕遮罩濾鏡,我們先來看第一個
**BlurMaskFilter**
Android中的很多自帶控件都有類似軟陰影的效果,比如說Button

它周圍就有一圈很淡的陰影效果,這種效果看起來讓控件更真實,那么是怎么做的呢?其實很簡單,使用BlurMaskFilter就可以得到類似的效果
~~~
public class MaskFilterView extends View {
private static final int RECT_SIZE = 800;
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private int left, top, right, bottom;//
public MaskFilterView(Context context) {
this(context, null);
}
public MaskFilterView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(0xFF603811);
// 設置畫筆遮罩濾鏡
mPaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID));
}
/**
* 初始化資源
*/
private void initRes(Context context) {
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
*/
left = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - RECT_SIZE / 2;
top = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - RECT_SIZE / 2;
right = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 + RECT_SIZE / 2;
bottom = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 + RECT_SIZE / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);
// 畫一個矩形
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
~~~
代碼中我們在畫布中央繪制了一個正方形,并設置了了它的模糊濾鏡,但是當你運行后發現并沒有任何的效果:

為什么會這樣呢?還記得上一節中我們講的AvoidXfermode么,在API 16的時候該類已經被標注為過時了,因為AvoidXfermode不支持硬件加速,如果在API 16+上想獲得正確的效果就必需關閉應用的硬件加速,當時我們是在AndroidManifest.xml文件中設置android:hardwareAccelerated為false來關閉的,具體有哪些繪制的方法不支持硬件加速可以參考下圖

但是大家想過沒如果在AndroidManifest.xml文件中關閉硬件加速那么我們整個應用都將不支持硬件加速,這顯然是不科學的,如果可以只針對某個View關閉硬件加速那豈不是很好么?當然,Android也給我們提供了這樣的功能,我們可以在View中通過
`setLayerType(LAYER_TYPE_SOFTWARE, null); `
來關閉單個View的硬件加速功能
再次運行即可得到正確的效果:

是不是很酷呢?BlurMaskFilter只有一個含參的構造函數BlurMaskFilter(float radius, BlurMaskFilter.Blur style),其中radius很容易理解,值越大我們的陰影越擴散,比如在上面的例子中我將radius改為50

可以明顯感到陰影的范圍擴大了,這個很好理解。而第二個參數style表示的是模糊的類型,上面我們用到的是SOLID,其效果就是在圖像的Alpha邊界外產生一層與Paint顏色一致的陰影效果而不影響圖像本身,除了SOLID還有三種,NORMAL,OUTER和INNER,NORMAL會將整個圖像模糊掉:

而OUTER會在Alpha邊界外產生一層陰影且會將原本的圖像變透明:

INNER則會在圖像內部產生模糊:

INNER效果其實并不理想,實際應用中我們使用的也少,我們往往會使用混合模式和漸變和獲得更完美的內陰影效果。如上所說BlurMaskFilter是根據Alpha通道的邊界來計算模糊的,如果是一張圖片(注:上面我們說過Android會把拷貝到資源目錄的圖片轉為RGB565,具體原因具體分析我會單獨開一篇帖子說,這里就先假設所有提及的圖片格式為RGB565)你會發現沒有任何效果,那么假使我們需要給圖片加一個類似陰影的效果該如何做呢?其實很簡單,我們可以嘗試從Bitmap中獲取其Alpha通道,并在繪制Bitmap前先以該Alpha通道繪制一個模糊效果不就行了?
~~~
public class BlurMaskFilterView extends View {
private Paint shadowPaint;// 畫筆
private Context mContext;// 上下文環境引用
private Bitmap srcBitmap, shadowBitmap;// 位圖和陰影位圖
private int x, y;// 位圖繪制時左上角的起點坐標
public BlurMaskFilterView(Context context) {
this(context, null);
}
public BlurMaskFilterView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 記得設置模式為SOFTWARE
setLayerType(LAYER_TYPE_SOFTWARE, null);
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
shadowPaint.setColor(Color.DKGRAY);
shadowPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
}
/**
* 初始化資源
*/
private void initRes(Context context) {
// 獲取位圖
srcBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);
// 獲取位圖的Alpha通道圖
shadowBitmap = srcBitmap.extractAlpha();
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
*/
x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - srcBitmap.getWidth() / 2;
y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - srcBitmap.getHeight() / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 先繪制陰影
canvas.drawBitmap(shadowBitmap, x, y, shadowPaint);
// 再繪制位圖
canvas.drawBitmap(srcBitmap, x, y, null);
}
}
~~~
如代碼所示我們通過Bitmap的extractAlpha()方法從原圖中分離出一個Alpha通道位圖并在計算模糊濾鏡的時候使用該位圖生成模糊效果:

相對于BlurMaskFilter來說
**EmbossMaskFilter**
的常用性比較低,倒不是說EmbossMaskFilter很沒用,只是相對于EmbossMaskFilter實現的效果來說遠不及BlurMaskFilter給人的感覺霸氣,說了半天那么EmbossMaskFilter到底是做什么的呢?
我們先來看一張圖:

這么一個看著像巧克力的東西就是用EmbossMaskFilter實現了,正如其名,他可以實現一種類似浮雕的效果,說白了就是讓你繪制的圖像感覺像是從屏幕中“凸”起來更有立體感一樣(在設計軟件中類似的效果稱之為斜面浮雕)。該類也只有一個含參的構造方法EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius),這些參數理解起來要比BlurMaskFilter困難得多,如果你沒有空間想象力的話,首先我們來看第一個direction指的是方向,什么方向呢?光照的方向!如果大家接觸過三維設計就一定懂,沒接觸也沒關系,我跟你說明白。假設一個沒有任何光線的黑屋子里有一張桌子,桌子上有一個小球,這時我們打開桌子上的臺燈,臺燈照亮了小球,這時候小球的狀態與下圖類似:

PS:略草,湊合看
小球最接近光源的地方肯定是最亮的這個沒有異議,在參數中specular就是跟高光有關的,其值是個雙向值越小或越大高光越強中間值則是最弱的,那么再看看什么是ambient呢?同樣我們看個球,你會發現即便只有一盞燈光,在球底部跟桌面相接的地方依然不會出現大片的“死黑”,這是因為光線在傳播的過程中碰到物體會產生反射!這種反射按照物體介質的粗糙度可以分為漫反射和鏡面反射,而這里我們的小球之所以背面沒有直接光照但仍能有一定的亮度就是因為大量的漫反射在空間傳播讓光線間接照射到小球背面,這種區別于直接照明的二次照明我們稱之為間接照明,產生的光線叫做環境光ambient,參數中的該值就是用來設置環境光的,在Android中環境光默認為白色,其值越大,陰影越淺,blurRadius則是設置圖像究竟“凸”出多大距離的很好理解,最難理解的一個參數是direction,上面我們也說了是光照方向的意思,該數組必須要有而且只能有三個值即float[x,y,z],這三個值代表了一個空間坐標系,我們的光照方向則由其定義,那么它是怎么定義的呢?首先x和y很好理解,平面的兩個維度嘛是吧,上面我們使用的是[1,1]也就是個45度角,而z軸表示光源是在屏幕后方還是屏幕前方,上面我們是用的是1,正值表示光源往屏幕外偏移1個單位,負值表示往屏幕里面偏移,這么一說如果我把其值改為[1,1,-1]那么我們的巧克力朝著我們的一面應該就看不到了對吧,試試看撒~~~這個效果我就不截圖了,因為一片漆黑……但是你依然能夠看到一點點灰度~就是因為我們的環境光ambient!,如果我們把值改為[1,1,2]往屏幕外偏移兩個單位,那么我們巧克力正面光照將更強:

看吧都爆色了!這里要提醒一點[x,y,z]表示的是空間坐標,代表光源的位置,那么一旦這個位置確定,[ax,ay,az]則沒有意義,也就是說同時擴大三個軸向值的倍數是沒有意義的,最終效果還是跟[x,y,z]一樣!懂了不?
額……忘了給代碼,大家可以自己去試試
~~~
public class EmbossMaskFilterView extends View {
private static final int H_COUNT = 2, V_COUNT = 4;// 水平和垂直切割數
private Paint mPaint;// 畫筆
private PointF[] mPointFs;// 存儲各個巧克力坐上坐標的點
private int width, height;// 單個巧克力寬高
private float coorY;// 單個巧克力坐上Y軸坐標值
public EmbossMaskFilterView(Context context) {
this(context, null);
}
public EmbossMaskFilterView(Context context, AttributeSet attrs) {
super(context, attrs);
// 不使用硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
// 初始化畫筆
initPaint();
// 計算參數
cal(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(0xFF603811);
// 設置畫筆遮罩濾鏡
mPaint.setMaskFilter(new EmbossMaskFilter(new float[] { 1, 1, 1F }, 0.1F, 10F, 20F));
}
/**
* 計算參數
*/
private void cal(Context context) {
int[] screenSize = MeasureUtil.getScreenSize((Activity) context);
width = screenSize[0] / H_COUNT;
height = screenSize[1] / V_COUNT;
int count = V_COUNT * H_COUNT;
mPointFs = new PointF[count];
for (int i = 0; i < count; i++) {
if (i % 2 == 0) {
coorY = i * height / 2F;
mPointFs[i] = new PointF(0, coorY);
} else {
mPointFs[i] = new PointF(width, coorY);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);
// 畫矩形
for (int i = 0; i < V_COUNT * H_COUNT; i++) {
canvas.drawRect(mPointFs[i].x, mPointFs[i].y, mPointFs[i].x + width, mPointFs[i].y + height, mPaint);
}
}
}
~~~
上面我們說了EmbossMaskFilter的使用面并不是很大,因為所說其參數稍復雜但是其實現原理是簡單粗暴的,簡而言之就是根據參數在圖像周圍繪制一個“色帶”來模擬浮雕的效果,如果我們的圖像很復雜EmbossMaskFilter很難會正確模擬,所以一般遇到這類圖直接call美工 = = 哈哈哈。
**setRasterizer (Rasterizer rasterizer)**
設置光柵,光柵這東西涉及太多太多物理知識,不講了一講又是一大堆,而且該方法同樣不支持HW在API 21中遺棄了~~~我們還是來看看對我們來說更好玩有趣的方法
**setPathEffect(PathEffect effect)**
PathEffect見文知意很明顯就是路徑效果的意思~~那這玩意肯定跟路徑Path有關咯?那是必須的撒!PathEffect跟上面的很多類一樣沒有具體的實現,但是其有六個子類:

這六個子類分別可以實現不同的路徑效果:

上圖從上往下分別是沒有PathEffect、CornerPathEffect、DiscretePathEffect、DashPathEffect、PathDashPathEffect、ComposePathEffect、SumPathEffect的效果,代碼的實現也非常簡單:
~~~
public class PathEffectView extends View {
private float mPhase;// 偏移值
private Paint mPaint;// 畫筆對象
private Path mPath;// 路徑對象
private PathEffect[] mEffects;// 路徑效果數組
public PathEffectView(Context context, AttributeSet attrs) {
super(context, attrs);
/*
* 實例化畫筆并設置屬性
*/
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.DKGRAY);
// 實例化路徑
mPath = new Path();
// 定義路徑的起點
mPath.moveTo(0, 0);
// 定義路徑的各個點
for (int i = 0; i <= 30; i++) {
mPath.lineTo(i * 35, (float) (Math.random() * 100));
}
// 創建路徑效果數組
mEffects = new PathEffect[7];
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/*
* 實例化各類特效
*/
mEffects[0] = null;
mEffects[1] = new CornerPathEffect(10);
mEffects[2] = new DiscretePathEffect(3.0F, 5.0F);
mEffects[3] = new DashPathEffect(new float[] { 20, 10, 5, 10 }, mPhase);
Path path = new Path();
path.addRect(0, 0, 8, 8, Path.Direction.CCW);
mEffects[4] = new PathDashPathEffect(path, 12, mPhase, PathDashPathEffect.Style.ROTATE);
mEffects[5] = new ComposePathEffect(mEffects[2], mEffects[4]);
mEffects[6] = new SumPathEffect(mEffects[4], mEffects[3]);
/*
* 繪制路徑
*/
for (int i = 0; i < mEffects.length; i++) {
mPaint.setPathEffect(mEffects[i]);
canvas.drawPath(mPath, mPaint);
// 每繪制一條將畫布向下平移250個像素
canvas.translate(0, 250);
}
// 刷新偏移值并重繪視圖實現動畫效果
mPhase += 1;
invalidate();
}
}
~~~
當我們不設置路徑效果的時候路徑的默認效果就如上圖第一條線那樣直的轉折生硬;而CornerPathEffect則可以將路徑的轉角變得圓滑如圖第二條線的效果,這六種路徑效果類都有且只有一個含參的構造方法,CornerPathEffect的構造方法只接受一個參數radius,意思就是轉角處的圓滑程度,我們嘗試更改一下上面的代碼:
`mEffects[1] = new CornerPathEffect(50); `

Look Pic是不是更平滑了呢?CornerPathEffect相對于其他的路徑效果來說最簡單了;DiscretePathEffect離散路徑效果相對來說則稍微復雜點,其會在路徑上繪制很多“雜點”的突出來模擬一種類似生銹鐵絲的效果如上圖第三條線,其構造方法有兩個參數,第一個呢指定這些突出的“雜點”的密度,值越小雜點越密集,第二個參數呢則是“雜點”突出的大小,值越大突出的距離越大反之反之,大家可以去自己去試下我就不演示了;DashPathEffect的效果相對與上面兩種路徑效果來說要略顯復雜,其雖說也是包含了兩個參數,但是第一個參數是一個浮點型的數組,那這個數組有什么意義呢?其實是這樣的,我們在定義該參數的時候只要浮點型數組中元素個數大于等于2即可,也就是說上面我們的代碼可以寫成這樣的:
`mEffects[3] = new DashPathEffect(new float[] {20, 10}, mPhase); `

從圖中我們可以看到我們之前的那種線條變成了一長一短的間隔線條,而float[] {20, 10}的偶數參數20(注意數組下標是從0開始哦)定義了我們第一條實線的長度,而奇數參數10則表示第一條虛線的長度,如果此時數組后面不再有數據則重復第一個數以此往復循環,比如我們20,10后沒數了,那么整條線就成了[20,10,20,10,20,10…………………………]這么一個狀態,當然如果你無聊,也可以:
`mEffects[3] = new DashPathEffect(new float[] {20, 10, 50, 5, 100, 30, 10, 5}, mPhase);`

而DashPathEffect的第二個參數我稱之為偏移值,動態改變其值會讓路徑產生動畫的效果,上面代碼已給出大家可以自己去試試;PathDashPathEffect和DashPathEffect是類似的,不同的是PathDashPathEffect可以讓我們自己定義路徑虛線的樣式,比如我們將其換成一個個小圓組成的虛線:
~~~
Path path = new Path();
path.addCircle(0, 0, 3, Direction.CCW);
mEffects[4] = new PathDashPathEffect(path, 12, mPhase, PathDashPathEffect.Style.ROTATE);
~~~

ComposePathEffect和SumPathEffect都可以用來組合兩種路徑效果,唯一不同的是組合的方式,ComposePathEffect(PathEffect outerpe, PathEffect innerpe)會先將路徑變成innerpe的效果,再去復合outerpe的路徑效果,即:outerpe(innerpe(Path));而SumPathEffect(PathEffect first, PathEffect second)則會把兩種路徑效果加起來再作用于路徑,具體區別大家去試試吧…………哥累了睡會~~~囧……
記得在1/12中我們繪制了了一個圓環并讓其實現動畫的效果,當時我們使用了線程來使其產生動畫,但是我們是不是也可以像上面的例子一樣直接在onDraw中invalidate()來產生動畫呢?這個問題留給大家。
在1/12中我們還說過盡量不要在onDraw中使用new關鍵字來生成對象,但是上例的代碼中我們卻在頻繁地使用,但是六個PathEffect的子類中除了構造方法什么都沒有,我們該如何避免頻繁地去new對象呢?這個問題也留給大家思考。
Path應用的廣泛性注定了PathEffect應用的廣泛,所謂一人得道雞犬升天就是這么個道理,只要是Path能存在的地方都可以考慮使用,下面我們來模擬一個類似心電圖的路徑小動畫:

這種效果呢也是非常非常地簡單,說白了就是無數條短小精悍的小“Path”連接成一條完整的心電路徑:
~~~
public class ECGView extends View {
private Paint mPaint;// 畫筆
private Path mPath;// 路徑對象
private int screenW, screenH;// 屏幕寬高
private float x, y;// 路徑初始坐標
private float initScreenW;// 屏幕初始寬度
private float initX;// 初始X軸坐標
private float transX, moveX;// 畫布移動的距離
private boolean isCanvasMove;// 畫布是否需要平移
public ECGView(Context context, AttributeSet set) {
super(context, set);
/*
* 實例化畫筆并設置屬性
*/
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.GREEN);
mPaint.setStrokeWidth(5);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setShadowLayer(7, 0, 0, Color.GREEN);
mPath = new Path();
transX = 0;
isCanvasMove = false;
}
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
/*
* 獲取屏幕寬高
*/
screenW = w;
screenH = h;
/*
* 設置起點坐標
*/
x = 0;
y = (screenH / 2) + (screenH / 4) + (screenH / 10);
// 屏幕初始寬度
initScreenW = screenW;
// 初始X軸坐標
initX = ((screenW / 2) + (screenW / 4));
moveX = (screenW / 24);
mPath.moveTo(x, y);
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
mPath.lineTo(x, y);
// 向左平移畫布
canvas.translate(-transX, 0);
// 計算坐標
calCoors();
// 繪制路徑
canvas.drawPath(mPath, mPaint);
invalidate();
}
/**
* 計算坐標
*/
private void calCoors() {
if (isCanvasMove == true) {
transX += 4;
}
if (x < initX) {
x += 8;
} else {
if (x < initX + moveX) {
x += 2;
y -= 8;
} else {
if (x < initX + (moveX * 2)) {
x += 2;
y += 14;
} else {
if (x < initX + (moveX * 3)) {
x += 2;
y -= 12;
} else {
if (x < initX + (moveX * 4)) {
x += 2;
y += 6;
} else {
if (x < initScreenW) {
x += 8;
} else {
isCanvasMove = true;
initX = initX + initScreenW;
}
}
}
}
}
}
}
}
~~~
我們在onSizeChanged(int w, int h, int oldw, int oldh)方法中獲取屏幕的寬高,該方法的具體用法我們會在7/12學習View的測繪時具體說明,這里就先不說了
上面在設置Paint屬性的時候我們使用到了一個
**setStrokeCap(Paint.Cap cap)**
方法,該方法用來設置我們畫筆的筆觸風格,上面的例子中我使用的是ROUND,表示是圓角的筆觸,那么什么叫筆觸呢,其實很簡單,就像我們現實世界中的筆,如果你用圓珠筆在紙上戳一點,那么這個點一定是個圓,即便很小,它代表了筆的筆觸形狀,如果我們把一支鉛筆筆尖削成方形的,那么畫出來的線條會是一條彎曲的“矩形”,這就是筆觸的意思。除了ROUND,Paint.Cap還提供了另外兩種類型:SQUARE和BUTT,具體大家自己去try~~
**setStrokeJoin(Paint.Join join)**
這個方法用于設置結合處的形態,就像上面的代碼中我們雖說是花了一條心電線,但是這條線其實是由無數條小線拼接成的,拼接處的形狀就由該方法指定。
上面的例子中我們還使用到了一個方法
**setShadowLayer(float radius, float dx, float dy, int shadowColor)**
該方法為我們繪制的圖形添加一個陰影層效果:
~~~
public class ShadowView extends View {
private static final int RECT_SIZE = 800;// 方形大小
private Paint mPaint;// 畫筆
private int left, top, right, bottom;// 繪制時坐標
public ShadowView(Context context, AttributeSet attrs) {
super(context, attrs);
// setShadowLayer不支持HW
setLayerType(LAYER_TYPE_SOFTWARE, null);
// 初始化畫筆
initPaint();
// 初始化資源
initRes(context);
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.RED);
mPaint.setStyle(Style.FILL);
mPaint.setShadowLayer(10, 3, 3, Color.DKGRAY);
}
/**
* 初始化資源
*/
private void initRes(Context context) {
/*
* 計算位圖繪制時左上角的坐標使其位于屏幕中心
*/
left = MeasureUtil.getScreenSize((Activity) context)[0] / 2 - RECT_SIZE / 2;
top = MeasureUtil.getScreenSize((Activity) context)[1] / 2 - RECT_SIZE / 2;
right = MeasureUtil.getScreenSize((Activity) context)[0] / 2 + RECT_SIZE / 2;
bottom = MeasureUtil.getScreenSize((Activity) context)[1] / 2 + RECT_SIZE / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 先繪制位圖
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
~~~
radius表示陰影的擴散半徑,而dx和dy表示陰影平面上的偏移值,shadowColor就不說了陰影顏色,最后提醒一點setShadowLayer同樣不支持HW哦!

上面我們講MaskFilter的時候曾用其子類BlurMaskFilter模擬過類似效果,跟BlurMaskFilter比起來這方法是不是更簡捷呢?但是BlurMaskFilter能做的setShadowLayer卻不一定能做到哦!
至此,Paint下的幾乎所有方法我們都已經學習了,正如我之前所說,工欲善其事必先利其器,自定義View很重要的一部分就是如何去畫一個Perfect的圖形,Android給我們提供了絕大部分的方法和類來模擬現實中真正的畫筆,我們只需要學會如何靈活運用即可,牛逼的人不需要復雜的技術即可實現復雜的效果~~這就是實力、才是真大神~~~~So~共勉
PS:這一節講得有點戳~~Because something happen in my life maybe changed my life,還有最近忙著改的一個相機應用的開源項目,所以很多事 = =,講得不是很好大家見諒,下一期補上更精彩的內容
源碼地址:[傳送門](http://download.csdn.net/detail/aigestudio/8193115)
溫馨提示:自定義控件其實很簡單系列文章每周一、周四更新一篇~
下集精彩預告:Paint中的方法幾乎都已經概述了一遍,但是有個方法我們還沒說setShader(Shader shader),這個方法很重要嗎?其實一般,但是其涉及到的一個東西對我們來說相當重要:Matrix,這個神秘的東西究竟是做什么用的呢?賣個關子先,下一節我們將會結束整個Paint的學習,是不是有點想躍躍一試的沖動?下期我將會給大家帶來兩個好玩的例子,這兩個例子來自于群里朋友的提問,不多說了,上圖:




鎖定本臺敬請關注:自定義控件其實很簡單1/3