Android 开发在写界面的过程中离不开和draw.xml打交道就比如要实现下面的效果
我们需要写一个类似下面的xml
其实写一个这样的文件并不难,难点在于不论我们的开发手册对于文件的命名规则定义的有多么规范,由于开发的人数越来越多,draw的复杂性 导致文件名过长,去找原有的xml的这个过程,往往都在5分钟以上,但是重写一个xml的时间只需要1-2分钟,这样就不得不让人深思,如何才能将这个时间节省下来呢,并能给大家带来一个好的开发体验,
首先我们先来思考一个问题,所有的xml文件,都是在xmlparser中解析成bean ,提供给java使用的,我们draw.xml如果是一个shape 则被解析成了GradientDrawable,如果他是一个selector 则被解析成了StateListDrawable,通过查看代码发现StateListDrawable 就是包装了不同状态下的GradientDrawable,那么draw.xml 的这个过程是不是可以手动使用代码来写呢,这样可以节省掉解析xml所消耗掉的时间,虽然这个时间小到可以忽略不计,
我们依据这个方法可以封装一下GradientDrawable,让他变成链式构建,方便使用
public class TsmGradientDrawable extends GradientDrawable {
public static TsmGradientDrawable getInstance() {
return new TsmGradientDrawable();
}
/**
* 圆角 填充颜色
*/
public static TsmGradientDrawable getRadiusRectColorDrawable(int radius, int color){
return TsmGradientDrawable.getInstance().drawRadiusRectColor(radius,color);
}
/**
* 圆角
* 填充颜色
* 外部实线
*/
public static TsmGradientDrawable getStrokeRadiusColorDrawable(int radius, int contentColor,int strokeColor,int strokeWidth){
return TsmGradientDrawable.getInstance().drawRadiusRectColor(radius,contentColor).drawStroke(strokeWidth,strokeColor);
}
/**
* android:shape="rectangle"
* 矩形
*/
public TsmGradientDrawable drawRect() {
this.setShape(GradientDrawable.RECTANGLE);
return this;
}
/**
* 圆角
*
*/
public TsmGradientDrawable drawRadius(int radius) {
this.setCornerRadius(DeviceUtil.dp2px(radius));
return this;
}
/**
* 内部填充颜色
*
*/
public TsmGradientDrawable drawColor(@ColorInt int color) {
setColor(color);
return this;
}
public TsmGradientDrawable size(int width, int height){
setSize(DeviceUtil.dp2px(width),DeviceUtil.dp2px(height));
return this;
}
/**
* 前2个是 top left
* 3-4 是 top right
* 5-6 bottom left
* 7-8 bottom right
* android:topLeftRadius=""
* android:topRightRadius=""
* android:bottomLeftRadius=""
* android:bottomRightRadius=""
*/
public TsmGradientDrawable drawRadius(float[] radius) {
for (int i = 0; i < radius.length; i++) {
radius[i] = DeviceUtil.dp2px(radius[i]);
}
this.setCornerRadii(radius);
return this;
}
public TsmGradientDrawable drawRadiusDp(float[] radius) {
this.setCornerRadii(radius);
return this;
}
/**
* 圆角
* 矩形
*
* android:shape="rectangle"
*/
public TsmGradientDrawable drawRadiusRect(int radius) {
drawRect().drawRadius(DeviceUtil.dp2px(radius));
return this;
}
/**
* 矩形
* 圆角
* 颜色
*
*
* android:shape="rectangle"
*/
public TsmGradientDrawable drawRadiusRectColor( int radius, @ColorInt int color) {
drawRect().drawRadius(radius).drawColor(color);
return this;
}
/**
* 绘制边界线
*
*
*/
public TsmGradientDrawable drawStroke( int width, @ColorInt int color) {
this.setStroke(width ,color,0,0);
return this;
}
/**
* 绘制边界线
*
*/
public TsmGradientDrawable drawStroke(int width, ColorStateList colorStateList) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setStroke(DeviceUtil.dp2px(width), colorStateList);
}
return this;
}
/**
* 绘制边界线
*
*/
public TsmGradientDrawable drawStroke(int width, @ColorInt int color, float dashWidth, float dashGap) {
setStroke(width, color, dashWidth, dashGap);
return this;
}
/**
* 绘制边界线
*
*
*/
public TsmGradientDrawable drawStroke(Context context, int width, ColorStateList colorStateList, float dashWidth, float dashGap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setStroke(DeviceUtil.dp2px(width), colorStateList, dashWidth, dashGap);
}
return this;
}
/**
* 绘制圆形
*/
public TsmGradientDrawable drawaOval() {
this.setShape(GradientDrawable.OVAL);
return this;
}
/**
* 绘制图形
*/
public TsmGradientDrawable drawShape( int shape) {
this.setShape(shape);
return this;
}
}
通过上面的这个类可以让我们用非常简单的代码就可以写出一个和draw.xml效果一样的背景,但是在使用的过程中发现,所有需要使用背景的view都需要在代码中是用这个方式来设置背景,这样来说其实并没有提升发开效率,
那么我们如何做才能将这个drawable 和View 绑定一起呢,通过写layout 的属性将draw写到一起呢,最简单最直接的方法就是通过自定义view 来实现,但是这就意味着我们需要针对不同种的view 来写不同的 declare-styleable ,原因是只有declare-styleable 的name 与 View的class文件名相同才会在编写xml过程中出现提示,否则不会出现提示,但是不同的view的declare-styleable 的命名不能完全一致,否则编译不过,这样就必须每一种view 都要写declare-styleable ,所以我粗略的统计了一下我需要书写的declare-styleable对应的view,他们分别是
//自定义view时需要继承自这个View
View
Button
EditText
TextView
ConstraintLayout
FrameLayout
LinearLayout
RelativeLayout
这里我贴一下View 的 declare-styleable
基本上所有的draw.xml的属性都写在这个里面了,为了尽可能的应对更多的样式,要是实在是不支持,这个时候再去写draw.xml,我觉得还是可以接受的,
这里在简单的贴一下textView 的declare-styleable ,方便对比一下
------------------上面是View的------------------下面是textView 的--------------------------------------------------
为了尽量让属性的名字相同,可以看到命名中只有前面控件的缩写是不同的,
此时我们只需要将这些属性与GradientDrawable 对应起来就可以实现不需要写drawable 来实现我么的效果了
fun initViews(context: Context, attributeSet: AttributeSet?) {
attributeSet?.let {
val type: TypedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ZRBaseView)
var model=ZRDrawableModel()
/**
* selector
*/
model.div_selector=type.getInt(R.styleable.ZiRoomBaseView_zr_div_selector,0)
/**
* 颜色
*/
model.div_color=type.getColor(R.styleable.ZiRoomBaseView_zr_div_color,0)
model.div_state_color=type.getColor(R.styleable.ZiRoomBaseView_zr_div_state_color,0)
/**
* 圆角
*/
model.div_top_left_radius=type.getDimension(R.styleable.ZiRoomBaseView_zr_div_top_left_radius,0f)
model.div_top_right_radius=type.getDimension(R.styleable.ZiRoomBaseView_zr_div_top_right_radius,0f)
....
/**
* 虚线
*/
model.div_stroken_color=type.getColor(R.styleable.ZiRoomBaseView_zr_div_stroken_color,0)
.....
type.recycle()
model?.drawDrawable()?.let {
background=it
}
}
}
上面的代码就是通过创建一个drawableModel 来构建一个drawable, 此时我们最开始的需求到这里就已经结束了
但是在阅读Resources 的getDrawable()的方法中发现并不是每一次加载drawable 都需要重新解析xml的,他在内存做了一个缓存,来尽可能的节省创建drawable的成本
ResourcesImpl.class
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
cachedDrawable.setChangingConfigurations(value.changingConfigurations);
return cachedDrawable;
}
}
final Drawable.ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
boolean needsNewDrawableAfterCache = false;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, density);
}
}
从上面的代码可以看出来他是缓存了Drawable.ConstantState,每次遇到需要创建draw的时候则利用这个Drawable.ConstantState来重新创建一个draw,从一些文章了解到Drawable.ConstantState中保存了draw中的一些必要属性,不需要每次都去解析xml,加快构建速度,反过来再看我们这边的修改就有一点low了,每次创建draw都需要重新创建Drawable.ConstantState,为了尽可能的还原系统的工作效率,我写了一个lru 的弱引用缓存来保存draw
class ZRDrawableCacheHelper {
/**
* 弱引用缓存
*/
private var linkedHashMap: LruCache>? = null
private constructor() {
/**
* 最大100个
*/
linkedHashMap = LruCache(100)
}
companion object {
@Volatile
private var helper: ZRDrawableCacheHelper? = null
fun getInstance(): ZRDrawableCacheHelper {
if (helper == null) {
synchronized(ZRDrawableCacheHelper::class.java) {
if (helper == null) {
helper = ZRDrawableCacheHelper()
}
}
}
return helper!!
}
}
/**
* 获取
*/
fun getDrawable(model:ZRDrawableModel):Drawable?{
return linkedHashMap?.get(model)?.get()
}
fun cacheDrawable(model:ZRDrawableModel?, draw : Drawable?){
model?.let {
draw?.let {
linkedHashMap?.put(model,WeakReference(draw))
}
}
}
}
他的使用方式则是每次通过DrawModel构建draw之前,先去缓存中查看一下是否存在缓存,如果不存在才重新创建drawable,并加入缓存中,这样创建draw的过程就变成了
object ZRDrawableCreateUtils {
/**
* 通过属性创建Drawable
*/
fun drawDrawable(model :ZRDrawableModel?):Drawable?{
if (model==null||!model.isDrawable)
return null
///获取缓存drawable
var drawable =ZRDrawableCacheHelper.getInstance().getDrawable(model)
if(drawable!=null){
return drawable
}
if (model.div_selector > 0) {
drawable = drawColor(model.div_color, null)
drawable = drawRadius(drawable, model.div_top_left_radius, model.div_top_right_radius, model.div_bottom_left_radius, model.div_bottom_right_radius, model.div_top_radius, model.div_bottom_radius, model.div_radius)
drawable = drawStroken(model.stroken_width, model.div_stroken_color, model.stroken_dashgap, model.stroken_dashwidth, drawable)
var stateDrawable: ZiRoomGradientDrawable? = null
stateDrawable = drawColor(model.div_state_color, stateDrawable)
stateDrawable = drawRadius(stateDrawable, model.div_top_left_radius, model.div_top_right_radius, model.div_bottom_left_radius, model.div_bottom_right_radius, model.div_top_radius, model.div_bottom_radius, model.div_radius)
stateDrawable = drawStroken(model.stroken_width, model.div_state_stroken_color, model.stroken_dashgap, model.stroken_dashwidth, stateDrawable)
var draw =drawSelector(model.div_selector, drawable, stateDrawable)
ZRDrawableCacheHelper.getInstance().cacheDrawable(model,draw)
return draw
}
if (model.isOnlyColorDrawable)/// 颜色不用缓存
return ColorDrawable(model.div_color)
drawable = drawColor(model.div_color, null)
drawable = drawRadius(drawable, model.div_top_left_radius, model.div_top_right_radius, model.div_bottom_left_radius, model.div_bottom_right_radius, model.div_top_radius, model.div_bottom_radius, model.div_radius)
drawable = drawStroken(model.stroken_width, model.div_stroken_color, model.stroken_dashgap, model.stroken_dashwidth, drawable)
ZRDrawableCacheHelper.getInstance().cacheDrawable(model,drawable)
return drawable
}
/**
* 绘制边界线
*/
@JvmStatic
fun drawStroken(width: Float, color: Int, gap: Float, dsshWidth: Float, drawable: ZiRoomGradientDrawable?): ZiRoomGradientDrawable? {
if (width > 0f && color != 0) {
return getZiRoomViewDraw(drawable).drawStroke(width.toInt(), color, dsshWidth, gap)
}
return drawable
}
/**
* 绘制填充色
*/
@JvmStatic
fun drawColor(color: Int, drawable: ZiRoomGradientDrawable?): ZiRoomGradientDrawable? {
if (color != 0) {
return getZiRoomViewDraw(drawable).drawColor(color)
}
return drawable
}
/**
* 绘制圆角
*/
@JvmStatic
fun drawRadius(drawable: ZiRoomGradientDrawable?, topLeft: Float, topRight: Float, bottomLeft: Float, bottomRight: Float, topRadius: Float, bottomRadius: Float, radius: Float): ZiRoomGradientDrawable? {
var totleRadius = topLeft + topRight + bottomLeft + bottomRight + topRadius + bottomRadius + radius
if (totleRadius <= 0) {
return drawable
}
var array = FloatArray(8)
if (radius > 0) {
array[0] = radius
array[1] = radius
array[2] = radius
array[3] = radius
array[4] = radius
array[5] = radius
array[6] = radius
array[7] = radius
}
if (topRadius > 0) {
array[0] = topRadius
array[1] = topRadius
array[2] = topRadius
array[3] = topRadius
}
if (bottomRadius > 0) {
array[4] = bottomRadius
array[5] = bottomRadius
array[6] = bottomRadius
array[7] = bottomRadius
}
if (topLeft > 0) {
array[0] = topLeft
array[1] = topLeft
}
if (topRight > 0) {
array[2] = topRight
array[3] = topRight
}
if (bottomLeft > 0) {
array[6] = bottomLeft
array[7] = bottomLeft
}
if (bottomRight > 0) {
array[4] = bottomRight
array[5] = bottomRight
}
return getZiRoomViewDraw(drawable).drawRadiusDp(array)
}
@JvmStatic
fun drawSelector(selector: Int, drawable: ZiRoomGradientDrawable?, stateDrawable: ZiRoomGradientDrawable?): ZiRoomStateListDrawable? {
drawable?.let {
stateDrawable?.let {
when (selector) {
1 -> {
return ZiRoomStateListDrawable().setSelectDrawable(stateDrawable, drawable)
}
2 -> {
return ZiRoomStateListDrawable().setCheckableeDrawable(stateDrawable, drawable)
}
3 -> {
return ZiRoomStateListDrawable().setEnableDrawable(stateDrawable, drawable)
}
4 -> {
return ZiRoomStateListDrawable().setPressedDrawable(stateDrawable, drawable)
}
else ->
return null
}
}
}
return null
}
/**
* 获取drawable
*/
@JvmStatic
fun getZiRoomViewDraw(drawable: ZiRoomGradientDrawable?): ZiRoomGradientDrawable {
return drawable ?: ZiRoomGradientDrawable().drawRect()
}
}
通过查看Resources的源码还解开了我一个疑惑,那就是在修改View 的draw过程中如果修改了当前的View 的drwable 的颜色,会影响到其他使用同样draw的控件,这并不是一个系统的bug,只不过是我们使用的方式不对
((GradientDrawable)getResources().getDrawable(R.drawable.bg).getConstantState().newDrawable().mutate()).setColor(Color.RED);
应该是先获取Drawable,在获取共享的ConstantState,利用ConstantState重新创建一个drawable,在里面mutate方法重新创建一个ConstantState依赖给Drawable,最后再设置你需要的样式
最后,在使用过程中发现由于drawable缓存了,由于drawable 缓存了了View 的宽高,在复用drawable 的过程中,两个view同时修改drawable的宽高,导致部分drawable显示出现问题,所以在创建drawable的过程中不应该直接缓存drawable,而是缓存Drawable.ConstantState,获取到缓存后,利用Drawable.ConstantState 重新创建一个drawable