内容简介:在Android系统张,图形图像的绘制需要在画布上进行操作和处理,但是绘制需要了解很多细节以及可能要进行一些复杂的处理,因此系统提供了一个被称之为Drawable的类来进行绘制处理。通过这个类可以减少我们的绘制工作和使用成本,同时系统也提供了众多的Drawable的派生类比如单色、图形、位图、裁剪、动画等等来完成一些常见的绘制需求。Drawable是一个抽象的可绘制类。他主要是提供了一个可绘制的区域bound属性以及一个draw成员函数,不同的派生类通过重载draw函数的实现而产生不同的绘制结果。如下是D
一、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("<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源码如下:
@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:
3.1、原理分析
那么,要实现“自定义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.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==0){ this.radius = new float[4]; }else{ this.radius = radius; } for (int i=0;i<radius.length;i++){ float v = radius[i]; if(v<0) 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) { int borderId = canvas.save(); paint.setStyle(Paint.Style.FILL); paint.setColor(this.backgroundColor); canvas.drawPath(mPath, paint); canvas.restoreToCount(borderId); } @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); } @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); shape = new RadiusBorderShape(); shape.setColor(lineColor); shape.setStrokeWidth(lineWidth); shape.setRadius(new float[]{leftTopRadius,rightTopRadius,rightBottomRadius,leftBottomRadius}); shape.setBackgroundColor(backgroundColor); 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加载,前者加载app内置资源,后者加载非app内置资源。通过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,通过自定义的方式去拦截和解析 ,但是由于篇幅问题,这里我们通过一般代码加载。
private Drawable parseDrawable(Context context) { Drawable drawable = null; try{ Resources resources = context.getResources(); XmlResourceParser xmlParse = resources.getXml(R.xml.radius_border); //这种方式可以同样建立缓存 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 null; } return drawable; }
当然,用法我们以ImageView为例
Drawable drawable = parseDrawable(context); 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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Android进阶:十三、自定义类加载器加载加密类文件
- 自定义MVC框架-自动加载类
- SpringBoot 启动分析(四) — 注解驱动的 Bean 定义加载
- Android将html数据加载到chrome自定义标签中
- Spring Boot 基础系列:实现一个自定义配置加载器(应用篇)
- JVM 源码分析之自定义类加载器如何拉长 YGC
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。