作为Android开发,shape标签的使用定然不陌生。

shape标签基本使用语法

<?xml version="1.0" encoding="utf-8"?>
<shapexmlns:android="http://schemas.android.com/apk/res/android"android:shape=["rectangle" | "oval" | "line" | "ring"] ><cornersandroid:radius="integer"android:topLeftRadius="integer"android:topRightRadius="integer"android:bottomLeftRadius="integer"android:bottomRightRadius="integer" /><gradientandroid: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"] /><paddingandroid:left="integer"android:top="integer"android:right="integer"android:bottom="integer" /><sizeandroid:width="integer"android:height="integer" /><solidandroid:color="color" /><strokeandroid: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呢?继续往下走,就会发现答案是肯定的。

@NonNullstatic 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方法进行解析。

@NonNullDrawable 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里的这些属性又是怎么获取的呢?

@Overridepublic 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个步骤:

@Overridepublic void draw(Canvas canvas) {1、判断是否需要绘制,如果不需要绘制,则直接returnif (!ensureValidRect()) {// nothing to drawreturn;}2、获取各类变量,并依据useLayer变量设置对应的属性// remember the alpha values, in case we temporarily overwrite them// when we modulate them with mAlphafinal int prevFillAlpha = mFillPaint.getAlpha();final int prevStrokeAlpha = mStrokePaint != null ? mStrokePaint.getAlpha() : 0;// compute the modulate alpha valuesfinal 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 thestroke is non-opaque, and our shapetype actually supportsfill+stroke. Otherwise we can just draw the stroke (if any) on topof 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 itallows us to apply paint effects like alpha and colorfilter tothe result of multiple separate draws. In our case, if the userasks for a non-opaque alpha value (via setAlpha), and we'restroking, then we need to apply the alpha AFTER we've drawnboth 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 usmFillPaint.setColorFilter(null);mStrokePaint.setColorFilter(null);} else {/*  if we're not using a layer, apply the dither/filter to ourindividual 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方法调用,那么图层就会发生变化,如果不恢复那么后续都会在新图层上面进行绘制。

剖析Android shape标签的绘制相关推荐

  1. (转载)Android GradientDrawable(shape标签定义) 静态使用和动态使用(圆角,渐变实现)

    最近被吐槽界面太丑,还是很尴尬的,全公司就一个UI设计师,所以很多事情还是不忍直视,一个同事问我,背景可不可以使用渐变的感觉,然后我就有种突然感觉眼前一亮的感觉.还真的没有做过这方面的东西,单纯使用渐 ...

  2. android 背景描边,Android告别使用shape标签,自定义实现圆角、背景色、描边Button...

    为什么不使用shape标签 我想大家平常都用过shape标签来定义一个Drawable,来实现一些例如圆角.设置描边等一些需求.但是,最近发现项目中res/drawable/下的shape标签文件越来 ...

  3. Android studio 渐变色,android shape 之渐变色角度理解

    android shape 之渐变色角度理解 首先明确shape属于drawable的一种,汉语直译就是可绘制对象(熟悉的万物皆对象).所以shape文件在drawable文件夹下. 其次动手创建sh ...

  4. android shape 无边框颜色,Android 使用shape定义不同控件的的颜色、背景色、边框色...

    Android 使用shape定义不同控件的的颜色.背景色.边框色 设置按钮的右边框和底边框颜色为红色,边框大小为3dp: 在drawable新建一个 buttonstyle.xml的文件,内容如下: ...

  5. Android vector标签 PathData 画图超详解

    此文章来源于https://www.cnblogs.com/yuhanghzsd/p/5466846.html点击打开链接 Android vector标签 PathData 画图超详解 SVG是一种 ...

  6. android 常用渐变背景绘制

    从上到下绘制如图所示 1 <?xml version="1.0" encoding="utf-8"?> 2 <shape xmlns:andr ...

  7. Android Activity标签属性

    Android Activity标签属性 Activity 是 Android 系统四大应用组件之一,用户可与 Activity 提供的屏幕进行交互,以执行拨打电话.拍摄照片.发送电子邮件等操作开发者 ...

  8. android shape.xml 属性详解

    转载源:http://blog.csdn.net/harvic880925/article/details/41850723 一.简单使用 刚开始,就先不讲一堆标签的意义及用法,先简单看看shape标 ...

  9. Android - shape圆形画法(oval)

    shape圆形画法(oval) 本文地址: http://blog.csdn.net/caroline_wendy 1. 创建一个目录drawable, 用于存放xml类型的图片资源; 2. 在dra ...

  10. 安卓Android Studio标签

    文章目录 (一)继承关系图 (二)标签常用属性 (三)教学案例:标签演示 (一)继承关系图 TextView是View的子类 (二)标签常用属性 (三)教学案例:标签演示 1.创建安卓应用 基于Emp ...

最新文章

  1. CentOS7下的离线yum源搭建
  2. 每天5分钟玩转容器技术 ---- 系列文章
  3. Struts国际化步骤
  4. Oracle SQL Access Advisor 说明
  5. C++排序之stable_sort()的方法
  6. cuda Memory Fence Functions
  7. 分页总页数计算方法 所有分页通用
  8. Criteo数据集探索
  9. C++ Primer 5th - 1.1 编写一个简单的C++程序
  10. python怎么下载panda包_pandas python下载
  11. 续:~英语 1038个词根 217个后缀!
  12. 数字图像处理:实验八 遥感图像增强
  13. 超级计算机中心建设方案,我校举办大连理工大学超算中心建设方案论证会
  14. oracle环境变量NLS值,设置NLS_LANG环境变量
  15. 电力电子pwm控制技术
  16. 利用scp 在linux之间传输文件
  17. 关于全概率和贝叶斯公式的使用场景说明
  18. 【Altium Designer 21】单个元器件更新对应PCB封装
  19. Net Core WebApi自定义拦截特性简单实现
  20. 永安在线API安全管控平台正式发布,以情报建立API安全基线

热门文章

  1. android 连续播放动画,Android ObjectAnimator 无限循环播放,实现上下左右浮动效果...
  2. STM32F401的外部中断EXTI
  3. 虚拟机运行python_虚的解释|虚的意思|汉典“虚”字的基本解释
  4. 微信\支付宝扫码条码区分规则
  5. java压缩图片大小_java压缩图片、等比例压缩图片
  6. 游戏后台开发九问--linux平台
  7. android 焦点获取问题(手机端和TV端)
  8. c语言setw,在C++中,setw(int n)
  9. 全方位剖析“清华同方”,脉络千里!!
  10. Ransac算法原理,步骤,代码,实例,转载总结