天天看點

android drawable轉bitmap_Android 通過DrawableInflater加載自定義Drawable

一、Drawable

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

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

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

Resources的作用是将整個過程進行了封裝、同時實作了資源的緩存。是以,為了更加直白的了解加載過程,以上步驟我們可以精簡如下:

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

注意:Drawable和drawable,前者是類,後者是類的執行個體,同樣drawableInflater也是類的執行個體。

二、流程分析和方法解析

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類,邏輯流程通常如下解析:

@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(" 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源碼如下:

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

三、實作自定義Drawable類的加載

通常我們說的自定義drawable是自定義xml檔案,如果實作一種可以複用并且Android系統中沒有内置的Drawable,此外實作多個布局檔案的引用,當然你可以說完全可以将代碼自定義到靜态方法中,實作多次引用也是可以,不過我們按照Android的建議,圖形化的對象盡量以xml形式呈現。

下面,我們定義一個形狀如下的Drawable:

android drawable轉bitmap_Android 通過DrawableInflater加載自定義Drawable
android drawable轉bitmap_Android 通過DrawableInflater加載自定義Drawable
android drawable轉bitmap_Android 通過DrawableInflater加載自定義Drawable

3.1、原理分析

那麼,要實作“自定義Drawable類的加載”需求,比如要進行技術可行性分析,那我們的依據是什麼呢?

在DrawableInflater中,除了通過inflateFromTag優先解析Drawable之外,我們發現同樣提供了inflateFromClass,通過這種方式我們同樣可以得到Drawable子類的執行個體。

Drawable drawable = inflateFromTag(name); //解析處Drawable的執行個體        if (drawable == null) {            drawable = inflateFromClass(name);        }
           

inflateFromClass的實作如下:

@NonNullprivate 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.2、代碼執行個體

[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
           

在這個類中,最終要的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類,當然,自定義往往需要自定義屬性。

[3]定義drwable檔案

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

android drawable轉bitmap_Android 通過DrawableInflater加載自定義Drawable

從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資源目錄即可。

android drawable轉bitmap_Android 通過DrawableInflater加載自定義Drawable

源碼内容如下:

<?xml version="1.0" encoding="utf-8"?>
           

[4]加載并使用

事實上由于編譯工具的要求sdk api大于24才可以使用,是以,我們android:background="@xml/radius_border"顯然存在問題,除非我們自行實作LayoutInfater.Factory2,通過自定義的方式去攔截和解析,但是由于篇幅問題,這裡我們通過一般代碼加載。

public class ResourceUtils {    private static final HashMap> 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);
           

四、總結

我們通過這種方式成功實作了自定義Drawable的加載,DrawableInflater作為加載引擎和路由,我們應該充分利用這種關系,作為Inflater,同樣LayoutInflater.Factory值得我們去實踐。