剖析Android shape标签的绘制

2022-06-17  本文已影响0人  愿天深海

作为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);
            }
        }
    }
/**
     * 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需要重新计算。

上一篇下一篇

猜你喜欢

热点阅读