內容簡介
文章介紹ImageView(方法也可以應用到其它View)圓角矩形(包括圓形)的一種實現方式,四個角可以分別指定為圓角。思路是利用“Xfermode + Path”
來進行Bitmap的裁剪。
背景
圓角矩形實現的方法應該很多,網上一大堆。很懷疑為啥安卓的控件不內置這樣的屬性(我不知道有)?
之前用到的網絡圖片加載庫(UniversalImageLoader等)都自帶“圓形圖片”這樣的功能。這次需要的效果是圓角矩形,而且只有圖片上面左、右兩個角是圓角。然后藐似沒發現有這種功能,剛好就自己實踐下了。
一個需要強調的事實就是,像ImageView這樣的控件,它可以是wrap_content這樣的,最終大小不定,由對應的Drawable或Bitmap資源決定其大小。另一種情況下ImageView的大小是固定的,此時圖片的實際填充效果(可視范圍)受到scaleType的影響,不一定和View大小一致,不過往往會保持圖片寬高比例,使得最終ImageView的寬高和顯示的圖片是一致的。
在畫布上進行裁剪時,必須明確要操作的相關Bitmap的尺寸。由于上面的原因,根據實際ImageView大小的確定方式不同,要么是取ImageView的大小來作為整個“圓角矩形”的范圍,要么是以實際展示的Bitmap的大小為準。
下面采取自定義ImageView子類的形式提供案例來說明“Xfermode + Path”實現圓角矩形的思路。而且會以ImageView固定大小(圖片填充,scaleType=fitXY)的形式,也就是說要顯示的圖片是完全填充ImageView的,它們一樣大小。如果以Bitmap為準,那么就得自己去設法得到原本ImageView的“設置下”顯示的圖片的范圍,然后對應的去裁剪。這里為突出重點,就不考慮那么多了(^-^)。
clipPath()版本
方法android.graphics.Canvas#clipPath(android.graphics.Path)用來沿著Path指定的路線從目前的canvas裁剪出新的區域的canvas,就是改變了畫布的可繪制區域。理解上,就像你拿著剪刀沿著圓環路徑裁剪畫紙就可以裁剪出一個圓型畫紙一樣。
Canvas類的一些API是直接繪制內容的操作,另一些是針對canvas(畫布)本身做設置的。clip**系列方法就是對畫布進行裁剪,之后的繪制(“可以簡單地”認為之前通過canvas的繪制已經固定在畫布對應存儲圖像的bitmap上了)都在裁剪后的區域中進行。
使用clipPath()實現圓角矩形的完整代碼如下:
public class RoundCornerImageView1 extends ImageView {
private float[] radiusArray = { 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f };
public RoundCornerImageView1(Context context) {
super(context);
}
public RoundCornerImageView1(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 設置四個角的圓角半徑
*/
public void setRadius(float leftTop, float rightTop, float rightBottom, float leftBottom) {
radiusArray[0] = leftTop;
radiusArray[1] = leftTop;
radiusArray[2] = rightTop;
radiusArray[3] = rightTop;
radiusArray[4] = rightBottom;
radiusArray[5] = rightBottom;
radiusArray[6] = leftBottom;
radiusArray[7] = leftBottom;
invalidate();
}
protected void onDraw(Canvas canvas) {
Path path = new Path();
path.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), radiusArray, Path.Direction.CW);
canvas.clipPath(path);
super.onDraw(canvas);
}
}
注意需要先在canvas上執行clipPath(),之后再繼續繪制原本的圖片,這樣就保證了繪制的內容范圍限制在裁剪后的“圓角矩形畫布”中。
上面方法addRoundRect()
的原型如下:
/**
* Add a closed round-rectangle contour to the path. Each corner receives
* two radius values [X, Y]. The corners are ordered top-left, top-right,
* bottom-right, bottom-left
*
* @param rect The bounds of a round-rectangle to add to the path
* @param radii Array of 8 values, 4 pairs of [X,Y] radii
* @param dir The direction to wind the round-rectangle's contour
*/
public void addRoundRect(RectF rect, float[] radii, Direction dir);
它就是用來描述一個圓角矩形的路徑。可以看到四個角都可以指定,而且還可以是不同的x,y半徑。但是這里只允許圓角是圓。
下圖是一些效果圖:
clipPath()缺陷
最初的版本就是這樣ok了,完成任務。后來測試說是圖片圓角處模糊,
這里先給一個對比圖,感受下:
我以為是網絡加載的圖片的Bitmap.Config
引起的,改后無果。關鍵字“clipPath 鋸齒”搜了下發現clipPath這種方式無法抗鋸齒。
后面看到StackOverflow上歪果仁的一個回答,說Xfermode可以實現。
在sdk目錄下有對應的一個關于Xfermode的使用演示:sdk\samples\android-19\ApiDemos\src\com\example\android\apis\graphics\Xfermodes.java。
如果使用了模擬器,可以在ApiDemos > Graphics > Xfermodes中看到下面的效果:
后面會附上Xfermode.java的核心代碼,這里說明下。矩形和圓分別是兩個獨立的Bitmap,上圖演示了選取Xfermode的子類PorterDuffXfermode作為“Xfermode("transfer-modes" in the drawing pipeline)”時其不同混合模式得到的效果。
把圓作為一個畫框看待,那么第2行第2個效果圖:SrcIn,畫了一個矩形,矩形只有落在圓中的部分才最終可見。
同樣的思路,可以先做一個圓角矩形的畫框——方式類似上面的clipPath()也是使用Path實現。然后讓原本的圖片畫在這個畫框上,效果就是圓角矩形的圖片了。
強調下,接下來的所有努力都是為了“抗鋸齒”!應用Xfermode會使用Paint,就可以開啟抗鋸齒(通過Paint.ANTI_ALIAS_FLAG
標志或setAntiAlias
方法)。
接下來就是用上面的示例來完成抗鋸齒的圓角矩形。
Xfermode版本
要弄清楚apiDemo中的圓和矩形混合效果的實現,先來看下它的核心代碼:
class SampleView extends View {
private Bitmap mSrcB; // 源位圖,矩形
private Bitmap mDstB; // 目標位圖,圓
protected void onDraw(Canvas canvas) {
...
// draw the src/dst example into our offscreen bitmap
int sc = canvas.saveLayer(x, y, x + W, y + H, null,
Canvas.MATRIX_SAVE_FLAG |
Canvas.CLIP_SAVE_FLAG |
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
Canvas.CLIP_TO_LAYER_SAVE_FLAG);
canvas.translate(x, y);
canvas.drawBitmap(mDstB, 0, 0, paint);
paint.setXfermode(sModes[i]);
canvas.drawBitmap(mSrcB, 0, 0, paint);
paint.setXfermode(null);
canvas.restoreToCount(sc);
...
}
}
成員變量:
- mSrcB: 源位圖,矩形
- mDstB: 目標位圖,圓
可以看到,先繪制矩形,然后setXfermode(),然后繪制圓。
上面的代碼有一個“模板”:匹配的saveLayer()和restoreToCount()
調用。
canvas擁有layer的概念,canvas默認擁有一個初始的layer。可以通過方法int saveLayer (RectF bounds, Paint paint, int saveFlags)
產生新的layer。新layer相當于一個區域為傳遞的bounds的“新畫布”,它關聯一個bitmap(an offscreen bitmap,它是完全透明的),之后的繪制操作都在此bitmap上執行。每個layer可以看做一個獨立的畫布,所有layer形成一個棧,棧底是初始的layer。每次在棧頂產生的新layer,任何時候都在棧頂的layer上執行繪圖,調用restoreToCount()后棧頂layer出棧,其對應的bitmap的內容合并(進行像素的argb混合)到之前layer中。很顯然,最后也只應該剩下最初的layer,這樣保證所繪制內容都最終輸出到canvas的目標bitmap中,形成最終的內容(可以假想“畫布生成的內容就是bitmap”——帶顏色的像素區域)。
這里不嚴謹的認為:每個layer是一個canvas(畫布),畫布關聯一個Bitmap存儲最終繪制的內容。實際上不像現實中的畫布或畫紙,Canvas更像一個“繪圖工具集”,包含直尺,圓規等繪圖工具。skia文檔中對SkCanvas的解釋是“drawing context”——繪畫環境。它提供的都是有關繪制的API,而繪制的內容會輸出到Canvas的“繪制目標”——畫紙,可以是Bitmap(像素集合),或者Hardware-layer(具備硬件加速的Bitmap)和DisplayList(存儲繪制指令的序列而非最終的像素集合),從存儲繪制結果的角度看本質是一樣的。
上面的代碼中,onDraw()方法在新的layer中使用Xfermode繪圖模式來畫圓和矩形。原因是drawBitmap()會把參數bitmap繪制到layer對應的bitmap中(也許用詞上是胡說八道,但這樣可以理解吧?),Xfermode模式下后續drawBitmap()方法會以當前layer的“整個區域的內容”作為混合操作的參考bitmap,所以為了不讓之前layer已有內容對混合產生影響,就使用一個全新的layer——也就是全新的bitmap來進行混合繪制,最終再合并回去。
下面把各個方法的API介紹簡單羅列下,重點是Xfermode類和PorterDuffXfermode類。
方法saveLayer()
原型如下:
/**
* This behaves the same as save(), but in addition it allocates an
* offscreen bitmap. All drawing calls are directed there, and only when
* the balancing call to restore() is made is that offscreen transfered to
* the canvas (or the previous layer). Subsequent calls to translate,
* scale, rotate, skew, concat or clipRect, clipPath all operate on this
* copy. When the balancing call to restore() is made, this copy is
* deleted and the previous matrix/clip state is restored.
*
* @param bounds May be null. The maximum size the offscreen bitmap
* needs to be (in local coordinates)
* @param paint This is copied, and is applied to the offscreen when
* restore() is called.
* @param saveFlags see _SAVE_FLAG constants
* @return value to pass to restoreToCount() to balance this save()
*/
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
在API文檔中還有下面的說明:
public int saveLayer (RectF bounds, Paint paint, int saveFlags);
This behaves the same as save(), but in addition it allocates and redirects drawing to an offscreen bitmap.
Note: this method is very expensive, incurring more than double rendering cost for contained content. Avoid using this method, especially if the bounds provided are large, or if the CLIP_TO_LAYER_SAVE_FLAG is omitted from the saveFlags parameter. It is recommended to use a hardware layer on a View to apply an xfermode, color filter, or alpha, as it will perform much better than this method.
All drawing calls are directed to a newly allocated offscreen bitmap. Only when the balancing call to restore() is made, is that offscreen buffer drawn back to the current target of the Canvas (either the screen, it's target Bitmap, or the previous layer).
Attributes of the Paint - alpha, Xfermode, and ColorFilter are applied when the offscreen bitmap is drawn back when restore() is called.
上面說到在使用Xfermode時,可以開啟硬件加速(hardware layer)來直接繪制,此時不需要產生新的layer,會具有更好的性能,后面會給出這種實現。
方法restoreToCount()
原型如下:
/**
* Efficient way to pop any calls to save() that happened after the save
* count reached saveCount. It is an error for saveCount to be less than 1.
*
* Example:
* int count = canvas.save();
* ... // more calls potentially to save()
* canvas.restoreToCount(count);
* // now the canvas is back in the same state it was before the initial
* // call to save().
*
* @param saveCount The save level to restore to.
*/
public native void restoreToCount(int saveCount);
根據約定,在調用saveLayer()后,執行restoreToCount()將新layer中的內容合并回之前layer。
PorterDuffXfermode
方法android.graphics.Paint#setXfermode
用來為paint設置Xfermode。之后使用此paint繪制的圖像就會應用具體Xfermode子類所表示的“模式”。
類Xfermode的說明:
Xfermode is the base class for objects that are called to implement custom "transfer-modes" in the drawing pipeline. The static function Create(Modes) can be called to return an instance of any of the predefined subclasses as specified in the Modes enum. When an Xfermode is assigned to an Paint, then objects drawn with that paint have the xfermode applied.
Xfermode表示要在“繪制管線中使用的顏色傳遞模式”。概括來說,每一次繪圖操作(drawXxx)底層都執行一次繪制管線,通常要經過:路徑生成(Path Generation)、光柵化(Rasterization)、著色(Shading)和傳遞(Transfer)四個階段。管線操作的輸入就是draw**的輸入,包括方法對應繪制圖形圖像的參數信息,以及canvas layer關聯的目標bitmap (下面用Dst Image表示)。
在Transfer階段,會根據之前階段產生的“source image”和Dst Image生成一個intermediate image(中間圖片)。過程是把每個(x,y)處的source image和Dst Image的像素顏色值使用指定的傳遞模式(Xfermode,如果未指定,默認是PorterDuffXferMode(SRC_OVER))對應的函數,得到結果color,然后傳遞給中間圖片作為其(x,y)的color,最后中間圖片和Dst Image再進行混合(使用Mask),結果就是修改后的Dst Image。
Xfermode是一個基類,它的子類表示實際的顏色傳遞模式。子類PorterDuffXfermode表示:Porter/Duff 顏色混合算法,這里有篇文章Porter/Duff描述了它。在ApiDemo中給出了Porter/Duff模式支持的16種不同混合效果。
代碼實現
上面介紹了ApiDemo中核心代碼片段的含義,接下來就繼續沿用其saveLayer()、ResetoreToCount()以及Xfermode()這幾個步驟來實現圓角矩形。
得到Dst Image
本身要繪制的圖像就是Dst Image,在ImageView的onDraw方法中,super.onDraw(canvas)會將需要繪制的內容繪制到傳遞的canvas中,這里為了得到對應的bitmap,可以產生一個新的Canvas對象然后把它作為ImageView.onDraw的輸出目標:
// 得到原始的圖片
final int w = getWidth();
final int h = getHeight();
Bitmap bitmapOriginal = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmapOriginal);
super.onDraw(c);
上面的w、h是原始圖片的寬、高,根據文章開始的假定,就是ImageView的寬高。bitmapOriginal作為super.onDraw的繪制結果。這樣就得到了“Xfermode中的Dst Bitmap”。
得到Src Bitmap - 圓角矩形
為了四個角可配,繼續使用Path來得到圓角矩形,重要的是為Paint設置ANTI_ALIAS_FLAG標志開啟抗鋸齒:
// 四個角的x,y半徑
private float[] radiusArray = { 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f };
private Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap makeRoundRectFrame(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Path path = new Path();
path.addRoundRect(new RectF(0, 0, w, h), radiusArray, Path.Direction.CW);
Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bitmapPaint.setColor(Color.GREEN); // 顏色隨意,不要有透明度。
c.drawPath(path, bitmapPaint);
return bm;
}
在新layer中繪制
if (bitmapFrame == null) {
bitmapFrame = makeRoundRectFrame(w, h);
}
int sc = canvas.saveLayer(0, 0, w, h, null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(bitmapFrame, 0, 0, bitmapPaint);
// 利用Xfermode取交集(利用bitmapFrame作為畫框來裁剪bitmapOriginal)
bitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmapOriginal, 0, 0, bitmapPaint);
bitmapPaint.setXfermode(null);
canvas.restoreToCount(sc);
上面的saveLayer()接收的saveFlags是和canvas已設置的狀態相關的,canvas需要恢復哪些方面的屬性,就需要標記對應SAVE_FLAG來保存相應的狀態。
因為上面對Paint開啟了抗鋸齒,最終得到的圓角矩形就不像clipPath那種會在圓角處產生模糊。
Hardware Layer
根據saveLayer方法的文檔介紹,可以去掉saveLayer()/restoreToCount()的調用,只需要在onDraw()中開啟硬件加速就可以實現相同的目標了,性能會更好:
setLayerType(LAYER_TYPE_HARDWARE, bitmapPaint);
// 利用Xfermode取交集(利用bitmapFrame作為畫框來裁剪bitmapOriginal)
canvas.drawBitmap(bitmapFrame, 0, 0, bitmapPaint);
bitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmapOriginal, 0, 0, bitmapPaint);
bitmapPaint.setXfermode(null);
結論
上面分別給出了clipPath和Xfermode方式實現圓角矩形的方式,根據場景不同——在什么地方來實現需要的圓角矩形——其它等像基于shader的方式也許是更好的選擇。
強調下,上面代碼限制ImageView和它展示的內容必須是同樣大小的,否則就以實際顯示圖片的Rect作為“圓角矩形畫框”的Rect。
Android有關2D和3D的很多操作,像上面的clipPath和Xfermode,底層都是native方式執行的,framework層幾乎只是很薄的C++包裝。而且是比較專業的知識了,到底要了解多少,就看自己的app的需求,以及興趣了。
Canvas Api的底層實現是Skia,之后引入了opengl es的實現(HWUI),后者支持硬件加速。
(本文使用Atom編寫)
![]() |
不含病毒。www.avast.com |