Android里,我們經常會用shape去定義View的形狀。如下是在xml里定義一個簡單shape的代碼:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#00FF00" />
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
</shape>
使用時,將它設置在view 的背景上,有的同學這樣問,如下使用shape,為什么不起作用?
第一例, 不起作用:
<ImageView
android:id="@+id/img_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/round_corner_rectangle"
android:scaleType="fitXY"
android:src="@drawable/img"/>
第二例,不起作用,看不到圓角效果
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/round_corner_rectangle">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
第三例,TextView 有圓角,正常
<TextView
android:background="@drawable/round_corner_rectangle"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
首先,shape是什么?
以圓角矩形<shape>為例,其中 shape 標簽在解析后對應于 GradientDrawable類(注意不是ShapeDrawable),即在xml里定義<shape>,運行期間會生成對應的GradientDrawable對象,同時傳入xml里定義的圓角屬性值。
查看GradientDrawable 源碼,將看到在xml里<shape>設定的各個角圓角弧度,被傳入并保存在數(shù)組mRadiusArray:
private void updateDrawableCorners(TypedArray a) {
......
setCornerRadii(new float[] {
topLeftRadius, topLeftRadius,
topRightRadius, topRightRadius,
bottomRightRadius, bottomRightRadius,
bottomLeftRadius, bottomLeftRadius
});
}
所以,設定shape標簽即設生成drawable 對象。
- Drawable 可以理解為:二維平面上,能畫出來的圖形圖像,如:BitmapDrawable, ShapeDrawable, PictureDrawable, LayerDrawable, 等等派生類。Drawable 都有自己的draw() 方法,來操縱 canvas
- canvas 畫布是透明的,可以在上面涂抹任意形狀,并填充上顏色、漸變等,即 Drawable
繼續(xù)查看GradientDrawable源碼,其繪制過程是基本圖形繪制,涉及:Canvas、Path、 Paint。其中path 定義封閉形狀,并設定好圓角,paint 畫筆設置顏色等,最終在canvas 畫布上畫出圖形,步驟如下:
- path定義封閉形狀代碼如下:
private void buildPathIfDirty() {
final GradientState st = mGradientState;
if (mPathIsDirty) {
ensureValidRect();
mPath.reset();
mPath.addRoundRect(mRect, st.mRadiusArray, Path.Direction.CW);
mPathIsDirty = false;
}
}
- 畫線及填充,558行:
switch (st.mShape) {
case RECTANGLE:
if (st.mRadiusArray != null) {
buildPathIfDirty();
// 畫線及填充
canvas.drawPath(mPath, mFillPaint);
if (haveStroke) {
// 描邊
canvas.drawPath(mPath, mStrokePaint);
}
}
以上分析了定義一個 圓角矩形時,GradientDrawable 將在 canvas上自我繪制的過程。
View設置各種drawable為背景,怎么起作用的?
以第三例為例,設置TextView的background,先了解以下基礎:
- TextView 繼承自View基類
- 設置各種背景都將轉化為drawable對象
- View 里有一個公用畫布 canvas
查看View源碼,View 里背景和內容的繪制步驟:
- 首先繪制底部 background
- 繪制具體的內容,通過onDraw 通知繼承View類子類繪制具體內容。
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) 的源碼及注釋 16153 行:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 接著,繪制內容,dispatchDraw 通知該 View 上的子結點進行自我繪制。
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
繪制Background 的過程,簡化一下即為drawable 直接調用自身 draw 方法,在同一畫布上進行繪制。
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
…
background.draw(canvas);
}
以上分析解釋了View繪制背景和內容的區(qū)別,同時,也順便可以解釋Imageview 的 background 和 src 的不同之處:
- 本質上無區(qū)別,都是各種不同類型的drawable,本質上都通過自身的draw方法在canvas上繪制。
- background 是背景,首先會在View基類的draw里被繪制。src 是內容,隨后在子類ImageView的 ondraw 里被繪制。
這里驗證一下,如果將 android:background=“@null” 會發(fā)生什么
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:text="此處背景透明"
android:layout_alignParentBottom="true"/>
會發(fā)現(xiàn),將會取和設置 transparent 也是一樣透明的效果。
將會看到,是和設置背景為 transparent 一樣透明的效果。
回頭來看文中開頭處提到的shape不起作用的例子:
第二例
其中TextView為外部 FrameLayout 的子結點,外部FrameLayout設置的的標簽與TextView無關,TextView的繪制范圍僅寬高受FrameLayout的影響,標簽只代表了一個圖像,不影響子節(jié)點。
解決方法:
FrameLayout 設置padding, 或者TextView設置 margin,padding要大于等于sqrt(r),其中r為所設圓角半徑值,并且兩者背景顏色一致。為何為sqrt(r),請自行畫圖計算。
第一例
ImageView 設置圓角為何不起作用。參見 ImageView 里源碼,src 對應 mDrawable,繪制時,將覆蓋底層 background,即設置了圓角的drawable。
private void updateDrawable(Drawable d) {
……
mDrawable = d;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
…
mDrawable.draw(canvas);
}
解決辦法
那該怎么給ImageView 畫圓角呢?辦法是通過paint 的SRC_IN模式:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
SRC_IN 模式設置后,將兩個繪制的效果疊加后取交集后展現(xiàn),比如:第一個繪制的是個圓形,第二個繪制的是個Bitmap,于是交集為圓形,就實現(xiàn)了圓形圖片效果。
而且,android Tint 也是靠 SRC_IN 來自動變成我們想要的背景顏色,來達到Material Design的效果。