為什么 Shape 不起作用

基礎知識

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,先了解以下基礎:

  1. TextView 繼承自View基類
  2. 設置各種背景都將轉化為drawable對象
  3. View 里有一個公用畫布 canvas

查看View源碼,View 里背景和內容的繪制步驟:

  1. 首先繪制底部 background
  2. 繪制具體的內容,通過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 的不同之處:

  1. 本質上無區(qū)別,都是各種不同類型的drawable,本質上都通過自身的draw方法在canvas上繪制。
  2. 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的效果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容