Android 經過DrawableInflater加載自定義Drawable

1、Drawable

在Android系統張,圖形圖像的繪製須要在畫布上進行操做和處理,可是繪製須要瞭解不少細節以及可能要進行一些複雜的處理,所以系統提供了一個被稱之爲Drawable的類來進行繪製處理。經過這個類能夠減小咱們的繪製工做和使用成本,同時系統也提供了衆多的Drawable的派生類好比單色、圖形、位圖、裁剪、動畫等等來完成一些常見的繪製需求。Drawable是一個抽象的可繪製類。他主要是提供了一個可繪製的區域bound屬性以及一個draw成員函數,不一樣的派生類經過重載draw函數的實現而產生不一樣的繪製結果。以下是Drawable的加載流程。java

從Resource.getDrawable會判斷是否.xml結尾,不是的話走6,7步,若是從xml中讀取,須要android

getResource.getDrawable -> ResourceImpl.loadDrawableForCookie -> Drawable.createFromXml -> drawableInflater.inflateFromXmlForDensity -> drawable.inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)git

Resources的做用是將整個過程進行了封裝、同時實現了資源的緩存。所以,爲了更加直白的瞭解加載過程,以上步驟咱們能夠精簡以下:github

Drawable.createFromXml -> drawableInflater.inflateFromXmlForDensity -> drawable.inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)canvas

注意:Drawable和drawable,前者是類,後者是類的實例,一樣drawableInflater也是類的實例api

 

2、流程分析和方法解析

Drawable.createFromXml是靜態調用,實際上整個過程是XmlPull的解析。最終,會調用到createFromXmlInnerForDensity緩存

@NonNull
    public static Drawable createFromXmlForDensity(@NonNull Resources r,
            @NonNull XmlPullParser parser, int density, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
        AttributeSet attrs = Xml.asAttributeSet(parser);

        int type;
        //noinspection StatementWithEmptyBody
        while ((type=parser.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            // Empty loop.
        }

        if (type != XmlPullParser.START_TAG) {
            throw new XmlPullParserException("No start tag found");
        }

        Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);

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

        return drawable;
    }

  @NonNull
    static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
            @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
            @Nullable Theme theme) throws XmlPullParserException, IOException {

         //經過Resources裏面的getDrawableInflater獲得DrawableInflater的實例
        return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
                density, theme);
    }

 

drawableInflater.inflateFromXmlForDensity 方法用來加載Drawable資源,若是不是咱們自定義的Drawable類,邏輯流程一般以下解析:app

@NonNull
    public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
       
        if (name.equals("drawable")) { //無心義的drawable
            name = attrs.getAttributeValue(null, "class");
            if (name == null) {
                throw new InflateException("<drawable> tag must specify class attribute");
            }
        }
        Drawable drawable = inflateFromTag(name); //解析處Drawable的實例
        if (drawable == null) {
            drawable = inflateFromClass(name);
        }
        drawable.inflate(mRes, parser, attrs, theme); 
       //獲得drawable實例,經過drawable.inflate去實現屬性的解析
        return drawable;  //返回實例
    }

inflateFromTag源碼以下:ide

@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;
        }
    }

那麼drawable.inflate方法是如何實現的?函數

Drawable自己是抽象類,根據不一樣實現去解析屬性,咱們以ShapeDrawable爲例,通常的經過TypeArray解析當前節點的屬性,若是存在子元素繼續遍歷。

@Override
    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
            throws XmlPullParserException, IOException {
        super.inflate(r, parser, attrs, theme);

        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ShapeDrawable);
        updateStateFromTypedArray(a);
        a.recycle();

        int type;
        final int outerDepth = parser.getDepth();
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();
            // 解析子節點
            if (!inflateTag(name, r, parser, attrs)) {
                android.util.Log.w("drawable", "Unknown element: " + name +
                        " for ShapeDrawable " + this);
            }
        }

        // Update local properties.
        updateLocalState();
    }

 

3、實現自定義Drawable類的加載

一般咱們說的自定義drawable是自定義xml文件,若是實現一種能夠複用而且Android系統中沒有內置的Drawable,此外實現多個佈局文件的引用,固然你能夠說徹底能夠將代碼自定義到靜態方法中,實現屢次引用也是能夠,不過咱們按照Android的建議,圖形化的對象儘可能以xml形式呈現。

下面,咱們定義一個形狀以下的Drawable:

3.一、原理分析


那麼,要實現「自定義Drawable類的加載」需求,好比要進行技術可行性分析,那咱們的依據是什麼呢?

在DrawableInflater中,除了經過inflateFromTag優先解析Drawable以外,咱們發現一樣提供了inflateFromClass,經過這種方式咱們一樣能夠獲得Drawable子類的實例。

Drawable drawable = inflateFromTag(name); //解析處Drawable的實例
        if (drawable == null) {
            drawable = inflateFromClass(name);
        }

inflateFromClass的實現以下:

@NonNull
private Drawable inflateFromClass(@NonNull String className) {
        try {
            Constructor<? extends Drawable> constructor;
            synchronized (CONSTRUCTOR_MAP) {
                constructor = CONSTRUCTOR_MAP.get(className);
                if (constructor == null) {
                    //經過ClassLoader加載Drawable類,而後轉爲Drawable類
                    final Class<? extends Drawable> clazz =
                            mClassLoader.loadClass(className).asSubclass(Drawable.class);
                    constructor = clazz.getConstructor();
                    CONSTRUCTOR_MAP.put(className, constructor);
                }
            }
            return constructor.newInstance();  //建立Drawable實例
        } catch (Exception e) {
           //省略
       }
     return null;
 }

 

注意:咱們經過ClassLoader去加載類,那麼還要注意一個事情就是混淆,混淆時咱們必須注意咱們自定義的Drawable類不能被混淆,不然沒法加載。

-keepclassmembers class * extends android.graphics.drawable.Drawable{
    public void *(android.view.View);
}

 

3.二、代碼實例

[1]定義圖形

首先,咱們須要定義一個Shape圖形,在Android系統中,實現圓角圓弧最好的方式是經過Path實現。

public  class RadiusBorderShape extends Shape {


        private Path mPath;
        @ColorInt
        private int color;  //邊框顏色
        private  float strokeWidth; //線寬
        private float[] radius;  //各個角的radius
        @ColorInt
        private int backgroundColor; //背景填充顏色

    public void setColor(@ColorInt  int color) {
            this.color = color;
        }

  public void setRadius(float[] radius) {

        if(radius==null || radius.length<4){
            this.radius = new float[4];
        }else{
            this.radius = radius;
        }
        for (int i=0;i<this.radius.length;i++){
            float v = this.radius[i];
            if(v<0) {
                this.radius[i] = 0f;
            }
        }
    }

    public void setStrokeWidth(float strokeWidth) {
            if(strokeWidth<0) {
                strokeWidth = 0;
            }
            this.strokeWidth = strokeWidth;
        }

        public RadiusBorderShape(){
            mPath = new Path();
            this.strokeWidth = 5f;
            this.color = Color.RED;
            this.backgroundColor = Color.GREEN;
            this.radius = new float[]{5f,0f,20f,30f};
        }

        @Override
        public void draw(Canvas canvas, Paint paint) {


            Paint.Style  old_style = paint.getStyle();
            int old_color = paint.getColor();
            float old_strokeWidth = paint.getStrokeWidth();


            paint.setStrokeWidth(this.strokeWidth);


            int backgroundId = canvas.save();
            canvas.translate(strokeWidth,strokeWidth);

            drawBackground(canvas, paint);
            drawBorder(canvas, paint);

            canvas.restoreToCount(backgroundId);

            paint.setStyle(old_style);
            paint.setColor(old_color);
            paint.setStrokeWidth(old_strokeWidth);


        }

    private void drawBorder(Canvas canvas, Paint paint) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(this.color);
        canvas.scale(1, 1);
        canvas.drawPath(mPath, paint);

    }

    private void drawBackground(Canvas canvas, Paint paint) {
       final Path.FillType fillType = mPath.getFillType();

        int borderId = canvas.save();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(this.backgroundColor);

        if(this.backgroundColor!=Color.TRANSPARENT){
            mPath.setFillType(Path.FillType.WINDING);  //填充,兼容低版本沒法填充的問題
        }
        canvas.drawPath(mPath, paint);
        canvas.restoreToCount(borderId);
        mPath.setFillType(fillType);//還原
    }

    @Override
        protected void onResize(float width, float height) {
            super.onResize(width, height);

            float w =  width - strokeWidth*2;  //減去左右側的線寬
            float h =  height - strokeWidth*2; //減去上下側的線寬
            mPath.reset();

            if(w<=0 && h<=0){
                return;
            }
            float leftTopThresold = radius[0];
            mPath.moveTo(0,leftTopThresold);
            //從180度處順時針旋轉,增量90度
            mPath.arcTo(new RectF(0,0,leftTopThresold,leftTopThresold),  180f, 90f);

            float rightTopThresold = radius[1];
            mPath.lineTo(w-rightTopThresold,0);
            mPath.arcTo(new RectF(w-rightTopThresold,0,w,rightTopThresold),  270f, 90f);

            float rightBottomThresold = radius[2];
            mPath.lineTo(w,h-rightBottomThresold);
            mPath.arcTo(new RectF(w-rightBottomThresold,h-rightBottomThresold,w,h),  0f, 90f);

            float leftBottomThresold = radius[3];
            mPath.lineTo(leftBottomThresold,h);
            mPath.arcTo(new RectF(0,h-leftBottomThresold,leftBottomThresold,h),  90f, 90f);


            mPath.lineTo(0,leftTopThresold);
            mPath.close();

        }

        @Override
        public Shape clone() throws CloneNotSupportedException {
            final RadiusBorderShape shape = (RadiusBorderShape) super.clone();
            shape.mPath = new Path(mPath);
            shape.radius = radius;
            shape.strokeWidth = strokeWidth;
            shape.color = color;
            return shape;
        }


    public void setBackgroundColor(int backgroundColor) {
            this.backgroundColor = backgroundColor;
    }
}

在這個類中,最終要的2個方法是onResize和draw方法,shape.onResize在Drawable中會被drawable.onBoundsChanged調用,從而實現Drawable大小的監聽。

[2]定義Drawable

public class RadiusRectDrawable extends ShapeDrawable {


    private int backgroundColor;
    private RadiusBorderShape shape;

    @Override
    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) throws XmlPullParserException, IOException {

        TypedArray array = RadiusRectDrawable.obtainAttributes(r, theme, attrs, R.styleable.RadiusRectDrawable);
        if(array==null) return;
        backgroundColor = array.getColor(R.styleable.RadiusRectDrawable_backgroundColor, Color.TRANSPARENT);
        array.recycle();

        super.inflate(r, parser, attrs, theme);
    }


//低版本api兼容
@Override
    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
        TypedArray array = RadiusRectDrawable.obtainAttributes(r, null, attrs, R.styleable.RadiusRectDrawable);
        if(array==null) return;
        backgroundColor = array.getColor(R.styleable.RadiusRectDrawable_backgroundColor, Color.TRANSPARENT);
        array.recycle();
        super.inflate(r, parser, attrs);
    }


    @Override
    protected boolean inflateTag(String name, Resources r, XmlPullParser parser, AttributeSet attrs) {
        if("RadiusBorderShape".equals(name)){

            TypedArray array = r.obtainAttributes(attrs, R.styleable.RadiusRectDrawable);

            int lineColor = array.getColor(R.styleable.RadiusRectDrawable_lineColor, Color.TRANSPARENT);
            float lineWidth = array.getFloat(R.styleable.RadiusRectDrawable_lineWidth, 0f);

            float leftTopRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_leftTop_radius, 0);
            float leftBottomRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_leftBottom_radius, 0);
            float rightTopRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_rightTop_radius, 0);
            float rightBottomRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_rightBottom_radius, 0);

            if(shape==null){
                shape = new RadiusBorderShape();
            }
            shape.setColor(lineColor);
            shape.setStrokeWidth(lineWidth);
            shape.setRadius(new float[]{leftTopRadius,rightTopRadius,rightBottomRadius,leftBottomRadius});
            shape.setBackgroundColor(backgroundColor);
            if(shape!=getShape()){
                setShape(shape);
            }

            array.recycle();

            return true;
        }
        else{
            return super.inflateTag(name, r, parser, attrs);
        }
    }

    protected static @NonNull TypedArray obtainAttributes(@NonNull Resources res,
                                                          @Nullable Resources.Theme theme, @NonNull AttributeSet set, @NonNull int[] attrs) {
        if (theme == null) {
            return res.obtainAttributes(set, attrs);
        }
        return theme.obtainStyledAttributes(set, attrs, 0, 0);
    }
}

這個就是咱們本身定義的Drawable類,固然,自定義每每須要自定義屬性。

<declare-styleable name="RadiusRectDrawable">
        <attr name="lineColor" format="color|reference"/>
        <attr name="backgroundColor" format="color|reference"/>
        <attr name="lineWidth" format="float|reference"/>
        <attr name="leftTop_radius" format="dimension|reference" />
        <attr name="leftBottom_radius" format="dimension|reference" />
        <attr name="rightBottom_radius" format="dimension|reference" />
        <attr name="rightTop_radius" format="dimension|reference" />
    </declare-styleable>

[3]定義drwable文件

自定義drawble的xml文件,安裝慣例應該在drawable資源文件夾下,可是咱們的編譯器表現的有些不友好,要求sdk版本大於24(android 7.0)才行。

從ResourcesImpl.loadDrawableForCookie加載邏輯來看,文件加載主要經過2種方式,文件讀取的核心代碼以下:

if (file.endsWith(".xml")) {
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                    rp.close();
                } else {
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }

通常代碼實際上能夠經過loadXmlResourceParser或者mAssets.openNonAsset加載,前者加載xml文件內置資源,後者加載圖片文件內置資源。經過loadXmlResourceParser加載文件,最後一個參數制定的是drawable,可是從loadXmlResourceParser源碼中並未使用第四個參數(篇幅有限,ResourcesImpl源碼自行查看),也就是說,加載資源時並無對資源文件所在目錄進行校驗。

所以說,編譯器會校驗類型,但運行時不會校驗。這樣咱們能夠將xml文件放置到非drawable目錄,能夠是Assets文件夾中,一樣也能夠是xml資源文件夾下。咱們這裏將定義文件放置到xml資源目錄便可。

 

源碼內容以下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.cc.myapplication.shape.RadiusRectDrawable
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:backgroundColor="@color/white"
    >
    <RadiusBorderShape
        app:lineColor="@color/colorAccent"
        app:lineWidth="5.5"
        app:leftTop_radius="50dip"
        app:leftBottom_radius="0dip"
        app:rightTop_radius="0dip"
        app:rightBottom_radius="0dip"
        />
</com.example.cc.myapplication.shape.RadiusRectDrawable>

 

[4]加載並使用

事實上因爲編譯工具的要求sdk api大於24纔可使用,所以,咱們android:background="@xml/radius_border"顯然存在問題,除非咱們自行實現LayoutInfater.Factory2,經過自定義的方式去攔截和解析,可是因爲篇幅問題,這裏咱們經過通常代碼加載。

public class ResourceUtils {

    private static final HashMap<String, Constructor<? extends Drawable>> CONSTRUCTOR_MAP =
            new HashMap<>();

    private Context context;

    private ResourceUtils(Context context){
        this.context = context;
    }

    public Context getContext() {
        return context;
    }
    //加載drawable
    public  static Drawable getDrawable(Context context, int xmlShapeId){
        try {
            ResourceUtils resourceUtils = new ResourceUtils(context);
            return resourceUtils.parseDrawable(xmlShapeId);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    private Drawable  parseDrawable(int xmlId)  { //R.xml.radius_border)
        Drawable drawable = null;
        try{

            if(Build.VERSION.SDK_INT<24) {
                drawable = parseDrawableFromClass(xmlId);
            }
            if(drawable!=null){
                return drawable;
            }

            Context context = getContext();
            Resources resources = context.getResources();
            XmlResourceParser xmlParse = resources.getXml(xmlId);
            if(Build.VERSION.SDK_INT>=21) {
                drawable = Drawable.createFromXml(resources, xmlParse, context.getTheme());
            }else{
                drawable = Drawable.createFromXml(resources, xmlParse);
            }
            xmlParse.close();
        }catch (Exception e){
            e.printStackTrace();
        }
        return drawable;
    }

    private Drawable parseDrawableFromClass(int xmlId){
        Drawable drawable = null;
        try {
            Context context = getContext();

            Resources resources = context.getResources();
            XmlResourceParser xmlParse = resources.getXml(xmlId);

            AttributeSet attrs = Xml.asAttributeSet(xmlParse);
            int type;
            while ((type = xmlParse.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
            }

            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }
            drawable = inflateFromClass(xmlParse.getName());
            if(drawable==null) return null;

            if (Build.VERSION.SDK_INT >= 21) {
                drawable.inflate(resources, xmlParse, attrs, context.getTheme());
            } else {
                drawable.inflate(resources, xmlParse, attrs);

            }
        }catch (Exception e){
            e.printStackTrace();
        }

        return  drawable;
    }


    @NonNull
    private Drawable inflateFromClass(@NonNull String className) {
        try {
            Constructor<? extends Drawable> constructor;
            synchronized (CONSTRUCTOR_MAP) {
                constructor = CONSTRUCTOR_MAP.get(className);
                if (constructor == null) {
                    //經過ClassLoader加載Drawable類,而後轉爲Drawable類
                    final Class<? extends Drawable> clazz =
                            getClass().getClassLoader().loadClass(className).asSubclass(Drawable.class);
                    constructor = clazz.getConstructor();
                    CONSTRUCTOR_MAP.put(className, constructor);
                }
            }
            return constructor.newInstance();  //建立Drawable實例
        } catch (Exception e) {
            //省略
        }
        return null;
    }
}

固然,用法咱們以ImageView爲例

Drawable drawable = ResourceUtils.getDrawable(mContext,R.xml.radius_border);

myImageView.setBackgroundDrawable(drawable);

 

4、總結

咱們經過這種方式成功實現了自定義Drawable的加載,DrawableInflater做爲加載引擎和路由,咱們應該充分利用這種關係,做爲Inflater,一樣LayoutInflater.Factory值得咱們去實踐。

 

附錄:

1)LayoutInflater.Factory2加載機制請參閱以下連接:

https://my.oschina.net/ososchina/blog/405904

2)DrawableInflater請參閱以下連接:

https://github.com/aosp-mirror/platform_frameworks_base/blob/master/graphics/java/android/graphics/drawable/DrawableInflater.java

相關文章
相關標籤/搜索