Android的ShapeDrawable和GradientDrawable源碼解析

ShapeDrawable和GradientDrawable算是drawable子類中使用頻率至關高的了,兩者的名字顯而易見,一個表示可繪製形狀,另外一個表示可繪製漸變(梯度)。
可是爲何要把這兩個看着絕不相關的可繪製類放到一塊兒講呢?
兩者類定義時的註釋中寫到這兩類均可以經過在XML中的shape標籤訂義,按照咱們正常的理解,shape標籤訂義的加載後應該是shape,gradient標籤加載後的應該是gradient。可是使用過shape標籤的同窗應該知道,shape父標籤只能定義一個shape形狀,在其內部子標籤中能夠定義gradient標籤。並且更更關鍵的是,咱們經過shape標籤訂義的對象,在代碼中加載出來以後居然是GradientDrawableandroid

<shape
    android:shape="rectangle"
    xmlns:android="http://schemas.android.com/apk/res/android">
</shape>
Drawable shape = getResources().getDrawable(R.drawable.shape);
shape.getClass().getSimpleName() // GradientDrawable
複製代碼

這也是爲何本文要把這兩個類放在一塊兒講述的緣由。程序員

(一)ShapeDrawable

和前面講到的ColorDrawable比較類似,除了特有的方法之外,大部分的方法實現基本是同樣的。canvas

1. ShapeState

若是非要找ColorDrawable和ShapeDrawable之間的差異,那麼最大的差異就在於ShapeDrawable中多了一個Shape的概念。回想一下ColorDrawable的draw方法,直接使用了canvas.drawRect方法繪製了一個矩形。而ShapeDrawable的draw方法,多了一層對Shape的處理。若是其設置了Shape形狀,那麼就會按照Shape的形狀來進行繪製,若是沒有設置Shape,那麼就會調用canvas.drawRect方法繪製矩形。數組

Shape對象一樣的,保存在了ConstantState的子類ShapeState中bash

static final class ShapeState extends ConstantState {
      // shape類本身的畫筆
      final @NonNull Paint mPaint;

      @Config int mChangingConfigurations;
      int[] mThemeAttrs;
      // 保存了對應的形狀
      Shape mShape;
      ColorStateList mTint;
      Mode mTintMode = DEFAULT_TINT_MODE;
      Rect mPadding;
      int mIntrinsicWidth; // 寬度
      int mIntrinsicHeight; // 高度
      int mAlpha = 255;// 透明度默認拉滿
      ShaderFactory mShaderFactory;// 着色器
  }
複製代碼

2. draw()

接下來看每個Drawable子類間差異最大的draw方法app

public void draw(Canvas canvas) {
      final Rect r = getBounds();// 獲取矩形範圍,默認爲0
      final ShapeState state = mShapeState;// 獲取關鍵的形狀狀態
      final Paint paint = state.mPaint;
      // 關鍵!繪製使用的是ShapeState中建立的畫筆Paint
      // ColorDrawable中是用的是類中一開始就建立的畫筆Paint

      final int prevAlpha = paint.getAlpha();// 獲取畫筆的透明度
      paint.setAlpha(modulateAlpha(prevAlpha, state.mAlpha));
      // 計算新透明度,經過setAlpha方法改變的是ShapeState中保存的alpha值

      // only draw shape if it may affect output
      if (paint.getAlpha() != 0 || paint.getXfermode() != null || paint.hasShadowLayer()) {
          final boolean clearColorFilter;
          if (mTintFilter != null && paint.getColorFilter() == null) {
              paint.setColorFilter(mTintFilter);
              clearColorFilter = true;
          } else {
              clearColorFilter = false;
          }

          if (state.mShape != null) {
              // need the save both for the translate, and for the (unknown)
              // Shape
              final int count = canvas.save(); // 保存當前畫布
              canvas.translate(r.left, r.top); 
              // 移動畫布到left,top後再繪製,至關於在新畫布的0,0位置,舊畫布的left,top處繪製
              onDraw(state.mShape, canvas, paint);
              // ShapeDrawable內的protected方法
              canvas.restoreToCount(count);// 恢復畫布
          } else {
              canvas.drawRect(r, paint); // 若是不存在Shape那麼就直接繪製矩形
          }

          if (clearColorFilter) {
              paint.setColorFilter(null);
          }
      }

      // restore
      paint.setAlpha(prevAlpha);
  }
複製代碼

3. onDraw()

那麼onDraw方法又作了些什麼?動畫

protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
       shape.draw(canvas, paint);// 每一個子類都會有本身的繪製流程
   }
複製代碼

至此,ShapeDrawable須要瞭解的內容只有這麼多ui

  • ShapeDrawable比ColorDrawable多了圖形的設置
  • shape標籤加載出來的drawable是GradientDrawable,代碼中強轉ShapeDrawable會報錯
  • 得到的Paint對象是保存在ShapeState中的
  • Shader着色器相關後續會探討

(二)GradientDrawable

前一節講述的ShapeDrawable的源代碼只有600多行,GradientDrawable的源代碼卻達到了2000+行。很明顯GradientDrawable的功能更多,至此咱們也可能稍微理解了一下爲何shape標籤加載後是GradientDrawable:shape標籤爲父標籤提供了基礎的形狀功能,gradient子標籤增長了相關的功能,因爲解析XML時還要判斷是否有gradient子標籤,因此這裏假設Google多是爲了減小複雜性,因此統一返回GradientDrawable(S有的功能G都有,G有的功能S卻沒有)
先來回顧一下gradient的功能this

<?xml version="1.0" encoding="utf-8"?>
<shape
    android:shape="rectangle"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke android:color="@color/colorAccent"/>
    <solid android:color="@color/colorAccent"/>
    <padding android:bottom="1dp"/>
    <size android:width="100dp"/>
    <corners android:radius="5dp"/>
    <gradient android:startColor="@color/colorPrimary"/>
</shape>
複製代碼

咱們發現,在shape子標籤下的全部屬性在GradientDrawable類的成員變量中都能找到其影子。spa

1. GradientState

每次看Drawable的子類時,第一個須要看的就是這個內部靜態類。它裏面所聲明的屬性確定都是這一個子類Drawable中獨有的。

final static class GradientState extends ConstantState {
        // 刪除部分紅員
        // 成員使用註解標記,從而存在取值範圍
        public @Shape int mShape = RECTANGLE;
        public @GradientType int mGradient = LINEAR_GRADIENT;
        ......
        public @ColorInt int[] mGradientColors;
        public @ColorInt int[] mTempColors; // no need to copy
        public float[] mTempPositions; // no need to copy
        public float[] mPositions;
        public int mStrokeWidth = -1; // if >= 0 use stroking.
        public float mStrokeDashWidth = 0.0f;
        public float mStrokeDashGap = 0.0f;
        public float mRadius = 0.0f; // use this if mRadiusArray is null
        public float[] mRadiusArray = null;
        ......
        @RadiusType int mGradientRadiusType = RADIUS_TYPE_PIXELS;
        boolean mUseLevel = false;
        boolean mUseLevelForShape = true;

        boolean mOpaqueOverBounds;
        boolean mOpaqueOverShape;

        ColorStateList mTint = null;
        PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;

        int mDensity = DisplayMetrics.DENSITY_DEFAULT;
        // 數組形式存儲的各種繪製參數
        int[] mThemeAttrs;
        ......
}
複製代碼

GradientDrawable和先前的Drawable的一個區別在於,其內部定義了許多註解,用於標記一些成員變量的取值範圍。從上面的代碼也能夠看出,對於Shape其定義了四個值:

@IntDef({RECTANGLE, OVAL, LINE, RING})
@Retention(RetentionPolicy.SOURCE)
public @interface Shape {}
複製代碼

除此以外,相比前面講述的其餘State多了一些對應的get/set方法,諸如

public void setStroke(int width, @Nullable ColorStateList colors, float dashWidth,
                float dashGap) {
            mStrokeWidth = width;
            mStrokeColors = colors;
            mStrokeDashWidth = dashWidth;
            mStrokeDashGap = dashGap;
            computeOpacity();
        }
複製代碼

同時能夠看到,在GradientDrawable中,存在和shape的子標籤一一對應的get/set方法,由此咱們能夠知道並非Google的開發人員弄錯了(Google程序員怎麼可能會錯(手動滑稽)),而是GradientDrawable包含了幾乎所有的繪製功能,而不只僅是一個圖形(也有setShape方法)。它比ShapeDrawable更加具體,所以通用了shape父標籤。

2.draw()

每個Drawable子類的最大區別基本就在該方法中體現。
這個GradientDrawable的draw方法相比前面介紹的幾種更加複雜,由於有了漸變、邊線、圓角等額外處理。
這裏能夠簡單分爲四個階段進行繪製 首先看一下draw方法的大概流程

public void draw(Canvas canvas) {
        // 1.判斷是否須要繪製,若是不須要繪製,則直接return
        if (!ensureValidRect()) {
            // nothing to draw
            return;
        }
        // 2.獲取各種變量,並依據useLayer變量設置對應的屬性
        final int prevFillAlpha = mFillPaint.getAlpha();
        final int prevStrokeAlpha = mStrokePaint != null ? mStrokePaint.getAlpha() : 0;
        final int currFillAlpha = modulateAlpha(prevFillAlpha);
        final int currStrokeAlpha = modulateAlpha(prevStrokeAlpha);

        final boolean haveStroke = currStrokeAlpha > 0 && mStrokePaint != null &&
                mStrokePaint.getStrokeWidth() > 0;
        final boolean haveFill = currFillAlpha > 0;
        final GradientState st = mGradientState;
        final ColorFilter colorFilter = mColorFilter != null ? mColorFilter : mTintFilter;


        final boolean useLayer = haveStroke && haveFill && st.mShape != LINE &&
                 currStrokeAlpha < 255 && (mAlpha < 255 || colorFilter != null);

        if (useLayer) {
            if (mLayerPaint == null) {
                mLayerPaint = new Paint();
            }
            mLayerPaint.setDither(st.mDither);
            mLayerPaint.setAlpha(mAlpha);
            mLayerPaint.setColorFilter(colorFilter);

            float rad = mStrokePaint.getStrokeWidth();
            canvas.saveLayer(mRect.left - rad, mRect.top - rad,
                             mRect.right + rad, mRect.bottom + rad,
                             mLayerPaint);

            // don't perform the filter in our individual paints // since the layer will do it for us mFillPaint.setColorFilter(null); mStrokePaint.setColorFilter(null); } else { /* if we're not using a layer, apply the dither/filter to our
                individual paints
            */
            mFillPaint.setAlpha(currFillAlpha);
            mFillPaint.setDither(st.mDither);
            mFillPaint.setColorFilter(colorFilter);
            if (colorFilter != null && st.mSolidColors == null) {
                mFillPaint.setColor(mAlpha << 24);
            }
            if (haveStroke) {
                mStrokePaint.setAlpha(currStrokeAlpha);
                mStrokePaint.setDither(st.mDither);
                mStrokePaint.setColorFilter(colorFilter);
            }
        }
        // 3.根據shape屬性繪製對應的圖形
        switch (st.mShape) {
            case RECTANGLE:
               // 省略
                break;
            case OVAL:
                // 省略
                break;
            case LINE:
                // 省略
                break;
            case RING:
                // 省略
                break;
        }
        // 4.恢復現場
        if (useLayer) {
            canvas.restore();
        } else {
            mFillPaint.setAlpha(prevFillAlpha);
            if (haveStroke) {
                mStrokePaint.setAlpha(prevStrokeAlpha);
            }
        }
    }
複製代碼

2.1 ensureValidRect()

先看一段代碼註釋

/**
     * This checks mGradientIsDirty, and if it is true, recomputes both our drawing
     * rectangle (mRect) and the gradient itself, since it depends on our
     * rectangle too.
     * @return true if the resulting rectangle is not empty, false otherwise
     檢查變量mGradientIsDirty,若是是true,那麼就從新計算mRect和gradient
     返回值:mRect(GradientDrawable最開始會初始化new Rect())是否爲空,返回!mRect.isEmpty()
     */
     mRect.isEmpty()方法返回的是left >= right || top >= bottom
     因此說,若是咱們沒有給mRect賦值一個非0的大小,那麼isEmpty就是true
     看名字也知道是「確保可用的矩形」,即最終返回true表示是valid
複製代碼

從代碼註釋中能夠看到,該方法和mGradientIsDirty相關,那麼這個變量什麼時候有改變呢?

在以上的幾個方法中,mGradientIsDirty會被設置爲true,這就對應着註釋中要從新計算的那一部分

private boolean ensureValidRect() {
        // 能夠看到只有一個大的if,若是值爲false,則直接走到最後一句return
        // 這個大if代碼塊裏,都是對漸變塊的處理,利用mRect的範圍更新內部漸變色塊
        if (mGradientIsDirty) {
            mGradientIsDirty = false;

            Rect bounds = getBounds();
            float inset = 0;

            if (mStrokePaint != null) {
                inset = mStrokePaint.getStrokeWidth() * 0.5f;
            }

            final GradientState st = mGradientState;

            mRect.set(bounds.left + inset, bounds.top + inset,
                      bounds.right - inset, bounds.bottom - inset);

            final int[] gradientColors = st.mGradientColors;
            if (gradientColors != null) {
                final RectF r = mRect;
                final float x0, x1, y0, y1;

                if (st.mGradient == LINEAR_GRADIENT) {
                    // 省略對應的邏輯
                } else if (st.mGradient == RADIAL_GRADIENT) {
                    // 省略對應的邏輯
                } else if (st.mGradient == SWEEP_GRADIENT) {
                    // 省略對應的邏輯
                }
                // 若是沒有設置顏色,則填充顏色默認按照黑色
                if (st.mSolidColors == null) {
                    mFillPaint.setColor(Color.BLACK);
                }
            }
        }
        // 能夠看到,並無對mRect進行處理,只是更新其內部gradient的狀態,這也和變量名相呼應
        // 因此,若是一開始就沒有設置大小,那麼這裏返回確定是false
        return !mRect.isEmpty();
    }
複製代碼

這裏,draw方法的第一部分就結束了,總結一下就是: 1.先判斷是否更新了內部漸變色塊的屬性,若是沒有變動,直接返回區域是否不爲空 2.若是有變動,則先按照區域大小和邊界大小對漸變色塊進行更新,而後返回區域是否不爲空 3.若是區域mRect爲空,那麼draw方法就會直接return,避免了無用繪製

2.2 useLayer變量相關

final boolean useLayer = haveStroke && haveFill && st.mShape != LINE &&
            currStrokeAlpha < 255 && (mAlpha < 255 || colorFilter != null);
    // useLayer屬性,只有在設置了邊界(筆劃/stroke)和內部填充模式,而且形狀不是線型等條件下才爲true
    if (useLayer) {
            if (mLayerPaint == null) {
                mLayerPaint = new Paint();
            }
            mLayerPaint.setDither(st.mDither);
            mLayerPaint.setAlpha(mAlpha);
            mLayerPaint.setColorFilter(colorFilter);

            float rad = mStrokePaint.getStrokeWidth();
            // mRect+stroke區域被保存下來,由於是一個層級
            // X軸向右正向,Y軸向下正向
            // 同時,建立了一個新的繪製圖層
            canvas.saveLayer(mRect.left - rad, mRect.top - rad,
                             mRect.right + rad, mRect.bottom + rad,
                             mLayerPaint);
            // mFillPaint和mStrokePaint都是開發者可控的
            mFillPaint.setColorFilter(null);
            mStrokePaint.setColorFilter(null);
        } else {
            mFillPaint.setAlpha(currFillAlpha);
            mFillPaint.setDither(st.mDither);
            mFillPaint.setColorFilter(colorFilter);
            if (colorFilter != null && st.mSolidColors == null) {
                mFillPaint.setColor(mAlpha << 24);
            }
            if (haveStroke) {
                mStrokePaint.setAlpha(currStrokeAlpha);
                mStrokePaint.setDither(st.mDither);
                mStrokePaint.setColorFilter(colorFilter);
            }
        } 
    }
複製代碼

至此,第二部分也結束了。總結一下就是: 1.其根據設置的屬性判斷是否須要再繪製一個layer 2.若是須要layer,則建立layer相關屬性並根據屬性建立新的圖層 3.若是不須要layer,則只設置相應的fill/stroke屬性便可

2.3 真正的繪製

這裏使用switch---case對四種不一樣的形狀進行繪製。
須要注意的是,這是時候使用的canvas.drawXXXX方法,多是saveLayer建立的新圖層,也多是沒有變過的老圖層。

  • 對於橢圓圖形,有對應的drawOval方法等能夠直接調用
  • 對於線型圖形,有對應的drawLine方法等能夠直接調用
  • 對於環形圖形,先調用了buildRing方法返回一個Path對象,在根據path調用drawPath方法方法完成環形繪製
  • 對於矩形圖形,只須要額外處理一下圓角問題便可,分別調用drawRoundRect/drawRect完成矩形繪製。可能有人會問了,我沒設置四個圓角啊,可是drawRoundRect繪製的是四個同樣的圓角啊?--這位同窗,問題不錯。
case RECTANGLE:
        if (st.mRadiusArray != null) {
            buildPathIfDirty();// 該方法用於建立非四個相同圓角矩形路徑path,源碼是使用path的形式進行繪製的哦!
            canvas.drawPath(mPath, mFillPaint);
            if (haveStroke) {
               canvas.drawPath(mPath, mStrokePaint);
            }
            
複製代碼

2.4 恢復現場

由於前面有saveLayer方法調用,那麼圖層就會發生變化,若是不恢復那麼後續都會在新圖層上面進行繪製。

if (useLayer) {
            canvas.restore();// 恢復圖層
        } else {
            mFillPaint.setAlpha(prevFillAlpha);// 恢復透明度
            if (haveStroke) {
                mStrokePaint.setAlpha(prevStrokeAlpha);
            }
        }
複製代碼

3.shape標籤是GradientDrawable的關鍵證據

咱們不從ResourceImpl分析,只看GradientDrawable的源碼,由於從xml中定義,必然會有一個xml的解析過程,那麼就找一下。
還真找到了。。。

private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
            Theme theme) throws XmlPullParserException, IOException {
        TypedArray a;
        int type;

        final int innerDepth = parser.getDepth() + 1;
        int depth;
        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
               && ((depth=parser.getDepth()) >= innerDepth
                       || type != XmlPullParser.END_TAG)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            if (depth > innerDepth) {
                continue;
            }

            String name = parser.getName();
            // 能夠看到這些都是shape父標籤下的子標籤對應的字段
            if (name.equals("size")) {
                a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSize);
                updateGradientDrawableSize(a);
                a.recycle();
            } else if (name.equals("gradient")) {
                a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableGradient);
                updateGradientDrawableGradient(r, a);
                a.recycle();
            } else if (name.equals("solid")) {
                a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSolid);
                updateGradientDrawableSolid(a);
                a.recycle();
            } else if (name.equals("stroke")) {
                a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableStroke);
                updateGradientDrawableStroke(a);
                a.recycle();
            } else if (name.equals("corners")) {
                a = obtainAttributes(r, theme, attrs, R.styleable.DrawableCorners);
                updateDrawableCorners(a);
                a.recycle();
            } else if (name.equals("padding")) {
                a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawablePadding);
                updateGradientDrawablePadding(a);
                a.recycle();
            } else {
                Log.w("drawable", "Bad element under <shape>: " + name);
            }
        }
    }
複製代碼

那究竟是在哪裏解析了呢?平時咱們解析layout文件的時候常常會使用LayoutInflater,那麼必然的,Drawable也存在對應的DrawableInflater

private Drawable inflateFromTag(@NonNull String name) {
        switch (name) {
            case "selector":
                return new StateListDrawable();
            case "animated-selector":
                return new AnimatedStateListDrawable();
            case "level-list":
                return new LevelListDrawable();
            case "layer-list":
                return new LayerDrawable();
            case "transition":
                return new TransitionDrawable();
            case "ripple":
                return new RippleDrawable();
            case "adaptive-icon":
                return new AdaptiveIconDrawable();
            case "color":
                return new ColorDrawable();
            case "shape":
                return new GradientDrawable();
            case "vector":
                return new VectorDrawable();
            case "animated-vector":
                return new AnimatedVectorDrawable();
            case "scale":
                return new ScaleDrawable();
            case "clip":
                return new ClipDrawable();
            case "rotate":
                return new RotateDrawable();
            case "animated-rotate":
                return new AnimatedRotateDrawable();
            case "animation-list":
                return new AnimationDrawable();
            case "inset":
                return new InsetDrawable();
            case "bitmap":
                return new BitmapDrawable();
            case "nine-patch":
                return new NinePatchDrawable();
            case "animated-image":
                return new AnimatedImageDrawable();
            default:
                return null;
        }
    }
複製代碼

至此,咱們也知道了shape標籤下的xml最終會被解析爲GradientDrawable。

下一篇將講講剩下簡單一點的諸如LevelList/Picture/StateList等

相關文章
相關標籤/搜索