Android 进阶学习(二十四) Android 中给View 添加Drawable的思考

Android 开发在写界面的过程中离不开和draw.xml打交道就比如要实现下面的效果


image.png

我们需要写一个类似下面的xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >

   <corners android:radius="4dp" />
   <solid android:color="@color/FFF4E3" />
   <stroke android:width="1dp" android:color="@color/FF961E" />
</shape>

其实写一个这样的文件并不难,难点在于不论我们的开发手册对于文件的命名规则定义的有多么规范,由于开发的人数越来越多,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;
   }

   /**
    * 圆角
    * <corners android:radius="3dp"/>
    */
   public TsmGradientDrawable drawRadius(int radius) {
       this.setCornerRadius(DeviceUtil.dp2px(radius));
       return this;
   }

   /**
    * 内部填充颜色
    * <solid android:color=""></solid>
    */
   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;
   }
   /**
    * 圆角
    * 矩形
    * <corners android:radius="3dp"/>
    * android:shape="rectangle"
    */
   public TsmGradientDrawable drawRadiusRect(int radius) {
       drawRect().drawRadius(DeviceUtil.dp2px(radius));
       return this;
   }
   /**
    * 矩形
    * 圆角
    * 颜色
    * <solid android:color=""></solid>
    * <corners android:radius="3dp"/>
    * android:shape="rectangle"
    */
   public TsmGradientDrawable drawRadiusRectColor( int radius, @ColorInt int color) {
       drawRect().drawRadius(radius).drawColor(color);
       return this;
   }

   /**
    * 绘制边界线
    * <p>
    * <stroke  android:width=""  android:color=""></stroke>
    */
   public TsmGradientDrawable drawStroke( int width, @ColorInt int color) {
       this.setStroke(width ,color,0,0);
       return this;
   }
   /**
    * 绘制边界线
    * <stroke  android:width=""  android:color=""></stroke>
    */
   public TsmGradientDrawable drawStroke(int width, ColorStateList colorStateList) {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
           setStroke(DeviceUtil.dp2px(width), colorStateList);
       }
       return this;
   }
   /**
    * 绘制边界线
    * <stroke  android:width=""  android:color=""  android:dashGap="" android:dashWidth="" ></stroke>
    */
   public TsmGradientDrawable drawStroke(int width, @ColorInt int color, float dashWidth, float dashGap) {
       setStroke(width, color, dashWidth, dashGap);
       return this;
   }

   /**
    * 绘制边界线
    * <p>
    * <stroke  android:width=""  android:color=""  android:dashGap="" android:dashWidth="" ></stroke>
    */
   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

   <declare-styleable name="ZRBaseView">
       <attr name="zr_div_selector">
           <enum name="nor" value="0" />
           <enum name="select" value="1" />
           <enum name="checked" value="2" />
           <enum name="enable" value="3" />
           <enum name="pressed" value="4" />
       </attr>
       <attr name="zr_div_radius" format="dimension"/>
       <attr name="zr_div_color" format="color"/>
       <attr name="zr_div_state_color" format="color"/>

       <attr name="zr_div_stroken_width" format="dimension"/>
       <attr name="zr_div_stroken_color" format="color"/>
       <attr name="zr_div_state_stroken_color" format="color"/>
       <attr name="zr_div_stroken_dashgap" format="dimension"/>
       <attr name="zr_div_state_stroken_dashgap" format="dimension"/>
       <attr name="zr_div_stroken_dashwidth" format="dimension"/>
       <attr name="zr_div_state_stroken_dashwidth" format="dimension"/>

       <attr name="zr_div_top_left_radius" format="dimension"/>
       <attr name="zr_div_top_right_radius" format="dimension"/>
       <attr name="zr_div_bottom_left_radius" format="dimension"/>
       <attr name="zr_div_bottom_right_radius" format="dimension"/>
       <attr name="zr_div_top_radius" format="dimension"/>
       <attr name="zr_div_bottom_radius" format="dimension"/>
   </declare-styleable>

基本上所有的draw.xml的属性都写在这个里面了,为了尽可能的应对更多的样式,要是实在是不支持,这个时候再去写draw.xml,我觉得还是可以接受的,
这里在简单的贴一下textView 的declare-styleable ,方便对比一下

       <attr name="zr_div_radius" format="dimension"/>
       <attr name="zr_div_color" format="color"/>
       <attr name="zr_div_state_color" format="color"/>

------------------上面是View的------------------下面是textView 的--------------------------------------------------
    <attr name="tv_div_radius" format="dimension"/>
       <attr name="tv_div_color" format="color"/>
       <attr name="tv_div_state_color" format="color"/>

为了尽量让属性的名字相同,可以看到命名中只有前面控件的缩写是不同的,
此时我们只需要将这些属性与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<ZRDrawableModel, WeakReference<Drawable>>? = 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<Drawable>(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容