Android shape标签使用方法介绍

目录
  • shape标签基本使用语法
  • Shape标签生成GradientDrawable对象
  • GradientDrawable获取shape子标签属性
  • GradientDrawable进行shape绘制

作为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方法调用,那么图层就会发生变化,如果不恢复那么后续都会在新图层上面进行绘制。

到此这篇关于Android shape标签使用方法介绍的文章就介绍到这了,更多相关Android shape标签内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android中shape的自定义艺术效果使用

    shape形状之意,可自定义各种形状,如背景椭圆,圆角等等 创建目录:drawable–右键–new–drawable resourse file–键入文件名my_shape–ok–修改selector标签为shape 1圆角 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android&quo

  • Android Shape属性创建环形进度条

    1,实现效果 2,实现代码: [1] shape_drawable.xml 文件 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:innerRadius="20dp" android:shape="ring" an

  • Android shape 绘制图形的实例详解

    Android shape 绘制图形 Android 绘制图形可以使用shape也可以使用自定义控件的方式,这里我们说下shape的方式去实现. 在绘制图形之前,我们先来了解下shape的几个属性. shape /* * 线行 圆形 矩形 / android:shape="line" android:shape="oval" android:shape="rectangle" size 图形的大小 <size android:height=

  • Android使用shape绘制阴影图层阴影效果示例

    最近在项目中用到一个比较有意思的阴影特效 从上面的效果图上可以发现在图片的右.下有一个阴影的特效,通过这个阴影效果明显会使得这个提示文本有一种立体的观感.瞬间高大上有木有? 基于以上UI效果,有两种最基本的实现方式:UI给出阴影底图和程序猿自我实现两种. 在这里UI设计师给出阴影底图的方式我们就不做讨论了,我们来看下程序猿自我实现的方式怎么做. 首先我们来分析一下上面UI效果,我们不难发现其实上图所示的ui效果本质上可以看成两个图层的叠加,那么有的小伙伴就要说了不就是两个图层的叠加嘛,用画笔(p

  • Android shape与selector标签使用详解

    目录 shape corners 圆角 stroke 边框 solid 填充背景色 gradient 渐变 圆形背景 ripple 水波纹 selector 标签 文本选中变色示例 checkbox选中效果变化示例 补充 关于透明色效果 透明度大全 Android中提供一种xml的方式,让我们可以自由地定义背景,比较常用的就是shape标签和selector标签 shape shape的翻译为形状的意思,一般用来定义背景的形状,如长方形,线条,圆形 rectangle 矩形 默认 oval 椭圆

  • android shape实现阴影或模糊边效果

    1.实现阴影或模糊边效果方式: 2.通过shape来实现,具体是通过layer-list 多层叠放的方式实现的 <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 边 --> <item> <shape androi

  • 详解Android用Shape制作单边框图的两种思路和坑

    开发中遇到单/多边框的UI,简单的可以自己写shape图,复杂的一般都让设计配合制作9patch图了. 今天不说需要切图的情况,只聊简单的单/多边框,主要是实现思路. 效果很简单: 就以上图为例介绍,只有上边框,边框红色.宽1dp,其余为白色. 思路一 两层画布叠加:底层红色:上层白色: 上层白色画布下移1dp. 代码实现: <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:andro

  • Android中Shape的用法详解

    ShapeDrawable是一种很常见的Drawable,可以理解为通过颜色来构造的图形,它既可以是纯色的图形,也可以是具有渐变效果的图形,ShapeDrawabled语法稍显复杂,如下所示: <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape=["rectangle" | "oval" | "line" | &q

  • Android shape标签使用方法介绍

    目录 shape标签基本使用语法 Shape标签生成GradientDrawable对象 GradientDrawable获取shape子标签属性 GradientDrawable进行shape绘制 作为Android开发,shape标签的使用定然不陌生. shape标签基本使用语法 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.an

  • Android判断是否Root方法介绍

    为了照顾那些着急的同学,先直接给出结论: private static final String[] rootRelatedDirs = new String[]{ "/su", "/su/bin/su", "/sbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/data/local/su", "/system/xb

  • Android LeakCanary的使用方法介绍

    目录 1.LeakCanary 如何自动初始化 2.LeakCanary如何检测内存泄漏 2.1LeakCanary初始化时做了什么 2.2LeakCanary如何触发检测 2.3LeakCanary如何检测泄漏的对象 2.4弱引用 WeakReference 1.LeakCanary 如何自动初始化 LeakCanary只需添加依赖就可以实现自动初始化.LeakCanary是通过ContentProvider实现初始化的,在ContentProvider 的 onCreate方法中初始化Lea

  • Android手机使用Fiddler方法介绍

    Fiddler是一款免费的抓包.调试工具,比Wireshark要小巧,更简洁,本节就来介绍如何通过WIFI来抓取Android手机的HTTP和HTTPS包. 一.连接Android手机 1.1.手机端配置 电脑配置WIFI后,手机需要设置当前WIFI的代理,而这个代理地址可以通过电脑的"ipconfig"命令得到,如下图所示: 以上的192.168.191.1就是本机的IP地址,然后在手机端的WIFI的高级设置中将代理地址设置为查询到的IP,端口号码自己定义,一会儿要用到,IP代理模式

  • android startActivityForResult的使用方法介绍

    Activity 跳转 都知道用startActivity(Intent)但是如果下面情况呢?Activity1 跳转到 Activity2  但是还需要在Activity2 再回到 Activity1呢? 可能有人说: 那我在Activity2  再使用 startActivity() 不就可以了 是的 但是 startActivityForResult() 能够直接完成这项工作[示例]Activity1: 有2个EditText 用于接收用户输入的2个字符串 要求把这2个字符串连接起来 我现

  • Android popupwindow简单使用方法介绍

    先看下效果 1.首页 package com.yskj.jh.demopopupwindow; import android.content.Context; import android.graphics.drawable.BitmapDrawable; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.LayoutInflater; import and

  • Android开发实现文件关联方法介绍

    Android开发实现文件关联方法,做一个项目的时候,需要点击文件打开我们自己的app.首先讲一下点击普通文件打开app的方法,只需要三行代码,在app启动活动里加一个过滤器. <intent-filter> <category android:name="android.intent.category.LAUNCHER"> </category></action></intent-filter> <intent-fil

  • android真机调试时无法显示logcat信息的解决方法介绍

    android真机调试时无法显示logcat信息的解决方法介绍: window-->show view-->android->devices, 打开devices,点击右边的截屏图片的按钮.等到出现截图的时候,logcat就出来信息了!

  • Android 中LayoutInflater.inflate()方法的介绍

    Android 中LayoutInflater.inflate()方法的介绍 最近一直想弄明白LayoutInflater对象的inflate方法的用法,今天做了实例. <LinearLayout android:id="@+id/ll_item_Group" android:layout_width="match_parent" android:layout_height="200dp" android:background="

随机推荐