在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
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(); }
一般咱們說的自定義drawable是自定義xml文件,若是實現一種能夠複用而且Android系統中沒有內置的Drawable,此外實現多個佈局文件的引用,固然你能夠說徹底能夠將代碼自定義到靜態方法中,實現屢次引用也是能夠,不過咱們按照Android的建議,圖形化的對象儘可能以xml形式呈現。
下面,咱們定義一個形狀以下的Drawable:
那麼,要實現「自定義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); }
[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);
咱們經過這種方式成功實現了自定義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