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等