1. 程式人生 > >Android 動態設定Shape

Android 動態設定Shape

引言:之前涉及到設定view背景的地方几乎都是通過寫<shape>標籤的方式實現的。慢慢的,專案裡的xml越來越多,命名都成問題了!於是就想用動態設定shape的方式來替換靜態配置shape標籤。

靜態配置shape

這裡對形狀可繪製物件的描述感覺有點出入,寫的是建立ShapeDrawable,但是進入對應的條目後發現是GradientDrawable.

動態設定shape

想要動態配置,首先需要知道在xml中寫的<shape><selector><level-list> 等標籤的對映物件是什麼。這裡有個插曲,最開始我也以為<shape>

標籤對應的就是ShapeDrawable呢,寫的時候發現沒法描邊(Stroke), 試了兩種方案 1.通過設定描邊畫筆,給paint設定寬度和顏色;2.用LevelListDrawable 通過設定多個層一個填充層,一個描邊層來組合。結果這兩種方法都不行,後來才去翻的原始碼。從view.setBackgroundResource()開始追。

Resources.java

  public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
            throws NotFoundException {
        final TypedValue value
= obtainTempTypedValue(); try { final ResourcesImpl impl = mResourcesImpl; impl.getValue(id, value, true); // 從這裡開始載入Drawable return impl.loadDrawable(this, value, id, theme, true); } finally { releaseTempTypedValue(value); } }

ResourcesImpl.java

  @Nullable
  Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
            boolean useCache) throws NotFoundException {
        try {
            ......

            final Drawable.ConstantState cs;
            if (isColorDrawable) {
                cs = sPreloadedColorDrawables.get(key);
            } else {
                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
            }

            Drawable dr;
            if (cs != null) {
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            } else {
                // 從xml或者資源流中載入Drawable
                dr = loadDrawableForCookie(wrapper, value, id, null);
            }
            return dr;
        } catch (Exception e) {
            ......
        }
    }
    /**
     * Loads a drawable from XML or resources stream.
     * 從xml或者資料流中載入Drawable
     */
    private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id, Resources.Theme theme) {

        final String file = value.string.toString();
        ......

        final Drawable dr;

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        try {
            if (file.endsWith(".xml")) {
            // ok,我們看下這裡,通過解析器把xml檔案解析成Drawable物件
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXml(wrapper, rp, theme);
                rp.close();
            } else {
            // 流的解析
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
                is.close();
            }
        } catch (Exception e) {
            ......
        }
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

        return dr;
    }

Drawable.java

    // 使用可選的theme從XML文件中建立drawable物件。
    public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme)
            throws XmlPullParserException, IOException {
        AttributeSet attrs = Xml.asAttributeSet(parser);
        ......
        // 從xml內部以指定主題建立一個Drawable物件
        Drawable drawable = createFromXmlInner(r, parser, attrs, theme);

        if (drawable == null) {
            throw new RuntimeException("Unknown initial tag: " + parser.getName());
        }

        return drawable;
    }
    // 從xml內部以指定主題建立一個Drawable物件
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
            Theme theme) throws XmlPullParserException, IOException {
        return r.getDrawableInflater().inflateFromXml(parser.getName(), parser, attrs, theme);
    }

追到這裡發現是從Resources 中呼叫了一個方法。在Resources.java 中搜索getDrawableInflater()這個方法,出現了DrawableInflater這個類。
Resources.java

    // 該inflater用於建立Drawable物件
    public final DrawableInflater getDrawableInflater() {
        if (mDrawableInflater == null) {
            mDrawableInflater = new DrawableInflater(this, mClassLoader);
        }
        return mDrawableInflater;
    }

沒法直接追下去了,既然這樣我們在原始碼中直接搜尋DrawableInflater.java這個類,然後檢視它裡面的inflateFromXml() 方法。(注:我這裡的原始碼版本是android-25)
DrawableInflater.java

    @NonNull
    @SuppressWarnings("deprecation")
    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 "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();
            default:
                return null;
        }
    }

到這裡總該明白與xml中的標籤(shape等標籤)相對應的物件是哪些了吧。專案中用的最多的就是圓角矩形背景了,然後有的會有描邊,還有選擇器的效果。我們就用GradientDrawable 來實現。GradientDrawable可以用來設定shape型別、shape填充色、描邊色和矩形的邊角弧度。

動態設定shape程式碼

以圓角矩形為例:
android 動態設定shape

    /**
     * 獲得一個指定填充色,邊框寬度、顏色的圓角矩形drawable。
     * Android 中 在xml中寫的"shape"標籤對映物件就是GradientDrawable。
     * 通過設定solidColors 和strokeColors 可實現選擇器的效果
     *
     * @param solidColors  填充色
     * @param strokeColors 描邊色
     * @param strokeWidth  描邊線寬度
     * @param dashWidth    虛線(破折線)的長度(以畫素為單位)
     * @param dashGap      虛線(破折線)間距,當dashGap=0dp時,為實線
     * @param radius       圓角角度
     * @return GradientDrawable
     */
    public static Drawable getShapeDrawable(ColorStateList solidColors,
                                            ColorStateList strokeColors, int strokeWidth, 
                                            float dashWidth, float dashGap,
                                            float radius) {
        GradientDrawable gradientDrawable = new GradientDrawable();
        gradientDrawable.setShape(GradientDrawable.RECTANGLE);
        gradientDrawable.setCornerRadius(radius);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            gradientDrawable.setColor(solidColors);
            //顯示一條虛線,破折線的寬度為dashWith,破折線之間的空隙的寬度為dashGap,當dashGap=0dp時,為實線
            gradientDrawable.setStroke(strokeWidth, strokeColors, dashWidth, dashGap);
        } else {
            gradientDrawable.setColor(solidColors.getDefaultColor());
            //顯示一條虛線,破折線的寬度為dashWith,破折線之間的空隙的寬度為dashGap,當dashGap=0dp時,為實線
            gradientDrawable.setStroke(strokeWidth, strokeColors.getDefaultColor(), dashWidth, dashGap);
        }
        return gradientDrawable;
    }
  /**
     * 獲得一個指定填充色,指定描邊色的圓角矩形drawable
     *
     * @param solidColor  填充色
     * @param strokeColor 描邊色
     * @param strokeWidth 描邊線寬度
     * @param dashWidth   虛線(破折線)寬度
     * @param dashGap     虛線(破折線)間距,當dashGap=0dp時,為實線
     * @param radius      圓角角度
     * @return GradientDrawable
     */
    public static Drawable getShapeDrawable(@ColorInt int solidColor,
                                            @ColorInt int strokeColor, int strokeWidth,
                                            float dashWidth, float dashGap,
                                            float radius) {
        return getShapeDrawable(ColorStateList.valueOf(solidColor),
                ColorStateList.valueOf(strokeColor), strokeWidth, dashWidth, dashGap,
                radius);
    }

    /**
     * 獲得一個選擇器Drawable.
     * Android 中 在xml中寫的"selector"標籤對映物件就是StateListDrawable 物件
     *
     * @param defaultDrawable 預設時顯示的Drawable
     * @param pressedDrawable 按下時顯示的Drawable
     * @return 選擇器Drawable
     */
    public static StateListDrawable getSelectorDrawable(Drawable defaultDrawable, Drawable pressedDrawable) {
        if (defaultDrawable == null) return null;
        if (pressedDrawable == null) pressedDrawable = defaultDrawable;
        int[][] state = {{-android.R.attr.state_pressed}, {android.R.attr.state_pressed}};
        StateListDrawable stateListDrawable = new StateListDrawable();
        stateListDrawable.addState(state[0], defaultDrawable);
        stateListDrawable.addState(state[1], pressedDrawable);
        return stateListDrawable;
    }

    /**
     * 獲得一個選擇器Drawable.
     * Android 中 在xml中寫的"selector"標籤對映物件就是StateListDrawable 物件
     *
     * @param defaultColor 預設時顯示的顏色
     * @param pressedColor 按下時顯示的顏色
     * @return 選擇器Drawable
     */
    public static StateListDrawable getSelectorDrawable(int defaultColor, int pressedColor, float radius) {

        Drawable defaultDrawable = getSolidShapeDrawable(defaultColor, radius);
        Drawable pressedDrawable = getSolidShapeDrawable(pressedColor, radius);

        return getSelectorDrawable(defaultDrawable, pressedDrawable);
    }