Android的ShapeDrawable和GradientDrawable源码解析

3,695 阅读11分钟

ShapeDrawable和GradientDrawable算是drawable子类中使用频率相当高的了,二者的名字显而易见,一个表示可绘制形状,另一个表示可绘制渐变(梯度)。
但是为什么要把这两个看着毫不相关的可绘制类放到一起讲呢?
二者类定义时的注释中写到这两类都可以通过在XML中的shape标签定义,按照我们正常的理解,shape标签定义的加载后应该是shape,gradient标签加载后的应该是gradient。但是使用过shape标签的同学应该知道,shape父标签只能定义一个shape形状,在其内部子标签中可以定义gradient标签。而且更更关键的是,我们通过shape标签定义的对象,在代码中加载出来之后竟然是GradientDrawable

<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比较相似,除了特有的方法以外,大部分的方法实现基本是一样的。

1. ShapeState

如果非要找ColorDrawable和ShapeDrawable之间的差别,那么最大的差别就在于ShapeDrawable中多了一个Shape的概念。回想一下ColorDrawable的draw方法,直接使用了canvas.drawRect方法绘制了一个矩形。而ShapeDrawable的draw方法,多了一层对Shape的处理。如果其设置了Shape形状,那么就会按照Shape的形状来进行绘制,如果没有设置Shape,那么就会调用canvas.drawRect方法绘制矩形。

Shape对象同样的,保存在了ConstantState的子类ShapeState中

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方法

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需要了解的内容只有这么多

  • 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的功能

<?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类的成员变量中都能找到其影子。

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等