不管是在我們的世界裡,還是在Android的世界裡,想要向神筆馬良一樣畫出各種精彩絕倫的畫,就必須得有一個前提------要有一支神奇的畫筆。在前面的學習中,我們已經初步了解了一些常用的畫筆屬性,比如普通的畫筆(Paint),帶邊框、填充的style,顔色(Color),寬度(StrokeWidth),抗鋸齒(ANTI_ALLAS_FLAG)等,這些都是基本的畫筆屬性,就好像一個普通人所擁有的畫筆工具。然而除此之外,還有各種各樣專業的畫筆工具,如記号筆、毛筆、蠟筆等,使用他們可以實作更加豐富的繪圖效果。下面我們就來看看畫筆的一些進階屬性,幫助我們實作更豐富的繪圖效果。
1.PorterDuffXfermode
在學習這個東西之前,先來看一張非常經典的圖,出自API Demo,基本上所有講PorterDuffXfermode的文章都會使用這張圖做說明,圖下圖所示。
上圖中列舉了16中PorterDuffXfermode,有點像數學中的交集、并集這樣的概念,相信大家配合圖例應該很好了解,它控制的是兩個圖像的混合顯示模式。
這裡要注意的是,PorterDuffXfermode設定的是兩個圖層交集區域的顯示方式,dst是先畫的圖形,而src是後畫的圖形。
當然,這些模式也不是經常使用的,用的最多的是,使用一張圖檔作為另一張圖檔的遮罩層,通過控制遮罩層的圖形,來控制下面被遮罩圖形的顯示效果。其中最常用的就是通過DST_IN、SRC_IN模式來實作将一個矩形圖檔變成圓角或者圓形圖檔的效果。
要使用PorterDuffXfermode非常簡單,隻需要讓畫面擁有這個屬性就可以了,比如下面要實作的執行個體。如下圖所示。
先用一個普通畫筆畫一個Mask遮罩層,再用帶PorterDuffXfermode的畫筆将圖像華仔遮罩層上,這樣就可以通過上面所說的效果來混合兩個圖像了,代碼如下所示。
private Bitmap handleImageRoundRect(Bitmap bm) {
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);//抗鋸齒
canvas.drawRoundRect(0,0,bm.getWidth(),bm.getHeight(),80,80,paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bm,0,0,paint);
return bitmap;
}
下面再來看一個稍微複雜點的效果------刮刮卡效果。我們都知道,刮刮卡一般有兩個圖層,即上面的用來放刮掉的圖層和下面隐藏的圖層。在初始狀态下,上面的圖層會将下面整個覆寫,當你用手刮上面的圖層的時候,下面的圖層會慢慢顯示出來,這也類似很多畫圖工具中的橡皮擦效果。這個效果同樣可以使用PorterDuffXfermode來實作。
首先需要做一些初始化工作,例如準備好圖檔,設定好Paint的一些特性,代碼如下所示。
private void init() {
mPaint = new Paint();
//關鍵的一步,這樣才能顯示出擦塗的效果。因為使用PorterDuffXfermode進
// 行圖層混合時,并不是簡單的隻進行圖層的計算,同時也會計算透明通道的值。
//正式由于混合了透明通道,才能形成了這樣的效果。
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mPaint.setStyle(Paint.Style.STROKE);
//以下代碼時為了讓筆觸和連接配接處更加圓滑一點,即Paint.Join.ROUND和Paint.Cap.ROUND屬性
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeWidth(50);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPath = new Path();
mBgBitmap = ((BitmapDrawable)getDrawable()).getBitmap();
mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(),mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mFgBitmap);
mCanvas.drawColor(Color.GRAY);
}
在上面的代碼中,給Paint設定了一些屬性,讓筆觸和連接配接處更加圓滑一點,即Paint.Join.ROUND和Paint.Cap.ROUND屬性。
接下來,看以下如何擷取使用者手指滑動所産生的路徑,代碼如下所示。使用Path儲存使用者手指劃過的路徑。當然,這裡如果使用貝塞爾曲線來做優化則會得到更好的顯示效果,這裡為了簡化示範功能,就不适應貝塞爾曲線了。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(event.getX(),event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(),event.getY());
break;
}
mCanvas.drawPath(mPath,mPaint);
invalidate();
return true;
}
最後,隻需要使用DST_IN模式将路徑繪制到前面覆寫的圖層上面即可。不過還需要做最關鍵的一步,那就是将畫筆的透明度設定為0,這樣才能顯示出擦除的效果。因為使用PorterDuffXfermode進行圖層混合時,并不是簡單的隻進行圖層的計算,同時也會計算透明通道的值。正式由于混合了透明通道,才能形成了這樣的效果。完整代碼如下所示。
public class XfermodeView extends android.support.v7.widget.AppCompatImageView {
private Bitmap mBgBitmap;
private Bitmap mFgBitmap;
private Paint mPaint;
private Canvas mCanvas;
private Path mPath;
public XfermodeView(Context context) {
super(context);
}
public XfermodeView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public XfermodeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
init();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(event.getX(),event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(),event.getY());
break;
}
mCanvas.drawPath(mPath,mPaint);
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBgBitmap,0,0,null);
canvas.drawBitmap(mFgBitmap,0,0,null);
}
private void init() {
mPaint = new Paint();
//關鍵的一步,這樣才能顯示出擦塗的效果。因為使用PorterDuffXfermode進
// 行圖層混合時,并不是簡單的隻進行圖層的計算,同時也會計算透明通道的值。
//正式由于混合了透明通道,才能形成了這樣的效果。
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mPaint.setStyle(Paint.Style.STROKE);
//以下代碼時為了讓筆觸和連接配接處更加圓滑一點,即Paint.Join.ROUND和Paint.Cap.ROUND屬性
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeWidth(50);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPath = new Path();
mBgBitmap = ((BitmapDrawable)getDrawable()).getBitmap();
mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(),mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mFgBitmap);
mCanvas.drawColor(Color.GRAY);
}
}
程式運作效果圖下圖所示,當使用者手指滑動時,就會擦除上面的圖層,形成刮刮卡的效果。
在使用PorterDuffXfermode時還有一點需要注意,那就是最好在繪圖時,将硬體加速關閉,因為有些模式并不支援硬體加速。
2.Shader
Shader又被成為着色器、渲染器,它用來實作一系列的漸變、渲染效果。Android中的Shader包括以下幾種。
- BitmapShader:位圖Shader。
- LinearGradient:線性Shader。
- RadialGradient:光速Shader。
- SweepGradient:梯度Shader。
- ComposeShader:混合Shader。
除了第一個Shader以外,其他的Shader都比較正常,實作了名副其實的漸變、渲染效果。而與其他的Shader所産生的漸變不同,BitmapShader産生的是一個圖像,這有點像Photoshop中的圖像填充漸變。它的作用是通過Paint對畫布進行指定Bitmap的填充,填充時有以下幾種模式可以選擇。
- CLAMP拉伸------拉伸的是圖檔最後一個像素,不斷重複
- REPEAT重複------橫向、縱向不對重複
- MIRROR鏡像------橫向不斷翻轉重複,縱向不斷翻轉重複
這幾種模式的含義都非常好了解,與字面意思基本相同。這裡最常使用的就是CLAMP拉伸模式,雖然他們拉伸最後一個像素,但是隻要将圖像設定為一定的大小,就可以避免這種拉伸。下面來看一個跟上面有類似效果的例子,将一個矩形的圖檔變成一張圓形的圖檔。當然,通過繪制不同的圖形,你也可以繪制出不同形狀的圖形。
程式非常簡單,無非就是使用BitmapShader來進行圖形填充,代碼如下所示。
private Bitmap handleImageCircle(Bitmap bm) {
BitmapShader bitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setShader(bitmapShader);
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(300, 250, 200, paint);
return bitmap;
}
通過以上的代碼再運作程式,效果圖如下所示。
相信通過這樣一幅圖,大家應該可以了解使用CLAMP拉伸模式來建立圓形圖像的原因了。
在講完BitmapShader這個比較特殊的Shader後,再來看看其他比較類似的Shader。
先來看一個最簡單,也是最常用的Shader------LinearGradient。直譯過來就是線性漸變,沒錯,它就是一個簡單的線性漸變,與Photoshop裡面的漸變效果類似。
要使用LinearGradient也非常簡單,隻需要指定漸變其實的顔色就可以了,代碼如下所示。
Paint paint = new Paint();
paint.setShader(new LinearGradient(0,0,400,400, Color.BLUE, Color.YELLOW, Shader.TileMode.REPEAT));
Bitmap bitmap = Bitmap.createBitmap(400,400, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawRect(0,0,400,400,paint);
return bitmap;
通過以上代碼,畫出一個LinearGradient,效果如下圖所示,他是一個從(0,0)到(400,400)的一個由藍色到黃色的漸變效果。
LinearGradient方法參數中的TileMode與在BitmapShader中的含義基本相同,這裡将繪制矩形大小設定為漸變圖像大小,是以沒有看見REPEAT的效果。如果圖像擴大,REPEAT的效果就出來了,如下圖所示。
其他幾種漸變模式與LinearGradient基本相同,隻是漸變的小時效果不同而已,這裡就不做過多的介紹了。
其實這些漸變效果通常不會直接使用在程式裡。通常情況下,把這種漸變效果作為一個遮罩層來使用,同時結合前面的PorterDuffXfermode。這樣處理後,遮罩層就不再是一個生硬的圖像,而是一個具有漸變效果的圖層。這樣處理的效果會更加柔和、更加自然。下面這個執行個體就示範了如何使用LinearGradient和PorterDuffXfermode來建立一個具有倒影效果的圖檔。
要實作這個效果,首先需要把原圖指派一份并進行翻轉,代碼如下所示。
Matrix matrix = new Matrix();
matrix.setScale(1f,-1f);//實作圖檔的垂直翻轉
Bitmap bitmap = Bitmap.createBitmap(bm, 0,0,bm.getWidth(),bm.getHeight(),matrix,true);
需要注意的是,使用matrix.setScale(1f,-1f)方法來實作圖檔的垂直翻轉,這是一個非常有用的技巧,避免了使用選擇變換的複雜計算。其原理相信大家隻要将值帶入前面講解圖像變換矩陣的計算公式中,就知道為什麼了。同理,還可以實作圖像的水準翻轉。
首先繪制兩張圖檔即原圖和倒影圖,隻是這個時候還未繪制漸變層,是以倒影圖和原圖的透明度相同。接下來,在倒影圖上面繪制一個同樣大小的漸變矩形,并通過Mode.DST_IN模式繪制到倒影圖上,進而形成一個具有過渡效果的漸變層,完整代碼如下所示。
public class InvertedImageView extends android.support.v7.widget.AppCompatImageView {
private Paint mPaint;
private Bitmap mSrcBitmap;
private Bitmap mDstBitmap;
private PorterDuffXfermode mXfermode;
public InvertedImageView(Context context) {
super(context);
}
public InvertedImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public InvertedImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK);//背景
canvas.drawBitmap(mSrcBitmap,0,0,null);//畫上面圖檔
canvas.drawBitmap(mDstBitmap,0,mSrcBitmap.getHeight(),null);//畫下面圖檔
mPaint.setXfermode(mXfermode);//将漸變和Xfermode設定到畫筆中
//繪制漸變效果矩形
canvas.drawRect(0,mSrcBitmap.getHeight(),mDstBitmap.getWidth(),mSrcBitmap.getHeight()*2,mPaint);
mPaint.setXfermode(null);
}
private void init() {
mSrcBitmap = ((BitmapDrawable)getDrawable()).getBitmap();
int height = mSrcBitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setScale(1f,-1f);//實作圖檔的垂直翻轉
mDstBitmap = Bitmap.createBitmap(mSrcBitmap, 0,0,mSrcBitmap.getWidth(),mSrcBitmap.getHeight(),matrix,true);
mPaint = new Paint();
mPaint.setShader(new LinearGradient(0,height, 0, height+height/4, 0xDD000000, 0x1000000, Shader.TileMode.CLAMP));
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
}
}
下圖是效果圖。
3.PathEffect
要了解PathEffect,先來看一張比較直覺的圖,來了解下什麼是PathEffect,如下圖
- PathEffect就是指,用各種筆觸效果來繪制一個路徑。Android系統提供了如上圖中展示的幾種繪制PathEffect的方式,從上到下一次是:沒效果、CornerPathEffect、DiscretePathEffect、DashPathEffect、PathDashPathEffect、ComposePathEffect。
- CornerPathEffec:非常好了解,就是将拐彎處變得圓滑,具體圓滑程度,則有參數決定。
- DiscretePathEffect:使用這個效果後,線段上就會産生許多雜點。
- DashPathEffect:顯然,這個效果可以用來繪制虛線,用一個數組來設定各個點之間的間隔。此後會繪制虛線是就重複這樣的間隔進行繪制,另一個參數phase則用來控制繪制時數組的一個偏移量,通常可以通過設定值來實作路徑的動态效果。
- PathDashPathEffect:這個效果與前面的DashPathEffect類似,隻不過他的功能更加強大,可以設定顯示點的圖形,即方形點的虛線、圓形點的虛線。
- ComposePathEffect:如果每次都隻能實作一種路徑的特效效果,那就顯得太單調了。Android提供了一種更加靈活的方式------通過ComposePathEffect來組合PathEffect,這個方法的功能就是将任意兩種路徑特性組合起來形成一個新的效果。
有了以上的了解,來看看上圖的效果時如何實作的。
首先,需要生産一個Path,這裡使用随機數來生産一些随機的點,并形成一條路徑,代碼如下所示。
mPath = new Path();
mPath.moveTo(0,0);
for (int i = 0; i < 30; i++) {
mPath.lineTo(i*35, (float) (Math.random()*100));
}
接下來,就可以再onDraw()方法中通過不同的PathEffect來繪制這些Path了,代碼如下所示。
mEffects[0] = null;
mEffects[1] = new CornerPathEffect(30);
mEffects[2] = new DiscretePathEffect(3.0f, 5.0f);
mEffects[3] = new DashPathEffect(new float[]{20,10,5,10}, 0);
Path path = new Path();
path.addRect(0,0,8,8,Path.Direction.CCW);
mEffects[4] = new PathDashPathEffect(path,12,10,PathDashPathEffect.Style.ROTATE);
mEffects[5] = new ComposePathEffect(mEffects[3],mEffects[1]);
for (int i = 0 ; i < mEffects.length; i++) {
mPaint.setPathEffect(mEffects[i]);
canvas.drawPath(mPath,mPaint);
canvas.translate(0,200);
}
每繪制一個Path,就将畫布平移,進而讓各種PathEffect一次繪制出來。