作为Android开发,shape标签的使用定然不陌生。
shape标签基本使用语法
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape=["rectangle" | "oval" | "line" | "ring"] >
<corners
android:radius="integer"
android:topLeftRadius="integer"
android:topRightRadius="integer"
android:bottomLeftRadius="integer"
android:bottomRightRadius="integer" />
<gradient
android:angle="integer"
android:centerX="integer"
android:centerY="integer"
android:centerColor="integer"
android:endColor="color"
android:gradientRadius="integer"
android:startColor="color"
android:type=["linear" | "radial" | "sweep"]
android:useLevel=["true" | "false"] />
<padding
android:left="integer"
android:top="integer"
android:right="integer"
android:bottom="integer" />
<size
android:width="integer"
android:height="integer" />
<solid
android:color="color" />
<stroke
android:width="integer"
android:color="color"
android:dashWidth="integer"
android:dashGap="integer" />
</shape>
shape标签可用于各种背景绘制,然而每需要一个新的背景,即使只有细微的改动,诸如一个角度的改变、颜色的改变,都需要重新创建一个xml文件以配置新背景的shape标签。
通过了解shape标签是如何进行背景绘制的,就可以后续进行自定义属性开发来解放大量shape标签下的xml文件的创建。
Shape标签生成GradientDrawable对象
首先来了解一下,shape标签下的xml文件是如何最终被解析为GradientDrawable对象。
View对象的background属性最终是一个Drawable对象,shape标签下的xml文件也是被赋予给了background属性,最终也是生成了一个Drawable对象。
在View的构造函数中可看到是通过TypedArray.getDrawable获得Drawable对象赋予background属性。
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
....
background = a.getDrawable(attr);
....
}
追踪下去,Resources.loadDrawable -> ResourcesImpl.loadDrawable -> ResourcesImpl.loadXmlDrawable。
因为是在xml文件中定义,因此必然需要一个xml解析器进行解析。在此处就获取了一个XmlResourceParser,然后传入Drawable.createFromXmlForDensity。
private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density, String file)
throws IOException, XmlPullParserException {
try (
XmlResourceParser rp =
loadXmlResourceParser(file, id, value.assetCookie, "drawable")
) {
return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
}
}
平时解析layout文件的时候经常会使用LayoutInflater,那么Drawable是否也存在对应的DrawableInflater呢?继续往下走,就会发现答案是肯定的。
@NonNull
static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
@Nullable Theme theme) throws XmlPullParserException, IOException {
return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
density, theme);
}
此处通过Resources.getDrawableInflater获取到DrawableInflater,接着就使用DrawableInflater的inflateFromXmlForDensity方法进行解析。
@NonNull
Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, int density, @Nullable Theme theme)
throws XmlPullParserException, IOException {
....
Drawable drawable = inflateFromTag(name);
if (drawable == null) {
drawable = inflateFromClass(name);
}
drawable.setSrcDensityOverride(density);
drawable.inflate(mRes, parser, attrs, theme);
return drawable;
}
在DrawableInflater的inflateFromXmlForDensity方法中可以看见,通过inflateFromTag方法,生成了Drawable对象,并最终将其返回,那么shape标签生成GradientDrawable对象的逻辑就在该方法内了。
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;
}
}
一目了然,通过不同的标签名字生成相应的Drawable对象。shape标签生成GradientDrawable对象,selector标签生成StateListDrawable对象。
GradientDrawable获取shape子标签属性
看GradientDrawable必然要先看GradientState。
每一个Drawable的子类,都会有一个继承于ConstantState的内部静态类,它里面所声明的属性肯定都是这一个子类Drawable中独有的。
final static class GradientState extends ConstantState {
public @Shape int mShape = RECTANGLE;
public ColorStateList mSolidColors;
public ColorStateList mStrokeColors;
public int mStrokeWidth = -1;
public float mStrokeDashWidth = 0.0f;
public float mRadius = 0.0f;
public float[] mRadiusArray = null;
....
}
@IntDef({RECTANGLE, OVAL, LINE, RING})
@Retention(RetentionPolicy.SOURCE)
public @interface Shape {}
可以看到Shape定义了四个值的取值范围。那么GradientState里的这些属性又是怎么获取的呢?
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
super.inflate(r, parser, attrs, theme);
mGradientState.setDensity(Drawable.resolveDensity(r, 0));
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawable);
updateStateFromTypedArray(a);
a.recycle();
inflateChildElements(r, parser, attrs, theme);
updateLocalState(r);
}
在GradientDrawable.inflate里,通过inflateChildElements就能获取到各个子标签属性了。
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
TypedArray a;
int type;
....
String name = parser.getName();
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);
}
}
}
看到了在写shape标签下的xml文件时,熟悉的"corners"、"solid"、"gradient"。
以"corners"为例:
private void updateDrawableCorners(TypedArray a) {
final GradientState st = mGradientState;
// Account for any configuration changes.
st.mChangingConfigurations |= a.getChangingConfigurations();
// Extract the theme attributes, if any.
st.mAttrCorners = a.extractThemeAttrs();
final int radius = a.getDimensionPixelSize(
R.styleable.DrawableCorners_radius, (int) st.mRadius);
setCornerRadius(radius);
// TODO: Update these to be themeable.
final int topLeftRadius = a.getDimensionPixelSize(
R.styleable.DrawableCorners_topLeftRadius, radius);
final int topRightRadius = a.getDimensionPixelSize(
R.styleable.DrawableCorners_topRightRadius, radius);
final int bottomLeftRadius = a.getDimensionPixelSize(
R.styleable.DrawableCorners_bottomLeftRadius, radius);
final int bottomRightRadius = a.getDimensionPixelSize(
R.styleable.DrawableCorners_bottomRightRadius, radius);
if (topLeftRadius != radius || topRightRadius != radius ||
bottomLeftRadius != radius || bottomRightRadius != radius) {
// The corner radii are specified in clockwise order (see Path.addRoundRect())
setCornerRadii(new float[] {
topLeftRadius, topLeftRadius,
topRightRadius, topRightRadius,
bottomRightRadius, bottomRightRadius,
bottomLeftRadius, bottomLeftRadius
});
}
}
通过setCornerRadius和setCornerRadii,把角度值赋值给了mGradientState属性。
GradientDrawable进行shape绘制
绘制自然是在draw方法内了,大致可分为4个步骤:
@Override
public void draw(Canvas canvas) {
1、判断是否需要绘制,如果不需要绘制,则直接return
if (!ensureValidRect()) {
// nothing to draw
return;
}
2、获取各类变量,并依据useLayer变量设置对应的属性
// remember the alpha values, in case we temporarily overwrite them
// when we modulate them with mAlpha
final int prevFillAlpha = mFillPaint.getAlpha();
final int prevStrokeAlpha = mStrokePaint != null ? mStrokePaint.getAlpha() : 0;
// compute the modulate alpha values
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 : mBlendModeColorFilter;
/* we need a layer iff we're drawing both a fill and stroke, and the
stroke is non-opaque, and our shapetype actually supports
fill+stroke. Otherwise we can just draw the stroke (if any) on top
of the fill (if any) without worrying about blending artifacts.
*/
final boolean useLayer = haveStroke && haveFill && st.mShape != LINE &&
currStrokeAlpha < 255 && (mAlpha < 255 || colorFilter != null);
/* Drawing with a layer is slower than direct drawing, but it
allows us to apply paint effects like alpha and colorfilter to
the result of multiple separate draws. In our case, if the user
asks for a non-opaque alpha value (via setAlpha), and we're
stroking, then we need to apply the alpha AFTER we've drawn
both the fill and the stroke.
*/
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:
根据是否有角度,以及角度是否相同,分别采用canvas.drawRect、canvas.drawRoundRect、canvas.drawPath进行绘制
case OVAL:
使用canvas.drawOval进行绘制
case LINE:
使用canvas.drawLine进行绘制
case RING:
使用canvas.drawPath进行绘制
}
4、恢复现场
if (useLayer) {
canvas.restore();
} else {
mFillPaint.setAlpha(prevFillAlpha);
if (haveStroke) {
mStrokePaint.setAlpha(prevStrokeAlpha);
}
}
}
- 第一部分判断是否需要绘制全靠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是否非空(也就是mRect有一个非零的大小)。
mGradientIsDirty会在一些方法中被赋值为true,例如改变了颜色、改变了gradient相关的,这意味着mRect和gradient需要重新计算。
-
第二部分依据代码中的注释可以非常清楚,获取各类变量,并依据useLayer变量设置对应的属性。useLayer属性,只有在设置了边界(笔划/stroke)和内部填充模式,并且形状不是线型等条件下才为true。 1.根据设置的属性判断是否需要再绘制一个layer; 2.如果需要layer,则创建layer相关属性并根据属性创建新的图层; 3.如果不需要layer,则只设置相应的fill/stroke属性即可。
-
第三部分根据shape四种属性绘制对应的图形。需要注意的是,这里使用的canvas.drawXXXX方法,可能是saveLayer创建的新图层,也可能是没有变过的老图层。
对于RECTANGLE,根据是否有角度,以及角度是否相同,分别采用canvas.drawRect、canvas.drawRoundRect、canvas.drawPath进行绘制。对于OVAL,使用canvas.drawOval进行绘制。对于LINE,使用canvas.drawLine进行绘制。对于RING,先调用了buildRing方法返回一个Path对象,再使用canvas.drawPath进行绘制。 -
第四部分恢复现场,因为前面有saveLayer方法调用,那么图层就会发生变化,如果不恢复那么后续都会在新图层上面进行绘制。
网友评论